db: fold legacy runtime bootstrap into v7

This commit is contained in:
ejclaw
2026-04-11 07:39:08 +09:00
parent eda59270e4
commit 7ef0738726
2 changed files with 133 additions and 166 deletions

View File

@@ -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;

View File

@@ -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`,