blob: b0649ee1341008e6d25eda23483f1420d481a0e9 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { DatabaseSync } from 'node:sqlite';
import {
decodePersistedLegacyRunHeader,
invocationOpeningFromLegacyRunHeader,
type LegacyRunHeader,
} from './legacy-run-header.js';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import { encodeCanonicalRuntimeEvent } from '@maka/core/canonical-runtime-event';
import {
rebuildTranscriptTurnExtents,
TERMINAL_RUNTIME_EVENT_SQL,
} from './runtime-transcript-query.js';
import {
buildInvocationOpenedEvent,
buildSyntheticTerminalRuntimeEvent,
} from '@maka/core/runtime-invocation';
export const SQLITE_RUNTIME_SCHEMA_VERSION = 19;
export const RUNTIME_RECOVERY_AUTHORITY_CAPABILITY = 'runtime_recovery_authority';
export const RUNTIME_RECOVERY_AUTHORITY_CAPABILITY_VERSION = 1;
export const RUNTIME_CONTINUATION_AUTHORITY_CAPABILITY = 'runtime_continuation_authority';
export const RUNTIME_CONTINUATION_AUTHORITY_CAPABILITY_VERSION = 1;
export const RUNTIME_WORKSPACE_VERSION_AUTHORITY_CAPABILITY = 'runtime_workspace_version_authority';
export const RUNTIME_WORKSPACE_VERSION_AUTHORITY_CAPABILITY_VERSION = 1;
const SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS = 5_000;
const SQLITE_INITIALIZATION_RETRY_DELAY_MS = 10;
const initializationRetryGate = new Int32Array(new SharedArrayBuffer(4));
const MIGRATIONS: ReadonlyMap<number, string> = new Map([
[
1,
`
CREATE TABLE runtime_events (
event_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
invocation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
event_seq INTEGER NOT NULL CHECK (event_seq > 0),
event_kind TEXT NOT NULL,
payload_json TEXT NOT NULL,
committed_at INTEGER NOT NULL,
UNIQUE (invocation_id, event_seq)
);
CREATE INDEX runtime_events_by_run
ON runtime_events(session_id, run_id, event_seq);
CREATE INDEX runtime_events_by_session
ON runtime_events(session_id, committed_at, event_id);
CREATE TABLE tool_journal_events (
journal_seq INTEGER PRIMARY KEY AUTOINCREMENT,
journal_event_id TEXT NOT NULL UNIQUE,
operation_id TEXT NOT NULL,
invocation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
state TEXT NOT NULL,
runtime_event_id TEXT,
canonical_args_hash TEXT,
recovery_mode TEXT,
external_handle TEXT,
metadata_json TEXT,
committed_at INTEGER NOT NULL,
FOREIGN KEY(runtime_event_id) REFERENCES runtime_events(event_id)
);
CREATE INDEX tool_journal_events_by_operation
ON tool_journal_events(operation_id, journal_seq);
CREATE TABLE tool_operations (
operation_id TEXT PRIMARY KEY,
invocation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
provider_tool_call_id TEXT NOT NULL,
tool_name TEXT NOT NULL,
canonical_args_hash TEXT NOT NULL,
recovery_mode TEXT NOT NULL,
current_state TEXT NOT NULL,
call_event_id TEXT NOT NULL,
result_event_id TEXT,
version INTEGER NOT NULL CHECK (version > 0),
FOREIGN KEY(call_event_id) REFERENCES runtime_events(event_id),
FOREIGN KEY(result_event_id) REFERENCES runtime_events(event_id),
UNIQUE(invocation_id, provider_tool_call_id)
);
`,
],
[
2,
`
CREATE TABLE runtime_partial_snapshots (
stream_key TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
invocation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
after_event_id TEXT,
payload_json TEXT NOT NULL,
text_content TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX runtime_partial_snapshots_by_run
ON runtime_partial_snapshots(session_id, run_id, updated_at, stream_key);
`,
],
[
3,
`
SELECT 1;
`,
],
[
4,
`
ALTER TABLE tool_operations ADD COLUMN dispatch_event_id TEXT
REFERENCES runtime_events(event_id);
`,
],
[
5,
`
CREATE TABLE runtime_capabilities (
capability TEXT PRIMARY KEY,
version INTEGER NOT NULL CHECK (version > 0)
);
INSERT INTO runtime_capabilities(capability, version)
VALUES ('runtime_recovery_authority', 1);
`,
],
[
6,
`
CREATE TABLE runtime_continuation_claims (
claim_id TEXT PRIMARY KEY,
source_session_id TEXT NOT NULL,
source_invocation_id TEXT NOT NULL,
source_run_id TEXT NOT NULL,
source_turn_id TEXT NOT NULL,
source_event_high_water INTEGER NOT NULL CHECK (source_event_high_water > 0),
source_prefix_digest TEXT NOT NULL,
boundary_digest TEXT NOT NULL UNIQUE,
boundary_json TEXT NOT NULL,
provider_projection_version INTEGER NOT NULL CHECK (provider_projection_version = 1),
provider_replay_digest TEXT NOT NULL,
target_session_id TEXT NOT NULL,
target_invocation_id TEXT NOT NULL UNIQUE,
target_run_id TEXT NOT NULL UNIQUE,
target_turn_id TEXT NOT NULL,
target_run_header_json TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
start_event_id TEXT UNIQUE REFERENCES runtime_events(event_id),
start_kind TEXT CHECK (
start_kind IS NULL OR start_kind IN ('runtime_admission', 'claim_repair')
),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
UNIQUE (
source_session_id,
source_run_id,
source_event_high_water,
source_prefix_digest
),
UNIQUE (target_session_id, target_turn_id)
);
INSERT INTO runtime_capabilities(capability, version)
VALUES ('runtime_continuation_authority', 1);
`,
],
[
7,
`
CREATE TABLE runtime_workspace_epochs (
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL UNIQUE,
repository_id TEXT NOT NULL,
workspace_instance_id TEXT NOT NULL UNIQUE,
mode TEXT NOT NULL CHECK (mode = 'managed_worktree'),
object_format TEXT NOT NULL CHECK (object_format IN ('sha1', 'sha256')),
source_commit_oid TEXT NOT NULL,
source_tree_oid TEXT NOT NULL,
initial_workspace_version_id TEXT NOT NULL UNIQUE,
materialization_profile_digest TEXT NOT NULL,
materialization_semantics TEXT NOT NULL
CHECK (materialization_semantics = 'git_tree_materialized_with_fixed_config_v1'),
policy_hash TEXT NOT NULL,
authority_session_id TEXT NOT NULL CHECK (authority_session_id = 'maka_workspace_authority'),
authority_invocation_id TEXT NOT NULL UNIQUE,
authority_run_id TEXT NOT NULL UNIQUE,
authority_turn_id TEXT NOT NULL UNIQUE,
epoch_opened_event_id TEXT NOT NULL UNIQUE REFERENCES runtime_events(event_id),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
committed_at INTEGER NOT NULL,
PRIMARY KEY (workspace_id, workspace_epoch_id)
);
CREATE TABLE runtime_workspace_versions (
workspace_version_id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL,
object_format TEXT NOT NULL CHECK (object_format IN ('sha1', 'sha256')),
origin_kind TEXT NOT NULL CHECK (origin_kind = 'baseline'),
origin_event_id TEXT NOT NULL,
parents_json TEXT NOT NULL CHECK (parents_json = '[]'),
commit_oid TEXT NOT NULL,
tree_oid TEXT NOT NULL,
policy_hash TEXT NOT NULL,
tree_delta_digest TEXT NOT NULL,
changed_file_count INTEGER NOT NULL CHECK (changed_file_count >= 0),
deleted_file_count INTEGER NOT NULL CHECK (deleted_file_count = 0),
accepted_event_id TEXT NOT NULL UNIQUE REFERENCES runtime_events(event_id),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
committed_at INTEGER NOT NULL,
FOREIGN KEY (workspace_id, workspace_epoch_id)
REFERENCES runtime_workspace_epochs(workspace_id, workspace_epoch_id),
UNIQUE (
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
)
);
CREATE TABLE runtime_workspace_heads (
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
workspace_version_id TEXT NOT NULL,
accepted_event_id TEXT NOT NULL,
commit_oid TEXT NOT NULL,
tree_oid TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision > 0),
PRIMARY KEY (workspace_id, workspace_epoch_id),
FOREIGN KEY (workspace_id, workspace_epoch_id)
REFERENCES runtime_workspace_epochs(workspace_id, workspace_epoch_id),
FOREIGN KEY (
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
) REFERENCES runtime_workspace_versions(
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
)
);
INSERT INTO runtime_capabilities(capability, version)
VALUES ('runtime_workspace_version_authority', 1);
`,
],
[
8,
`
SELECT 1;
`,
],
[
9,
`
CREATE TABLE runtime_storage_root_binding (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
root_id TEXT NOT NULL CHECK (
length(root_id) = 64 AND root_id NOT GLOB '*[^0-9a-f]*'
),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1)
);
`,
],
[
10,
`
CREATE TABLE runtime_partial_segments (
stream_key TEXT NOT NULL,
segment_seq INTEGER NOT NULL CHECK (segment_seq > 0),
text_content TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (stream_key, segment_seq),
FOREIGN KEY (stream_key)
REFERENCES runtime_partial_snapshots(stream_key)
ON DELETE CASCADE
);
`,
],
[
11,
`
CREATE TABLE runtime_session_event_ordinals (
session_id TEXT NOT NULL,
ordinal INTEGER NOT NULL CHECK (ordinal > 0),
event_id TEXT NOT NULL UNIQUE,
PRIMARY KEY (session_id, ordinal),
FOREIGN KEY (event_id) REFERENCES runtime_events(event_id) ON DELETE CASCADE
) WITHOUT ROWID;
INSERT INTO runtime_session_event_ordinals(session_id, ordinal, event_id)
SELECT
session_id,
ROW_NUMBER() OVER (
PARTITION BY session_id
ORDER BY rowid ASC
),
event_id
FROM runtime_events;
`,
],
[
12,
`
DROP TABLE IF EXISTS headless_task_run_events;
`,
],
[
13,
`
ALTER TABLE runtime_workspace_heads RENAME TO runtime_workspace_heads_v12;
ALTER TABLE runtime_workspace_versions RENAME TO runtime_workspace_versions_v12;
CREATE TABLE runtime_workspace_versions (
workspace_version_id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL,
object_format TEXT NOT NULL CHECK (object_format IN ('sha1', 'sha256')),
origin_kind TEXT NOT NULL CHECK (origin_kind IN ('baseline', 'tool_mutation')),
origin_event_id TEXT NOT NULL,
parents_json TEXT NOT NULL,
operation_id TEXT,
dispatch_event_id TEXT REFERENCES runtime_events(event_id),
outcome_event_id TEXT REFERENCES runtime_events(event_id),
base_head_revision INTEGER CHECK (base_head_revision IS NULL OR base_head_revision > 0),
execution_profile_digest TEXT,
commit_oid TEXT NOT NULL,
tree_oid TEXT NOT NULL,
policy_hash TEXT NOT NULL,
tree_delta_digest TEXT NOT NULL,
changed_file_count INTEGER NOT NULL CHECK (changed_file_count >= 0),
deleted_file_count INTEGER NOT NULL CHECK (deleted_file_count >= 0),
accepted_event_id TEXT NOT NULL UNIQUE REFERENCES runtime_events(event_id),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
committed_at INTEGER NOT NULL,
FOREIGN KEY (workspace_id, workspace_epoch_id)
REFERENCES runtime_workspace_epochs(workspace_id, workspace_epoch_id),
UNIQUE (
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
),
CHECK (
(origin_kind = 'baseline' AND parents_json = '[]' AND operation_id IS NULL
AND dispatch_event_id IS NULL AND outcome_event_id IS NULL
AND base_head_revision IS NULL AND execution_profile_digest IS NULL)
OR
(origin_kind = 'tool_mutation' AND parents_json <> '[]' AND operation_id IS NOT NULL
AND dispatch_event_id IS NOT NULL AND outcome_event_id IS NOT NULL
AND base_head_revision IS NOT NULL AND execution_profile_digest IS NOT NULL)
)
);
INSERT INTO runtime_workspace_versions (
workspace_version_id, repository_id, workspace_id, workspace_epoch_id,
object_format, origin_kind, origin_event_id, parents_json,
operation_id, dispatch_event_id, outcome_event_id, base_head_revision,
execution_profile_digest, commit_oid, tree_oid, policy_hash,
tree_delta_digest, changed_file_count, deleted_file_count,
accepted_event_id, protocol_version, committed_at
)
SELECT
workspace_version_id, repository_id, workspace_id, workspace_epoch_id,
object_format, origin_kind, origin_event_id, parents_json,
NULL, NULL, NULL, NULL, NULL, commit_oid, tree_oid, policy_hash,
tree_delta_digest, changed_file_count, deleted_file_count,
accepted_event_id, protocol_version, committed_at
FROM runtime_workspace_versions_v12;
CREATE TABLE runtime_workspace_heads (
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL,
repository_id TEXT NOT NULL,
workspace_version_id TEXT NOT NULL,
accepted_event_id TEXT NOT NULL,
commit_oid TEXT NOT NULL,
tree_oid TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision > 0),
PRIMARY KEY (workspace_id, workspace_epoch_id),
FOREIGN KEY (workspace_id, workspace_epoch_id)
REFERENCES runtime_workspace_epochs(workspace_id, workspace_epoch_id),
FOREIGN KEY (
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
) REFERENCES runtime_workspace_versions(
workspace_id,
workspace_epoch_id,
workspace_version_id,
accepted_event_id
)
);
INSERT INTO runtime_workspace_heads
SELECT * FROM runtime_workspace_heads_v12;
DROP TABLE runtime_workspace_heads_v12;
DROP TABLE runtime_workspace_versions_v12;
`,
],
[
14,
`
ALTER TABLE runtime_workspace_versions
ADD COLUMN changed_paths_json TEXT NOT NULL DEFAULT '[]';
CREATE TABLE runtime_managed_mutation_reservations (
workspace_instance_id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL,
workspace_id TEXT NOT NULL,
workspace_epoch_id TEXT NOT NULL,
operation_id TEXT NOT NULL UNIQUE REFERENCES tool_operations(operation_id),
dispatch_event_id TEXT NOT NULL UNIQUE REFERENCES runtime_events(event_id),
base_workspace_version_id TEXT NOT NULL,
base_accepted_event_id TEXT NOT NULL,
base_head_revision INTEGER NOT NULL CHECK (base_head_revision > 0),
base_commit_oid TEXT NOT NULL,
base_tree_oid TEXT NOT NULL,
expected_paths_json TEXT NOT NULL,
execution_profile_digest TEXT NOT NULL,
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
reserved_at INTEGER NOT NULL,
FOREIGN KEY (workspace_id, workspace_epoch_id)
REFERENCES runtime_workspace_epochs(workspace_id, workspace_epoch_id)
);
`,
],
[
15,
`
CREATE TABLE runtime_continuation_claims_v15 (
claim_id TEXT PRIMARY KEY,
source_session_id TEXT NOT NULL,
source_invocation_id TEXT NOT NULL,
source_run_id TEXT NOT NULL,
source_turn_id TEXT NOT NULL,
source_event_high_water INTEGER NOT NULL CHECK (source_event_high_water > 0),
source_prefix_digest TEXT NOT NULL,
boundary_digest TEXT NOT NULL UNIQUE,
boundary_json TEXT NOT NULL,
provider_projection_version INTEGER NOT NULL CHECK (provider_projection_version IN (1, 2)),
provider_replay_digest TEXT NOT NULL,
target_session_id TEXT NOT NULL,
target_invocation_id TEXT NOT NULL UNIQUE,
target_run_id TEXT NOT NULL UNIQUE,
target_turn_id TEXT NOT NULL,
target_run_header_json TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
start_event_id TEXT UNIQUE REFERENCES runtime_events(event_id),
start_kind TEXT CHECK (
start_kind IS NULL OR start_kind IN ('runtime_admission', 'claim_repair')
),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
UNIQUE (
source_session_id,
source_run_id,
source_event_high_water,
source_prefix_digest
),
UNIQUE (target_session_id, target_turn_id)
);
INSERT INTO runtime_continuation_claims_v15
SELECT * FROM runtime_continuation_claims;
DROP TABLE runtime_continuation_claims;
ALTER TABLE runtime_continuation_claims_v15 RENAME TO runtime_continuation_claims;
`,
],
[
16,
`
CREATE INDEX runtime_events_by_session_kind
ON runtime_events(session_id, event_kind, invocation_id);
CREATE UNIQUE INDEX runtime_events_one_opening_per_invocation
ON runtime_events(invocation_id)
WHERE event_kind = 'invocation_opened';
CREATE TABLE runtime_legacy_invocation_openings (
invocation_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
run_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
opened_at INTEGER NOT NULL,
opening_json TEXT NOT NULL,
-- UNIQUE is what indexes this side of the foreign key. SQLite indexes only
-- the parent, so without it every deleted RuntimeEvent scans this whole
-- table looking for rows to cascade.
anchor_event_id TEXT NOT NULL UNIQUE
REFERENCES runtime_events(event_id) ON DELETE CASCADE
) WITHOUT ROWID;
CREATE INDEX runtime_legacy_invocation_openings_by_session
ON runtime_legacy_invocation_openings(session_id, opened_at, invocation_id);
-- Rebuilt rather than renamed in place, because the column rename is not
-- the only thing this claim needs. Its start event belongs to the target
-- Session, and the foreign key had no ON DELETE clause, so purging that
-- Session was refused outright by the constraint and the whole purge rolled
-- back. A continuation whose target has been deleted no longer names
-- anything, so the claim goes with it and the source boundary it held is
-- free again.
CREATE TABLE runtime_continuation_claims_v16 (
claim_id TEXT PRIMARY KEY,
source_session_id TEXT NOT NULL,
source_invocation_id TEXT NOT NULL,
source_run_id TEXT NOT NULL,
source_turn_id TEXT NOT NULL,
source_event_high_water INTEGER NOT NULL CHECK (source_event_high_water > 0),
source_prefix_digest TEXT NOT NULL,
boundary_digest TEXT NOT NULL UNIQUE,
boundary_json TEXT NOT NULL,
provider_projection_version INTEGER NOT NULL CHECK (provider_projection_version IN (1, 2)),
provider_replay_digest TEXT NOT NULL,
target_session_id TEXT NOT NULL,
target_invocation_id TEXT NOT NULL UNIQUE,
target_run_id TEXT NOT NULL UNIQUE,
target_turn_id TEXT NOT NULL,
target_opening_json TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
start_event_id TEXT UNIQUE REFERENCES runtime_events(event_id) ON DELETE CASCADE,
start_kind TEXT CHECK (
start_kind IS NULL OR start_kind IN ('runtime_admission', 'claim_repair')
),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
UNIQUE (
source_session_id,
source_run_id,
source_event_high_water,
source_prefix_digest
),
UNIQUE (target_session_id, target_turn_id)
);
INSERT INTO runtime_continuation_claims_v16 (
claim_id, source_session_id, source_invocation_id, source_run_id, source_turn_id,
source_event_high_water, source_prefix_digest, boundary_digest, boundary_json,
provider_projection_version, provider_replay_digest, target_session_id,
target_invocation_id, target_run_id, target_turn_id, target_opening_json,
claimed_at, start_event_id, start_kind, protocol_version
)
SELECT
claim_id, source_session_id, source_invocation_id, source_run_id, source_turn_id,
source_event_high_water, source_prefix_digest, boundary_digest, boundary_json,
provider_projection_version, provider_replay_digest, target_session_id,
target_invocation_id, target_run_id, target_turn_id, target_run_header_json,
claimed_at, start_event_id, start_kind, protocol_version
FROM runtime_continuation_claims;
DROP TABLE runtime_continuation_claims;
ALTER TABLE runtime_continuation_claims_v16 RENAME TO runtime_continuation_claims;
`,
],
[
17,
`
-- Handoff replaces a physical attempt, not the admitted logical Turn.
-- Keep physical identities and each source boundary exclusive.
CREATE TABLE runtime_continuation_claims_v17 (
claim_id TEXT PRIMARY KEY,
source_session_id TEXT NOT NULL,
source_invocation_id TEXT NOT NULL,
source_run_id TEXT NOT NULL,
source_turn_id TEXT NOT NULL,
source_event_high_water INTEGER NOT NULL CHECK (source_event_high_water > 0),
source_prefix_digest TEXT NOT NULL,
boundary_digest TEXT NOT NULL UNIQUE,
boundary_json TEXT NOT NULL,
provider_projection_version INTEGER NOT NULL CHECK (provider_projection_version IN (1, 2)),
provider_replay_digest TEXT NOT NULL,
target_session_id TEXT NOT NULL,
target_invocation_id TEXT NOT NULL UNIQUE,
target_run_id TEXT NOT NULL UNIQUE,
target_turn_id TEXT NOT NULL,
target_opening_json TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
start_event_id TEXT UNIQUE REFERENCES runtime_events(event_id) ON DELETE CASCADE,
start_kind TEXT CHECK (start_kind IS NULL OR start_kind IN ('runtime_admission', 'claim_repair')),
protocol_version INTEGER NOT NULL CHECK (protocol_version = 1),
UNIQUE (source_session_id, source_run_id, source_event_high_water, source_prefix_digest)
);
INSERT INTO runtime_continuation_claims_v17 SELECT * FROM runtime_continuation_claims;
DROP TABLE runtime_continuation_claims;
ALTER TABLE runtime_continuation_claims_v17 RENAME TO runtime_continuation_claims;
CREATE UNIQUE INDEX runtime_continuation_fresh_turn
ON runtime_continuation_claims(target_session_id, target_turn_id)
WHERE json_extract(target_opening_json, '$.source.kind') <> 'handoff';
`,
],
[
18,
`
CREATE INDEX IF NOT EXISTS runtime_events_terminal ON runtime_events(invocation_id, event_seq) WHERE ${TERMINAL_RUNTIME_EVENT_SQL};
`,
],
[
19,
`
CREATE TABLE IF NOT EXISTS runtime_session_turn_extents (
session_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
first_ordinal INTEGER NOT NULL CHECK (first_ordinal > 0),
last_ordinal INTEGER NOT NULL CHECK (last_ordinal >= first_ordinal),
PRIMARY KEY (session_id, turn_id)
) WITHOUT ROWID;
CREATE INDEX IF NOT EXISTS runtime_session_turn_extents_by_first
ON runtime_session_turn_extents(session_id, first_ordinal, last_ordinal);
`,
],
]);
/**
* Data migrations that a SQL statement cannot express, applied inside the same
* transaction as their schema step. They project persisted records through the
* one TypeScript mapping that owns that projection, so a migration and the live
* writer can never classify a field two different ways.
*/
const DATA_MIGRATIONS: ReadonlyMap<number, (db: DatabaseSync) => void> = new Map([
[
16,
(db) => {
backfillInvocationOpeningFacts(db);
projectContinuationClaimOpenings(db);
},
],
[19, (db) => rebuildTranscriptTurnExtents(db)],
]);
/**
* Replace each open claim's embedded target Run header with the opening fact it
* always implied.
*
* The header was only ever there so the start event could be checked against
* it, and the check went through the projection anyway. Projecting once, here,
* leaves one representation instead of a copy plus a derivation.
*
* A row this cannot project is dropped rather than left half-migrated: an
* undecodable claim could not have admitted a start event before this migration
* either, and keeping it would only block the boundary it holds.
*/
function projectContinuationClaimOpenings(db: DatabaseSync): void {
const rows = db
.prepare('SELECT claim_id, target_opening_json FROM runtime_continuation_claims')
.all() as Array<{ claim_id: string; target_opening_json: string }>;
const update = db.prepare(
'UPDATE runtime_continuation_claims SET target_opening_json = ? WHERE claim_id = ?',
);
const remove = db.prepare('DELETE FROM runtime_continuation_claims WHERE claim_id = ?');
for (const row of rows) {
try {
const header = decodePersistedLegacyRunHeader(JSON.parse(row.target_opening_json));
update.run(JSON.stringify(invocationOpeningFromLegacyRunHeader(header)), row.claim_id);
} catch {
remove.run(row.claim_id);
}
}
}
/**
* Give every Run header its opening fact, so that after this migration the
* opening lives in the runtime database rather than on the header.
*
* A run that never wrote a RuntimeEvent gets the real thing: the opening fact
* as event one of its own invocation. A run that already has events cannot,
* because it owns an immutable sequence whose position 1, digests and coverage
* other facts already point at; inserting into it would rewrite signed history.
* Its opening is recorded in `runtime_legacy_invocation_openings` instead,
* which only this migration ever writes. Readers merge the two, so nothing
* downstream has to know which shelf a given opening came off.
*
* A shelved opening describes a ledger that already exists, so it is anchored to
* that ledger's first event and dies with it. Without the anchor, deleting a
* Session's events would leave the opening behind, and the inventory would
* report the run again with no ending — a completed run coming back as an
* active one.
*
* A header this cannot project fails closed: it is skipped, and its transcript
* and tool evidence stay exactly as readable as before.
*/
function backfillInvocationOpeningFacts(db: DatabaseSync): void {
if (!hasTable(db, 'core_agent_runs')) return;
// The header column is dropped by the core-execution migration that follows
// this one, so its absence means every header it held is already an opening
// fact. Nothing left to project, and the two scopes stay independently
// replayable.
if (!hasColumn(db, 'core_agent_runs', 'record_json')) return;
const rows = db
.prepare(`
SELECT
r.session_id,
r.run_id,
r.record_json,
first_event.invocation_id AS existing_invocation_id,
first_event.event_id AS anchor_event_id
FROM core_agent_runs r
LEFT JOIN runtime_events first_event ON first_event.event_id = (
SELECT e.event_id FROM runtime_events e
WHERE e.session_id = r.session_id AND e.run_id = r.run_id
ORDER BY e.event_seq ASC LIMIT 1
)
ORDER BY r.created_at ASC, r.run_id ASC
`)
.all() as Array<{
session_id: string;
run_id: string;
record_json: string;
existing_invocation_id: string | null;
anchor_event_id: string | null;
}>;
const insertEvent = db.prepare(`
INSERT INTO runtime_events (
event_id, session_id, invocation_id, run_id, turn_id, event_seq,
event_kind, payload_json, committed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertOrdinal = db.prepare(`
INSERT INTO runtime_session_event_ordinals(session_id, ordinal, event_id)
SELECT ?, COALESCE(MAX(ordinal), 0) + 1, ?
FROM runtime_session_event_ordinals WHERE session_id = ?
`);
const insertLegacyOpening = db.prepare(`
INSERT OR IGNORE INTO runtime_legacy_invocation_openings (
invocation_id, session_id, run_id, turn_id, opened_at, opening_json,
anchor_event_id
) VALUES (?, ?, ?, ?, ?, ?, ?)
`);
// The header column is dropped right after this, so a header this cannot read
// is a run that would silently cease to exist. Refusing the whole migration
// keeps the database as it was, and the failure names the row instead of
// hiding it.
const unreadable = (row: { session_id: string; run_id: string }, cause: unknown): Error =>
new Error(
`Cannot migrate the AgentRun header of ${row.session_id}/${row.run_id}: ${
cause instanceof Error ? cause.message : String(cause)
}`,
{ cause },
);
for (const row of rows) {
let header: LegacyRunHeader;
let opening: string;
try {
header = decodePersistedLegacyRunHeader(JSON.parse(row.record_json));
opening = JSON.stringify(invocationOpeningFromLegacyRunHeader(header));
} catch (error) {
throw unreadable(row, error);
}
if (row.existing_invocation_id !== null && row.anchor_event_id !== null) {
// The invocation id its own events already carry is the one every reader
// joins on, so the legacy row is keyed by that rather than by the header's
// copy, which older builds minted independently.
insertLegacyOpening.run(
row.existing_invocation_id,
header.sessionId,
header.runId,
header.turnId,
header.createdAt,
opening,
row.anchor_event_id,
);
continue;
}
// A run with no events of its own gets the facts its header held, where
// facts live now: the opening, and the ending if the header recorded one.
// A header still marked in flight stays open; recovery settles it the way it
// settles any run the process died holding.
const run = {
sessionId: header.sessionId,
invocationId: header.invocationId ?? header.runId,
runId: header.runId,
turnId: header.turnId,
};
const events = [
buildInvocationOpenedEvent({
id: `invocation_opened:${header.runId}`,
run,
openedAt: header.createdAt,
opening: invocationOpeningFromLegacyRunHeader(header),
}),
...(header.status === 'completed' ||
header.status === 'failed' ||
header.status === 'cancelled'
? [
buildSyntheticTerminalRuntimeEvent({
id: `invocation_terminal:${header.runId}`,
invocationId: run.invocationId,
run,
status: header.status,
ts: header.completedAt ?? header.updatedAt,
...(header.failureClass !== undefined ? { failureClass: header.failureClass } : {}),
...(header.failureMessage !== undefined ? { message: header.failureMessage } : {}),
...(header.abortSource !== undefined ? { abortSource: header.abortSource } : {}),
}),
]
: []),
];
events.forEach((event, index) => {
let encoded: { event: RuntimeEvent; json: string };
try {
encoded = encodeCanonicalRuntimeEvent(event);
} catch (error) {
throw unreadable(row, error);
}
insertEvent.run(
event.id,
event.sessionId,
event.invocationId,
event.runId,
event.turnId,
index + 1,
runtimeEventKind(event),
encoded.json,
event.ts,
);
insertOrdinal.run(event.sessionId, event.id, event.sessionId);
});
}
}
/** The `event_kind` column: the one coarse label every reader indexes events by. */
export function runtimeEventKind(event: RuntimeEvent): string {
return (
event.content?.kind ??
event.status ??
(event.actions?.workspaceFact ? 'workspace_fact' : undefined) ??
(event.actions?.toolDispatch ? 'tool_dispatch' : undefined) ??
(event.actions?.endInvocation ? 'invocation_end' : 'runtime_fact')
);
}
function hasColumn(db: DatabaseSync, table: string, column: string): boolean {
const columns = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name?: unknown }>;
return columns.some((candidate) => candidate.name === column);
}
function hasTable(db: DatabaseSync, name: string): boolean {
const row = db
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?")
.get(name) as { present?: unknown } | undefined;
return row?.present === 1;
}
export function configureSqliteRuntimeDatabase(db: DatabaseSync): void {
// Bound lock acquisition before touching persistent journal state. WAL mode is
// database-persistent, so established workspaces only need to verify it rather
// than making every concurrent opener execute the setting form of the pragma.
configureSqliteRuntimeLockWait(db);
ensureWalJournalMode(db);
db.exec('PRAGMA synchronous = FULL');
db.exec('PRAGMA foreign_keys = ON');
}
/** Configure connection-local lock waiting without changing persistent database state. */
export function configureSqliteRuntimeLockWait(db: DatabaseSync): void {
db.exec(`PRAGMA busy_timeout = ${SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS}`);
}
export function migrateSqliteRuntimeDatabase(
db: DatabaseSync,
options: { transaction?: 'self' | 'caller' } = {},
): void {
const observedVersion = readUserVersion(db);
if (observedVersion > SQLITE_RUNTIME_SCHEMA_VERSION) {
throw new Error(
`SQLite runtime schema ${observedVersion} is newer than supported version ${SQLITE_RUNTIME_SCHEMA_VERSION}`,
);
}
if (observedVersion === SQLITE_RUNTIME_SCHEMA_VERSION) return;
// The optimistic read keeps established databases on a read-only open path.
// Any pending upgrade is serialized by one write transaction, then re-reads
// user_version under that lock so a concurrent opener cannot apply a
// migration another process just committed.
const ownsTransaction = options.transaction !== 'caller';
if (ownsTransaction) db.exec('BEGIN IMMEDIATE');
try {
const current = readUserVersion(db);
if (current > SQLITE_RUNTIME_SCHEMA_VERSION) {
throw new Error(
`SQLite runtime schema ${current} is newer than supported version ${SQLITE_RUNTIME_SCHEMA_VERSION}`,
);
}
for (let version = current + 1; version <= SQLITE_RUNTIME_SCHEMA_VERSION; version += 1) {
const sql = MIGRATIONS.get(version);
if (!sql) throw new Error(`Missing SQLite runtime migration ${version}`);
db.exec(sql);
DATA_MIGRATIONS.get(version)?.(db);
db.exec(`PRAGMA user_version = ${version}`);
}
if (ownsTransaction) db.exec('COMMIT');
} catch (error) {
if (ownsTransaction) rollback(db);
throw error;
}
}
export function readUserVersion(db: DatabaseSync): number {
const row = db.prepare('PRAGMA user_version').get() as { user_version?: unknown } | undefined;
const value = row?.user_version;
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
throw new Error('Invalid SQLite runtime schema version');
}
return value;
}
function readJournalMode(db: DatabaseSync): string {
const row = db.prepare('PRAGMA journal_mode').get() as { journal_mode?: unknown } | undefined;
if (typeof row?.journal_mode !== 'string') {
throw new Error('Invalid SQLite runtime journal mode');
}
return row.journal_mode.toLowerCase();
}
function ensureWalJournalMode(db: DatabaseSync): void {
const deadline = Date.now() + SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS;
while (true) {
try {
const journalMode = readJournalMode(db);
if (journalMode === 'wal' || journalMode === 'memory') return;
db.exec('PRAGMA journal_mode = WAL');
const configuredMode = readJournalMode(db);
if (configuredMode !== 'wal') {
throw new Error(`SQLite runtime requires WAL journal mode, received ${configuredMode}`);
}
return;
} catch (error) {
if (!isSqliteBusy(error) || Date.now() >= deadline) throw error;
Atomics.wait(
initializationRetryGate,
0,
0,
Math.min(SQLITE_INITIALIZATION_RETRY_DELAY_MS, Math.max(1, deadline - Date.now())),
);
}
}
}
function isSqliteBusy(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const sqliteError = error as Error & {
code?: unknown;
errcode?: unknown;
errstr?: unknown;
};
return (
sqliteError.errcode === 5 ||
sqliteError.code === 'SQLITE_BUSY' ||
sqliteError.errstr === 'database is locked' ||
/database (?:is )?(?:locked|busy)/i.test(sqliteError.message)
);
}
function rollback(db: DatabaseSync): void {
try {
db.exec('ROLLBACK');
} catch {
// Preserve the migration failure that triggered rollback.
}
}