db: fold legacy runtime bootstrap into v7
This commit is contained in:
@@ -6,9 +6,14 @@ import {
|
|||||||
fillCanonicalChannelOwnerLeaseMetadata,
|
fillCanonicalChannelOwnerLeaseMetadata,
|
||||||
fillCanonicalPairedTaskMetadata,
|
fillCanonicalPairedTaskMetadata,
|
||||||
fillCanonicalServiceHandoffMetadata,
|
fillCanonicalServiceHandoffMetadata,
|
||||||
|
inferExecutionLeaseServiceIdFromCanonicalMetadata,
|
||||||
} from '../canonical-role-metadata.js';
|
} from '../canonical-role-metadata.js';
|
||||||
import { normalizeStoredAgentType } from '../room-registration.js';
|
import { normalizeStoredAgentType } from '../room-registration.js';
|
||||||
import { tableHasColumn, tryExecMigration } from './helpers.js';
|
import {
|
||||||
|
getTableColumns,
|
||||||
|
tableHasColumn,
|
||||||
|
tryExecMigration,
|
||||||
|
} from './helpers.js';
|
||||||
import type { SchemaMigrationDefinition } from './types.js';
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
|
||||||
interface StoredPairedTaskServiceRow {
|
interface StoredPairedTaskServiceRow {
|
||||||
@@ -52,6 +57,17 @@ interface StoredWorkItemServiceRow {
|
|||||||
delivery_role?: string | null;
|
delivery_role?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LegacyExecutionLeaseServiceRow {
|
||||||
|
rowid: number;
|
||||||
|
role: string;
|
||||||
|
owner_service_id?: string | null;
|
||||||
|
reviewer_service_id?: string | null;
|
||||||
|
arbiter_service_id?: string | null;
|
||||||
|
owner_agent_type?: string | null;
|
||||||
|
reviewer_agent_type?: string | null;
|
||||||
|
arbiter_agent_type?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePairedRole(
|
function normalizePairedRole(
|
||||||
role: string | null | undefined,
|
role: string | null | undefined,
|
||||||
): 'owner' | 'reviewer' | 'arbiter' | null {
|
): 'owner' | 'reviewer' | 'arbiter' | null {
|
||||||
@@ -354,10 +370,110 @@ function backfillCanonicalWorkItemServiceIds(database: Database): void {
|
|||||||
tx(rows);
|
tx(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
||||||
|
if (
|
||||||
|
!tableHasColumn(
|
||||||
|
database,
|
||||||
|
'paired_task_execution_leases',
|
||||||
|
'claimed_service_id',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairedTasksTable = database
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT 1
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
AND name = 'paired_tasks'
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
if (!pairedTasksTable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairedTaskColumns = getTableColumns(database, 'paired_tasks');
|
||||||
|
const selectPairedTaskColumn = (columnName: string): string =>
|
||||||
|
pairedTaskColumns.includes(columnName)
|
||||||
|
? `paired_tasks.${columnName} AS ${columnName}`
|
||||||
|
: `NULL AS ${columnName}`;
|
||||||
|
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
paired_task_execution_leases.rowid AS rowid,
|
||||||
|
paired_task_execution_leases.role AS role,
|
||||||
|
${selectPairedTaskColumn('owner_service_id')},
|
||||||
|
${selectPairedTaskColumn('reviewer_service_id')},
|
||||||
|
${selectPairedTaskColumn('arbiter_service_id')},
|
||||||
|
${selectPairedTaskColumn('owner_agent_type')},
|
||||||
|
${selectPairedTaskColumn('reviewer_agent_type')},
|
||||||
|
${selectPairedTaskColumn('arbiter_agent_type')}
|
||||||
|
FROM paired_task_execution_leases
|
||||||
|
LEFT JOIN paired_tasks
|
||||||
|
ON paired_tasks.id = paired_task_execution_leases.task_id
|
||||||
|
WHERE paired_task_execution_leases.claimed_service_id IS NULL
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.all() as LegacyExecutionLeaseServiceRow[];
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = database.prepare(
|
||||||
|
`
|
||||||
|
UPDATE paired_task_execution_leases
|
||||||
|
SET claimed_service_id = ?
|
||||||
|
WHERE rowid = ?
|
||||||
|
AND claimed_service_id IS NULL
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tx = database.transaction(
|
||||||
|
(leaseRows: LegacyExecutionLeaseServiceRow[]) => {
|
||||||
|
for (const row of leaseRows) {
|
||||||
|
const claimedServiceId =
|
||||||
|
inferExecutionLeaseServiceIdFromCanonicalMetadata(row);
|
||||||
|
if (!claimedServiceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.run(claimedServiceId, row.rowid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
export const RUNTIME_SERVICE_METADATA_MIGRATION = {
|
export const RUNTIME_SERVICE_METADATA_MIGRATION = {
|
||||||
version: 7,
|
version: 7,
|
||||||
name: 'runtime_service_metadata',
|
name: 'runtime_service_metadata',
|
||||||
apply(database) {
|
apply(database) {
|
||||||
|
for (const statement of [
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN intended_role TEXT`,
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN source_role TEXT`,
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN target_role TEXT`,
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN source_agent_type TEXT`,
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN source_service_id TEXT`,
|
||||||
|
`ALTER TABLE service_handoffs ADD COLUMN target_service_id TEXT`,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN owner_service_id TEXT`,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN reviewer_service_id TEXT`,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN owner_agent_type TEXT`,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN reviewer_agent_type TEXT`,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN arbiter_agent_type TEXT`,
|
||||||
|
`ALTER TABLE paired_task_execution_leases ADD COLUMN expires_at TEXT`,
|
||||||
|
`ALTER TABLE paired_task_execution_leases ADD COLUMN claimed_service_id TEXT`,
|
||||||
|
`ALTER TABLE work_items ADD COLUMN delivery_role TEXT`,
|
||||||
|
`ALTER TABLE work_items ADD COLUMN service_id TEXT`,
|
||||||
|
]) {
|
||||||
|
tryExecMigration(database, statement);
|
||||||
|
}
|
||||||
|
|
||||||
for (const column of [
|
for (const column of [
|
||||||
'owner_service_id',
|
'owner_service_id',
|
||||||
'reviewer_service_id',
|
'reviewer_service_id',
|
||||||
@@ -372,9 +488,22 @@ export const RUNTIME_SERVICE_METADATA_MIGRATION = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.exec(`
|
||||||
|
UPDATE paired_task_execution_leases
|
||||||
|
SET expires_at = COALESCE(
|
||||||
|
expires_at,
|
||||||
|
strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '+10 minutes')
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at
|
||||||
|
ON paired_task_execution_leases(expires_at)
|
||||||
|
`);
|
||||||
|
|
||||||
backfillCanonicalPairedTaskServiceIds(database);
|
backfillCanonicalPairedTaskServiceIds(database);
|
||||||
backfillCanonicalChannelOwnerServiceIds(database);
|
backfillCanonicalChannelOwnerServiceIds(database);
|
||||||
backfillCanonicalServiceHandoffServiceIds(database);
|
backfillCanonicalServiceHandoffServiceIds(database);
|
||||||
backfillCanonicalWorkItemServiceIds(database);
|
backfillCanonicalWorkItemServiceIds(database);
|
||||||
|
backfillLegacyExecutionLeaseServiceShadows(database);
|
||||||
},
|
},
|
||||||
} satisfies SchemaMigrationDefinition;
|
} satisfies SchemaMigrationDefinition;
|
||||||
|
|||||||
168
src/db/schema.ts
168
src/db/schema.ts
@@ -1,12 +1,7 @@
|
|||||||
import { Database } from 'bun:sqlite';
|
import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
import { inferExecutionLeaseServiceIdFromCanonicalMetadata } from './canonical-role-metadata.js';
|
|
||||||
import { buildPairedTurnAttemptId } from './paired-turn-attempts.js';
|
import { buildPairedTurnAttemptId } from './paired-turn-attempts.js';
|
||||||
import {
|
import { tableHasColumn, tryExecMigration } from './migrations/helpers.js';
|
||||||
getTableColumns,
|
|
||||||
tableHasColumn,
|
|
||||||
tryExecMigration,
|
|
||||||
} from './migrations/helpers.js';
|
|
||||||
|
|
||||||
// Legacy monolithic migration bundle kept for compatibility with databases
|
// Legacy monolithic migration bundle kept for compatibility with databases
|
||||||
// that predate ordered schema migration tracking. New schema changes belong in
|
// that predate ordered schema migration tracking. New schema changes belong in
|
||||||
@@ -145,17 +140,6 @@ function tableHasForeignKey(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyExecutionLeaseServiceRow {
|
|
||||||
rowid: number;
|
|
||||||
role: string;
|
|
||||||
owner_service_id?: string | null;
|
|
||||||
reviewer_service_id?: string | null;
|
|
||||||
arbiter_service_id?: string | null;
|
|
||||||
owner_agent_type?: string | null;
|
|
||||||
reviewer_agent_type?: string | null;
|
|
||||||
arbiter_agent_type?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PairedTurnAttemptTimingRow {
|
interface PairedTurnAttemptTimingRow {
|
||||||
turn_id: string;
|
turn_id: string;
|
||||||
attempt_no: number;
|
attempt_no: number;
|
||||||
@@ -2076,160 +2060,14 @@ export function applyPairedTurnAttemptProvenanceConstraints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
|
||||||
if (
|
|
||||||
!tableHasColumn(
|
|
||||||
database,
|
|
||||||
'paired_task_execution_leases',
|
|
||||||
'claimed_service_id',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairedTasksTable = database
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT 1
|
|
||||||
FROM sqlite_master
|
|
||||||
WHERE type = 'table'
|
|
||||||
AND name = 'paired_tasks'
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.get();
|
|
||||||
if (!pairedTasksTable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairedTaskColumns = getTableColumns(database, 'paired_tasks');
|
|
||||||
const selectPairedTaskColumn = (columnName: string): string =>
|
|
||||||
pairedTaskColumns.includes(columnName)
|
|
||||||
? `paired_tasks.${columnName} AS ${columnName}`
|
|
||||||
: `NULL AS ${columnName}`;
|
|
||||||
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
paired_task_execution_leases.rowid AS rowid,
|
|
||||||
paired_task_execution_leases.role AS role,
|
|
||||||
${selectPairedTaskColumn('owner_service_id')},
|
|
||||||
${selectPairedTaskColumn('reviewer_service_id')},
|
|
||||||
${selectPairedTaskColumn('arbiter_service_id')},
|
|
||||||
${selectPairedTaskColumn('owner_agent_type')},
|
|
||||||
${selectPairedTaskColumn('reviewer_agent_type')},
|
|
||||||
${selectPairedTaskColumn('arbiter_agent_type')}
|
|
||||||
FROM paired_task_execution_leases
|
|
||||||
LEFT JOIN paired_tasks
|
|
||||||
ON paired_tasks.id = paired_task_execution_leases.task_id
|
|
||||||
WHERE paired_task_execution_leases.claimed_service_id IS NULL
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.all() as LegacyExecutionLeaseServiceRow[];
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = database.prepare(
|
|
||||||
`
|
|
||||||
UPDATE paired_task_execution_leases
|
|
||||||
SET claimed_service_id = ?
|
|
||||||
WHERE rowid = ?
|
|
||||||
AND claimed_service_id IS NULL
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
const tx = database.transaction(
|
|
||||||
(leaseRows: LegacyExecutionLeaseServiceRow[]) => {
|
|
||||||
for (const row of leaseRows) {
|
|
||||||
const claimedServiceId =
|
|
||||||
inferExecutionLeaseServiceIdFromCanonicalMetadata(row);
|
|
||||||
if (!claimedServiceId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
update.run(claimedServiceId, row.rowid);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tx(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyLegacySchemaMigrations(
|
export function applyLegacySchemaMigrations(
|
||||||
database: Database,
|
database: Database,
|
||||||
_args: {
|
_args: {
|
||||||
assistantName: string;
|
assistantName: string;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
tryExecMigration(
|
// Some legacy columns must exist before later numbered migrations can run.
|
||||||
database,
|
// `work_items.service_id` / `delivery_role` are referenced by v4 indexes.
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN intended_role TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN source_role TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN target_role TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN source_agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN source_service_id TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN target_service_id TEXT`,
|
|
||||||
);
|
|
||||||
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN owner_agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN reviewer_agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN arbiter_agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN owner_service_id TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN reviewer_service_id TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_task_execution_leases ADD COLUMN expires_at TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_task_execution_leases ADD COLUMN claimed_service_id TEXT`,
|
|
||||||
);
|
|
||||||
database.exec(`
|
|
||||||
UPDATE paired_task_execution_leases
|
|
||||||
SET expires_at = COALESCE(
|
|
||||||
expires_at,
|
|
||||||
strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '+10 minutes')
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
// Legacy lease rows predate claimed_service_id. Infer the original service
|
|
||||||
// from paired-task role metadata when possible instead of blanket-marking
|
|
||||||
// everything as the current service, which would let startup cleanup delete
|
|
||||||
// leases that belong to another runtime.
|
|
||||||
backfillLegacyExecutionLeaseServiceShadows(database);
|
|
||||||
database.exec(`
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at
|
|
||||||
ON paired_task_execution_leases(expires_at)
|
|
||||||
`);
|
|
||||||
tryExecMigration(
|
tryExecMigration(
|
||||||
database,
|
database,
|
||||||
`ALTER TABLE work_items ADD COLUMN delivery_role TEXT`,
|
`ALTER TABLE work_items ADD COLUMN delivery_role TEXT`,
|
||||||
|
|||||||
Reference in New Issue
Block a user