db: extract paired schema cleanup migrations
This commit is contained in:
@@ -31,6 +31,9 @@ function getExpectedSchemaMigrations(): Array<{
|
|||||||
{ version: 4, name: 'message_seq_and_work_item_indexes' },
|
{ version: 4, name: 'message_seq_and_work_item_indexes' },
|
||||||
{ version: 5, name: 'sessions_composite_key' },
|
{ version: 5, name: 'sessions_composite_key' },
|
||||||
{ version: 6, name: 'chat_channel_metadata' },
|
{ version: 6, name: 'chat_channel_metadata' },
|
||||||
|
{ version: 7, name: 'runtime_service_metadata' },
|
||||||
|
{ version: 8, name: 'paired_task_schema_cleanup' },
|
||||||
|
{ version: 9, name: 'paired_workspace_project_schema_cleanup' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
380
src/db/migrations/007_runtime-service-metadata.ts
Normal file
380
src/db/migrations/007_runtime-service-metadata.ts
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import type { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
|
import { SERVICE_SESSION_SCOPE, normalizeServiceId } from '../../config.js';
|
||||||
|
import { resolveRoleServiceShadow } from '../../role-service-shadow.js';
|
||||||
|
import {
|
||||||
|
fillCanonicalChannelOwnerLeaseMetadata,
|
||||||
|
fillCanonicalPairedTaskMetadata,
|
||||||
|
fillCanonicalServiceHandoffMetadata,
|
||||||
|
} from '../canonical-role-metadata.js';
|
||||||
|
import { normalizeStoredAgentType } from '../room-registration.js';
|
||||||
|
import { tableHasColumn, tryExecMigration } from './helpers.js';
|
||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
|
||||||
|
interface StoredPairedTaskServiceRow {
|
||||||
|
rowid: number;
|
||||||
|
id: string;
|
||||||
|
owner_service_id?: string | null;
|
||||||
|
reviewer_service_id?: string | null;
|
||||||
|
owner_agent_type?: string | null;
|
||||||
|
reviewer_agent_type?: string | null;
|
||||||
|
arbiter_agent_type?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoredChannelOwnerLeaseServiceRow {
|
||||||
|
rowid: number;
|
||||||
|
chat_jid: 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 StoredServiceHandoffServiceRow {
|
||||||
|
id: number;
|
||||||
|
chat_jid: string;
|
||||||
|
group_folder: string;
|
||||||
|
source_service_id?: string | null;
|
||||||
|
target_service_id?: string | null;
|
||||||
|
source_role?: string | null;
|
||||||
|
target_role?: string | null;
|
||||||
|
source_agent_type?: string | null;
|
||||||
|
target_agent_type?: string | null;
|
||||||
|
intended_role?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoredWorkItemServiceRow {
|
||||||
|
id: number;
|
||||||
|
agent_type?: string | null;
|
||||||
|
service_id?: string | null;
|
||||||
|
delivery_role?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePairedRole(
|
||||||
|
role: string | null | undefined,
|
||||||
|
): 'owner' | 'reviewer' | 'arbiter' | null {
|
||||||
|
return role === 'owner' || role === 'reviewer' || role === 'arbiter'
|
||||||
|
? role
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillCanonicalPairedTaskServiceIds(database: Database): void {
|
||||||
|
if (
|
||||||
|
!tableHasColumn(database, 'paired_tasks', 'owner_service_id') ||
|
||||||
|
!tableHasColumn(database, 'paired_tasks', 'reviewer_service_id')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
rowid,
|
||||||
|
id,
|
||||||
|
owner_service_id,
|
||||||
|
reviewer_service_id,
|
||||||
|
owner_agent_type,
|
||||||
|
reviewer_agent_type,
|
||||||
|
arbiter_agent_type
|
||||||
|
FROM paired_tasks
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.all() as StoredPairedTaskServiceRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = database.prepare(
|
||||||
|
`
|
||||||
|
UPDATE paired_tasks
|
||||||
|
SET owner_service_id = ?,
|
||||||
|
reviewer_service_id = ?,
|
||||||
|
owner_agent_type = ?,
|
||||||
|
reviewer_agent_type = ?,
|
||||||
|
arbiter_agent_type = ?
|
||||||
|
WHERE rowid = ?
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tx = database.transaction((taskRows: StoredPairedTaskServiceRow[]) => {
|
||||||
|
for (const row of taskRows) {
|
||||||
|
const {
|
||||||
|
ownerAgentType,
|
||||||
|
reviewerAgentType,
|
||||||
|
arbiterAgentType,
|
||||||
|
ownerServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
} = fillCanonicalPairedTaskMetadata({
|
||||||
|
id: row.id,
|
||||||
|
owner_service_id: row.owner_service_id,
|
||||||
|
reviewer_service_id: row.reviewer_service_id,
|
||||||
|
owner_agent_type: row.owner_agent_type,
|
||||||
|
reviewer_agent_type: row.reviewer_agent_type,
|
||||||
|
arbiter_agent_type: row.arbiter_agent_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
row.owner_service_id === ownerServiceId &&
|
||||||
|
row.reviewer_service_id === reviewerServiceId &&
|
||||||
|
row.owner_agent_type === ownerAgentType &&
|
||||||
|
row.reviewer_agent_type === reviewerAgentType &&
|
||||||
|
row.arbiter_agent_type === arbiterAgentType
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.run(
|
||||||
|
ownerServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
ownerAgentType,
|
||||||
|
reviewerAgentType,
|
||||||
|
arbiterAgentType,
|
||||||
|
row.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillCanonicalChannelOwnerServiceIds(database: Database): void {
|
||||||
|
if (!tableHasColumn(database, 'channel_owner', 'owner_service_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
rowid,
|
||||||
|
chat_jid,
|
||||||
|
owner_service_id,
|
||||||
|
reviewer_service_id,
|
||||||
|
arbiter_service_id,
|
||||||
|
owner_agent_type,
|
||||||
|
reviewer_agent_type,
|
||||||
|
arbiter_agent_type
|
||||||
|
FROM channel_owner
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.all() as StoredChannelOwnerLeaseServiceRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = database.prepare(
|
||||||
|
`
|
||||||
|
UPDATE channel_owner
|
||||||
|
SET owner_service_id = ?,
|
||||||
|
reviewer_service_id = ?,
|
||||||
|
arbiter_service_id = ?,
|
||||||
|
owner_agent_type = ?,
|
||||||
|
reviewer_agent_type = ?,
|
||||||
|
arbiter_agent_type = ?
|
||||||
|
WHERE rowid = ?
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tx = database.transaction(
|
||||||
|
(leaseRows: StoredChannelOwnerLeaseServiceRow[]) => {
|
||||||
|
for (const row of leaseRows) {
|
||||||
|
const {
|
||||||
|
ownerAgentType,
|
||||||
|
reviewerAgentType,
|
||||||
|
arbiterAgentType,
|
||||||
|
ownerServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
arbiterServiceId,
|
||||||
|
} = fillCanonicalChannelOwnerLeaseMetadata({
|
||||||
|
chat_jid: row.chat_jid,
|
||||||
|
owner_service_id: row.owner_service_id,
|
||||||
|
reviewer_service_id: row.reviewer_service_id,
|
||||||
|
arbiter_service_id: row.arbiter_service_id,
|
||||||
|
owner_agent_type: row.owner_agent_type,
|
||||||
|
reviewer_agent_type: row.reviewer_agent_type,
|
||||||
|
arbiter_agent_type: row.arbiter_agent_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
row.owner_service_id === ownerServiceId &&
|
||||||
|
row.reviewer_service_id === reviewerServiceId &&
|
||||||
|
row.arbiter_service_id === arbiterServiceId &&
|
||||||
|
row.owner_agent_type === ownerAgentType &&
|
||||||
|
row.reviewer_agent_type === reviewerAgentType &&
|
||||||
|
row.arbiter_agent_type === arbiterAgentType
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.run(
|
||||||
|
ownerServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
arbiterServiceId,
|
||||||
|
ownerAgentType,
|
||||||
|
reviewerAgentType,
|
||||||
|
arbiterAgentType,
|
||||||
|
row.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillCanonicalServiceHandoffServiceIds(database: Database): void {
|
||||||
|
if (!tableHasColumn(database, 'service_handoffs', 'source_service_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
chat_jid,
|
||||||
|
group_folder,
|
||||||
|
source_service_id,
|
||||||
|
target_service_id,
|
||||||
|
source_role,
|
||||||
|
target_role,
|
||||||
|
source_agent_type,
|
||||||
|
target_agent_type,
|
||||||
|
intended_role
|
||||||
|
FROM service_handoffs
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.all() as StoredServiceHandoffServiceRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = database.prepare(
|
||||||
|
`
|
||||||
|
UPDATE service_handoffs
|
||||||
|
SET source_service_id = ?,
|
||||||
|
target_service_id = ?,
|
||||||
|
source_agent_type = ?,
|
||||||
|
target_agent_type = ?,
|
||||||
|
source_role = ?,
|
||||||
|
target_role = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const tx = database.transaction(
|
||||||
|
(handoffRows: StoredServiceHandoffServiceRow[]) => {
|
||||||
|
for (const row of handoffRows) {
|
||||||
|
const {
|
||||||
|
sourceRole,
|
||||||
|
targetRole,
|
||||||
|
sourceAgentType,
|
||||||
|
targetAgentType,
|
||||||
|
sourceServiceId,
|
||||||
|
targetServiceId,
|
||||||
|
} = fillCanonicalServiceHandoffMetadata({
|
||||||
|
id: row.id,
|
||||||
|
chat_jid: row.chat_jid,
|
||||||
|
source_service_id: row.source_service_id,
|
||||||
|
target_service_id: row.target_service_id,
|
||||||
|
source_role: row.source_role,
|
||||||
|
target_role: row.target_role,
|
||||||
|
intended_role: row.intended_role,
|
||||||
|
source_agent_type: row.source_agent_type,
|
||||||
|
target_agent_type: row.target_agent_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
row.source_service_id === sourceServiceId &&
|
||||||
|
row.target_service_id === targetServiceId &&
|
||||||
|
row.source_agent_type === (sourceAgentType ?? null) &&
|
||||||
|
row.target_agent_type === targetAgentType &&
|
||||||
|
row.source_role === sourceRole &&
|
||||||
|
row.target_role === targetRole
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.run(
|
||||||
|
sourceServiceId,
|
||||||
|
targetServiceId,
|
||||||
|
sourceAgentType,
|
||||||
|
targetAgentType,
|
||||||
|
sourceRole,
|
||||||
|
targetRole,
|
||||||
|
row.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillCanonicalWorkItemServiceIds(database: Database): void {
|
||||||
|
if (!tableHasColumn(database, 'work_items', 'service_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT id, agent_type, service_id, delivery_role
|
||||||
|
FROM work_items`,
|
||||||
|
)
|
||||||
|
.all() as StoredWorkItemServiceRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = database.prepare(
|
||||||
|
`UPDATE work_items
|
||||||
|
SET service_id = ?
|
||||||
|
WHERE id = ?`,
|
||||||
|
);
|
||||||
|
const tx = database.transaction(
|
||||||
|
(workItemRows: StoredWorkItemServiceRow[]) => {
|
||||||
|
for (const row of workItemRows) {
|
||||||
|
const agentType =
|
||||||
|
normalizeStoredAgentType(row.agent_type) ?? 'claude-code';
|
||||||
|
const deliveryRole = normalizePairedRole(row.delivery_role) ?? 'owner';
|
||||||
|
const serviceId =
|
||||||
|
(row.service_id ? normalizeServiceId(row.service_id) : null) ??
|
||||||
|
resolveRoleServiceShadow(deliveryRole, agentType) ??
|
||||||
|
SERVICE_SESSION_SCOPE;
|
||||||
|
|
||||||
|
if (row.service_id === serviceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.run(serviceId, row.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RUNTIME_SERVICE_METADATA_MIGRATION = {
|
||||||
|
version: 7,
|
||||||
|
name: 'runtime_service_metadata',
|
||||||
|
apply(database) {
|
||||||
|
for (const column of [
|
||||||
|
'owner_service_id',
|
||||||
|
'reviewer_service_id',
|
||||||
|
'arbiter_service_id',
|
||||||
|
'owner_agent_type',
|
||||||
|
'reviewer_agent_type',
|
||||||
|
'arbiter_agent_type',
|
||||||
|
]) {
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE channel_owner ADD COLUMN ${column} TEXT`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
backfillCanonicalPairedTaskServiceIds(database);
|
||||||
|
backfillCanonicalChannelOwnerServiceIds(database);
|
||||||
|
backfillCanonicalServiceHandoffServiceIds(database);
|
||||||
|
backfillCanonicalWorkItemServiceIds(database);
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
124
src/db/migrations/008_paired-task-schema-cleanup.ts
Normal file
124
src/db/migrations/008_paired-task-schema-cleanup.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
import { getTableColumns, tryExecMigration } from './helpers.js';
|
||||||
|
|
||||||
|
export const PAIRED_TASK_SCHEMA_CLEANUP_MIGRATION = {
|
||||||
|
version: 8,
|
||||||
|
name: 'paired_task_schema_cleanup',
|
||||||
|
apply(database) {
|
||||||
|
const pairedTasksSqlRow = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
||||||
|
)
|
||||||
|
.get() as { sql?: string } | undefined;
|
||||||
|
const pairedTasksSql = pairedTasksSqlRow?.sql || '';
|
||||||
|
const pairedTasksNeedsRebuild =
|
||||||
|
pairedTasksSql &&
|
||||||
|
(pairedTasksSql.includes('task_policy') ||
|
||||||
|
!pairedTasksSql.includes('round_trip_count'));
|
||||||
|
if (pairedTasksNeedsRebuild) {
|
||||||
|
database.exec(`DROP TABLE IF EXISTS paired_tasks`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS paired_tasks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
chat_jid TEXT NOT NULL,
|
||||||
|
group_folder TEXT NOT NULL,
|
||||||
|
owner_service_id TEXT NOT NULL,
|
||||||
|
reviewer_service_id TEXT NOT NULL,
|
||||||
|
owner_agent_type TEXT,
|
||||||
|
reviewer_agent_type TEXT,
|
||||||
|
arbiter_agent_type TEXT,
|
||||||
|
title TEXT,
|
||||||
|
source_ref TEXT,
|
||||||
|
plan_notes TEXT,
|
||||||
|
review_requested_at TEXT,
|
||||||
|
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
arbiter_verdict TEXT,
|
||||||
|
arbiter_requested_at TEXT,
|
||||||
|
completion_reason TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')),
|
||||||
|
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
||||||
|
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
||||||
|
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status
|
||||||
|
ON paired_tasks(chat_jid, status, updated_at);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ptSqlRow = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
||||||
|
)
|
||||||
|
.get() as { sql?: string } | undefined;
|
||||||
|
const ptSql = ptSqlRow?.sql || '';
|
||||||
|
if (ptSql && !ptSql.includes('arbiter_requested')) {
|
||||||
|
const pairedTaskCols = getTableColumns(database, 'paired_tasks');
|
||||||
|
const selectPairedTaskColumn = (columnName: string): string =>
|
||||||
|
pairedTaskCols.includes(columnName)
|
||||||
|
? columnName
|
||||||
|
: `NULL AS ${columnName}`;
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE paired_tasks_new (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
chat_jid TEXT NOT NULL,
|
||||||
|
group_folder TEXT NOT NULL,
|
||||||
|
owner_service_id TEXT,
|
||||||
|
reviewer_service_id TEXT,
|
||||||
|
owner_agent_type TEXT,
|
||||||
|
reviewer_agent_type TEXT,
|
||||||
|
arbiter_agent_type TEXT,
|
||||||
|
title TEXT,
|
||||||
|
source_ref TEXT,
|
||||||
|
plan_notes TEXT,
|
||||||
|
review_requested_at TEXT,
|
||||||
|
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
arbiter_verdict TEXT,
|
||||||
|
arbiter_requested_at TEXT,
|
||||||
|
completion_reason TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')),
|
||||||
|
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
||||||
|
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
||||||
|
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
||||||
|
);
|
||||||
|
INSERT INTO paired_tasks_new (
|
||||||
|
id, chat_jid, group_folder, owner_service_id, reviewer_service_id,
|
||||||
|
owner_agent_type, reviewer_agent_type,
|
||||||
|
arbiter_agent_type, title, source_ref, plan_notes, review_requested_at,
|
||||||
|
round_trip_count, status, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id, chat_jid, group_folder,
|
||||||
|
${selectPairedTaskColumn('owner_service_id')},
|
||||||
|
${selectPairedTaskColumn('reviewer_service_id')},
|
||||||
|
owner_agent_type, reviewer_agent_type,
|
||||||
|
arbiter_agent_type, title, source_ref, plan_notes, review_requested_at,
|
||||||
|
round_trip_count, status, created_at, updated_at
|
||||||
|
FROM paired_tasks;
|
||||||
|
DROP TABLE paired_tasks;
|
||||||
|
ALTER TABLE paired_tasks_new RENAME TO paired_tasks;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status
|
||||||
|
ON paired_tasks(chat_jid, status, updated_at);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE paired_tasks ADD COLUMN completion_reason TEXT`,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const table of [
|
||||||
|
'paired_executions',
|
||||||
|
'paired_approvals',
|
||||||
|
'paired_artifacts',
|
||||||
|
'paired_events',
|
||||||
|
]) {
|
||||||
|
database.exec(`DROP TABLE IF EXISTS ${table}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
|
||||||
|
export const PAIRED_WORKSPACE_PROJECT_SCHEMA_CLEANUP_MIGRATION = {
|
||||||
|
version: 9,
|
||||||
|
name: 'paired_workspace_project_schema_cleanup',
|
||||||
|
apply(database) {
|
||||||
|
const pairedWsSqlRow = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_workspaces'`,
|
||||||
|
)
|
||||||
|
.get() as { sql?: string } | undefined;
|
||||||
|
const pairedWsSql = pairedWsSqlRow?.sql || '';
|
||||||
|
if (pairedWsSql && pairedWsSql.includes('snapshot_source_fingerprint')) {
|
||||||
|
database.exec(`DROP TABLE IF EXISTS paired_workspaces`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS paired_workspaces (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
task_id TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
workspace_dir TEXT NOT NULL,
|
||||||
|
snapshot_source_dir TEXT,
|
||||||
|
snapshot_ref TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'ready',
|
||||||
|
snapshot_refreshed_at TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
CHECK (role IN ('owner', 'reviewer')),
|
||||||
|
CHECK (status IN ('ready', 'stale'))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_paired_workspaces_task_role
|
||||||
|
ON paired_workspaces(task_id, role);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pairedProjSqlRow = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_projects'`,
|
||||||
|
)
|
||||||
|
.get() as { sql?: string } | undefined;
|
||||||
|
const pairedProjSql = pairedProjSqlRow?.sql || '';
|
||||||
|
if (pairedProjSql && pairedProjSql.includes('workspace_topology')) {
|
||||||
|
database.exec(`DROP TABLE IF EXISTS paired_projects`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS paired_projects (
|
||||||
|
chat_jid TEXT PRIMARY KEY,
|
||||||
|
group_folder TEXT NOT NULL,
|
||||||
|
canonical_work_dir TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
@@ -6,6 +6,9 @@ import { ROOM_REGISTRATION_CANONICAL_COLUMNS_MIGRATION } from './003_room-regist
|
|||||||
import { MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION } from './004_message-seq-and-work-item-indexes.js';
|
import { MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION } from './004_message-seq-and-work-item-indexes.js';
|
||||||
import { SESSIONS_COMPOSITE_KEY_MIGRATION } from './005_sessions-composite-key.js';
|
import { SESSIONS_COMPOSITE_KEY_MIGRATION } from './005_sessions-composite-key.js';
|
||||||
import { CHAT_CHANNEL_METADATA_MIGRATION } from './006_chat-channel-metadata.js';
|
import { CHAT_CHANNEL_METADATA_MIGRATION } from './006_chat-channel-metadata.js';
|
||||||
|
import { RUNTIME_SERVICE_METADATA_MIGRATION } from './007_runtime-service-metadata.js';
|
||||||
|
import { PAIRED_TASK_SCHEMA_CLEANUP_MIGRATION } from './008_paired-task-schema-cleanup.js';
|
||||||
|
import { PAIRED_WORKSPACE_PROJECT_SCHEMA_CLEANUP_MIGRATION } from './009_paired-workspace-project-schema-cleanup.js';
|
||||||
import type {
|
import type {
|
||||||
SchemaMigrationArgs,
|
SchemaMigrationArgs,
|
||||||
SchemaMigrationDefinition,
|
SchemaMigrationDefinition,
|
||||||
@@ -20,6 +23,9 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
|||||||
MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION,
|
MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION,
|
||||||
SESSIONS_COMPOSITE_KEY_MIGRATION,
|
SESSIONS_COMPOSITE_KEY_MIGRATION,
|
||||||
CHAT_CHANNEL_METADATA_MIGRATION,
|
CHAT_CHANNEL_METADATA_MIGRATION,
|
||||||
|
RUNTIME_SERVICE_METADATA_MIGRATION,
|
||||||
|
PAIRED_TASK_SCHEMA_CLEANUP_MIGRATION,
|
||||||
|
PAIRED_WORKSPACE_PROJECT_SCHEMA_CLEANUP_MIGRATION,
|
||||||
];
|
];
|
||||||
|
|
||||||
function ensureSchemaMigrationsTable(database: Database): void {
|
function ensureSchemaMigrationsTable(database: Database): void {
|
||||||
@@ -68,6 +74,7 @@ export function applyVersionedSchemaMigrations(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fail fast: a partially-applied migration must not be recorded as complete.
|
||||||
migration.apply(database, args);
|
migration.apply(database, args);
|
||||||
if (!alreadyApplied) {
|
if (!alreadyApplied) {
|
||||||
recordAppliedSchemaMigration(database, migration);
|
recordAppliedSchemaMigration(database, migration);
|
||||||
|
|||||||
551
src/db/schema.ts
551
src/db/schema.ts
@@ -1,13 +1,6 @@
|
|||||||
import { Database } from 'bun:sqlite';
|
import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
import { SERVICE_SESSION_SCOPE, normalizeServiceId } from '../config.js';
|
import { inferExecutionLeaseServiceIdFromCanonicalMetadata } from './canonical-role-metadata.js';
|
||||||
import { resolveRoleServiceShadow } from '../role-service-shadow.js';
|
|
||||||
import {
|
|
||||||
fillCanonicalChannelOwnerLeaseMetadata,
|
|
||||||
fillCanonicalPairedTaskMetadata,
|
|
||||||
fillCanonicalServiceHandoffMetadata,
|
|
||||||
inferExecutionLeaseServiceIdFromCanonicalMetadata,
|
|
||||||
} from './canonical-role-metadata.js';
|
|
||||||
import {
|
import {
|
||||||
backfillPairedTurnAttemptsFromTurns,
|
backfillPairedTurnAttemptsFromTurns,
|
||||||
buildPairedTurnAttemptId,
|
buildPairedTurnAttemptId,
|
||||||
@@ -17,7 +10,6 @@ import {
|
|||||||
tableHasColumn,
|
tableHasColumn,
|
||||||
tryExecMigration,
|
tryExecMigration,
|
||||||
} from './migrations/helpers.js';
|
} from './migrations/helpers.js';
|
||||||
import { normalizeStoredAgentType } from './room-registration.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
|
||||||
@@ -167,42 +159,6 @@ interface LegacyExecutionLeaseServiceRow {
|
|||||||
arbiter_agent_type?: string | null;
|
arbiter_agent_type?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredPairedTaskServiceRow {
|
|
||||||
rowid: number;
|
|
||||||
id: string;
|
|
||||||
chat_jid: string;
|
|
||||||
group_folder: string;
|
|
||||||
owner_service_id?: string | null;
|
|
||||||
reviewer_service_id?: string | null;
|
|
||||||
owner_agent_type?: string | null;
|
|
||||||
reviewer_agent_type?: string | null;
|
|
||||||
arbiter_agent_type?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoredChannelOwnerLeaseServiceRow {
|
|
||||||
rowid: number;
|
|
||||||
chat_jid: 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 StoredServiceHandoffServiceRow {
|
|
||||||
id: number;
|
|
||||||
chat_jid: string;
|
|
||||||
group_folder: string;
|
|
||||||
source_service_id?: string | null;
|
|
||||||
target_service_id?: string | null;
|
|
||||||
source_role?: string | null;
|
|
||||||
target_role?: string | null;
|
|
||||||
source_agent_type?: string | null;
|
|
||||||
target_agent_type?: string | null;
|
|
||||||
intended_role?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PairedTurnAttemptTimingRow {
|
interface PairedTurnAttemptTimingRow {
|
||||||
turn_id: string;
|
turn_id: string;
|
||||||
attempt_no: number;
|
attempt_no: number;
|
||||||
@@ -2113,27 +2069,6 @@ function applyPairedTurnAttemptProvenanceConstraints(database: Database): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredWorkItemServiceRow {
|
|
||||||
id: number;
|
|
||||||
agent_type?: string | null;
|
|
||||||
service_id?: string | null;
|
|
||||||
delivery_role?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePairedRole(
|
|
||||||
role: string | null | undefined,
|
|
||||||
): 'owner' | 'reviewer' | 'arbiter' | null {
|
|
||||||
return role === 'owner' || role === 'reviewer' || role === 'arbiter'
|
|
||||||
? role
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferLegacyExecutionLeaseServiceId(
|
|
||||||
row: LegacyExecutionLeaseServiceRow,
|
|
||||||
): string | null {
|
|
||||||
return inferExecutionLeaseServiceIdFromCanonicalMetadata(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
||||||
if (
|
if (
|
||||||
!tableHasColumn(
|
!tableHasColumn(
|
||||||
@@ -2200,7 +2135,8 @@ function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
|||||||
const tx = database.transaction(
|
const tx = database.transaction(
|
||||||
(leaseRows: LegacyExecutionLeaseServiceRow[]) => {
|
(leaseRows: LegacyExecutionLeaseServiceRow[]) => {
|
||||||
for (const row of leaseRows) {
|
for (const row of leaseRows) {
|
||||||
const claimedServiceId = inferLegacyExecutionLeaseServiceId(row);
|
const claimedServiceId =
|
||||||
|
inferExecutionLeaseServiceIdFromCanonicalMetadata(row);
|
||||||
if (!claimedServiceId) {
|
if (!claimedServiceId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2212,302 +2148,6 @@ function backfillLegacyExecutionLeaseServiceShadows(database: Database): void {
|
|||||||
tx(rows);
|
tx(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function backfillCanonicalPairedTaskServiceIds(database: Database): void {
|
|
||||||
if (
|
|
||||||
!tableHasColumn(database, 'paired_tasks', 'owner_service_id') ||
|
|
||||||
!tableHasColumn(database, 'paired_tasks', 'reviewer_service_id')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
rowid,
|
|
||||||
id,
|
|
||||||
chat_jid,
|
|
||||||
group_folder,
|
|
||||||
owner_service_id,
|
|
||||||
reviewer_service_id,
|
|
||||||
owner_agent_type,
|
|
||||||
reviewer_agent_type,
|
|
||||||
arbiter_agent_type
|
|
||||||
FROM paired_tasks
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.all() as StoredPairedTaskServiceRow[];
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = database.prepare(
|
|
||||||
`
|
|
||||||
UPDATE paired_tasks
|
|
||||||
SET owner_service_id = ?,
|
|
||||||
reviewer_service_id = ?,
|
|
||||||
owner_agent_type = ?,
|
|
||||||
reviewer_agent_type = ?,
|
|
||||||
arbiter_agent_type = ?
|
|
||||||
WHERE rowid = ?
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
const tx = database.transaction((taskRows: StoredPairedTaskServiceRow[]) => {
|
|
||||||
for (const row of taskRows) {
|
|
||||||
const {
|
|
||||||
ownerAgentType,
|
|
||||||
reviewerAgentType,
|
|
||||||
arbiterAgentType,
|
|
||||||
ownerServiceId,
|
|
||||||
reviewerServiceId,
|
|
||||||
} = fillCanonicalPairedTaskMetadata({
|
|
||||||
id: row.id,
|
|
||||||
owner_service_id: row.owner_service_id,
|
|
||||||
reviewer_service_id: row.reviewer_service_id,
|
|
||||||
owner_agent_type: row.owner_agent_type,
|
|
||||||
reviewer_agent_type: row.reviewer_agent_type,
|
|
||||||
arbiter_agent_type: row.arbiter_agent_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
row.owner_service_id === ownerServiceId &&
|
|
||||||
row.reviewer_service_id === reviewerServiceId &&
|
|
||||||
row.owner_agent_type === ownerAgentType &&
|
|
||||||
row.reviewer_agent_type === reviewerAgentType &&
|
|
||||||
row.arbiter_agent_type === arbiterAgentType
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
update.run(
|
|
||||||
ownerServiceId,
|
|
||||||
reviewerServiceId,
|
|
||||||
ownerAgentType,
|
|
||||||
reviewerAgentType,
|
|
||||||
arbiterAgentType,
|
|
||||||
row.rowid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tx(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
function backfillCanonicalChannelOwnerServiceIds(database: Database): void {
|
|
||||||
if (!tableHasColumn(database, 'channel_owner', 'owner_service_id')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
rowid,
|
|
||||||
chat_jid,
|
|
||||||
owner_service_id,
|
|
||||||
reviewer_service_id,
|
|
||||||
arbiter_service_id,
|
|
||||||
owner_agent_type,
|
|
||||||
reviewer_agent_type,
|
|
||||||
arbiter_agent_type
|
|
||||||
FROM channel_owner
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.all() as StoredChannelOwnerLeaseServiceRow[];
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = database.prepare(
|
|
||||||
`
|
|
||||||
UPDATE channel_owner
|
|
||||||
SET owner_service_id = ?,
|
|
||||||
reviewer_service_id = ?,
|
|
||||||
arbiter_service_id = ?,
|
|
||||||
owner_agent_type = ?,
|
|
||||||
reviewer_agent_type = ?,
|
|
||||||
arbiter_agent_type = ?
|
|
||||||
WHERE rowid = ?
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
const tx = database.transaction(
|
|
||||||
(leaseRows: StoredChannelOwnerLeaseServiceRow[]) => {
|
|
||||||
for (const row of leaseRows) {
|
|
||||||
const {
|
|
||||||
ownerAgentType,
|
|
||||||
reviewerAgentType,
|
|
||||||
arbiterAgentType,
|
|
||||||
ownerServiceId,
|
|
||||||
reviewerServiceId,
|
|
||||||
arbiterServiceId,
|
|
||||||
} = fillCanonicalChannelOwnerLeaseMetadata({
|
|
||||||
chat_jid: row.chat_jid,
|
|
||||||
owner_service_id: row.owner_service_id,
|
|
||||||
reviewer_service_id: row.reviewer_service_id,
|
|
||||||
arbiter_service_id: row.arbiter_service_id,
|
|
||||||
owner_agent_type: row.owner_agent_type,
|
|
||||||
reviewer_agent_type: row.reviewer_agent_type,
|
|
||||||
arbiter_agent_type: row.arbiter_agent_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
row.owner_service_id === ownerServiceId &&
|
|
||||||
row.reviewer_service_id === reviewerServiceId &&
|
|
||||||
row.arbiter_service_id === arbiterServiceId &&
|
|
||||||
row.owner_agent_type === ownerAgentType &&
|
|
||||||
row.reviewer_agent_type === reviewerAgentType &&
|
|
||||||
row.arbiter_agent_type === arbiterAgentType
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
update.run(
|
|
||||||
ownerServiceId,
|
|
||||||
reviewerServiceId,
|
|
||||||
arbiterServiceId,
|
|
||||||
ownerAgentType,
|
|
||||||
reviewerAgentType,
|
|
||||||
arbiterAgentType,
|
|
||||||
row.rowid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tx(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
function backfillCanonicalServiceHandoffServiceIds(database: Database): void {
|
|
||||||
if (!tableHasColumn(database, 'service_handoffs', 'source_service_id')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
chat_jid,
|
|
||||||
group_folder,
|
|
||||||
source_service_id,
|
|
||||||
target_service_id,
|
|
||||||
source_role,
|
|
||||||
target_role,
|
|
||||||
source_agent_type,
|
|
||||||
target_agent_type,
|
|
||||||
intended_role
|
|
||||||
FROM service_handoffs
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.all() as StoredServiceHandoffServiceRow[];
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = database.prepare(
|
|
||||||
`
|
|
||||||
UPDATE service_handoffs
|
|
||||||
SET source_service_id = ?,
|
|
||||||
target_service_id = ?,
|
|
||||||
source_agent_type = ?,
|
|
||||||
target_agent_type = ?,
|
|
||||||
source_role = ?,
|
|
||||||
target_role = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
const tx = database.transaction(
|
|
||||||
(handoffRows: StoredServiceHandoffServiceRow[]) => {
|
|
||||||
for (const row of handoffRows) {
|
|
||||||
const {
|
|
||||||
sourceRole,
|
|
||||||
targetRole,
|
|
||||||
sourceAgentType,
|
|
||||||
targetAgentType,
|
|
||||||
sourceServiceId,
|
|
||||||
targetServiceId,
|
|
||||||
} = fillCanonicalServiceHandoffMetadata({
|
|
||||||
id: row.id,
|
|
||||||
chat_jid: row.chat_jid,
|
|
||||||
source_service_id: row.source_service_id,
|
|
||||||
target_service_id: row.target_service_id,
|
|
||||||
source_role: row.source_role,
|
|
||||||
target_role: row.target_role,
|
|
||||||
intended_role: row.intended_role,
|
|
||||||
source_agent_type: row.source_agent_type,
|
|
||||||
target_agent_type: row.target_agent_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
row.source_service_id === sourceServiceId &&
|
|
||||||
row.target_service_id === targetServiceId &&
|
|
||||||
row.source_agent_type === (sourceAgentType ?? null) &&
|
|
||||||
row.target_agent_type === targetAgentType &&
|
|
||||||
row.source_role === sourceRole &&
|
|
||||||
row.target_role === targetRole
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
update.run(
|
|
||||||
sourceServiceId,
|
|
||||||
targetServiceId,
|
|
||||||
sourceAgentType,
|
|
||||||
targetAgentType,
|
|
||||||
sourceRole,
|
|
||||||
targetRole,
|
|
||||||
row.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tx(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
function backfillCanonicalWorkItemServiceIds(database: Database): void {
|
|
||||||
if (!tableHasColumn(database, 'work_items', 'service_id')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT id, agent_type, service_id, delivery_role
|
|
||||||
FROM work_items`,
|
|
||||||
)
|
|
||||||
.all() as StoredWorkItemServiceRow[];
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = database.prepare(
|
|
||||||
`UPDATE work_items
|
|
||||||
SET service_id = ?
|
|
||||||
WHERE id = ?`,
|
|
||||||
);
|
|
||||||
const tx = database.transaction(
|
|
||||||
(workItemRows: StoredWorkItemServiceRow[]) => {
|
|
||||||
for (const row of workItemRows) {
|
|
||||||
const agentType =
|
|
||||||
normalizeStoredAgentType(row.agent_type) ?? 'claude-code';
|
|
||||||
const deliveryRole = normalizePairedRole(row.delivery_role) ?? 'owner';
|
|
||||||
const serviceId =
|
|
||||||
(row.service_id ? normalizeServiceId(row.service_id) : null) ??
|
|
||||||
resolveRoleServiceShadow(deliveryRole, agentType) ??
|
|
||||||
SERVICE_SESSION_SCOPE;
|
|
||||||
|
|
||||||
if (row.service_id === serviceId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
update.run(serviceId, row.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tx(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyLegacySchemaMigrations(
|
export function applyLegacySchemaMigrations(
|
||||||
database: Database,
|
database: Database,
|
||||||
_args: {
|
_args: {
|
||||||
@@ -2651,7 +2291,6 @@ export function applyLegacySchemaMigrations(
|
|||||||
CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at
|
CREATE INDEX IF NOT EXISTS idx_paired_task_execution_leases_expires_at
|
||||||
ON 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`,
|
||||||
@@ -2763,190 +2402,6 @@ export function applyLegacySchemaMigrations(
|
|||||||
rebuildPairedTurnsWithoutLegacyScratchColumns(database);
|
rebuildPairedTurnsWithoutLegacyScratchColumns(database);
|
||||||
rebuildPairedTurnAttemptForeignKeyTables(database);
|
rebuildPairedTurnAttemptForeignKeyTables(database);
|
||||||
applyPairedTurnAttemptProvenanceConstraints(database);
|
applyPairedTurnAttemptProvenanceConstraints(database);
|
||||||
|
|
||||||
for (const column of [
|
|
||||||
'owner_service_id',
|
|
||||||
'reviewer_service_id',
|
|
||||||
'arbiter_service_id',
|
|
||||||
'owner_agent_type',
|
|
||||||
'reviewer_agent_type',
|
|
||||||
'arbiter_agent_type',
|
|
||||||
]) {
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE channel_owner ADD COLUMN ${column} TEXT`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
backfillCanonicalPairedTaskServiceIds(database);
|
|
||||||
backfillCanonicalChannelOwnerServiceIds(database);
|
|
||||||
backfillCanonicalServiceHandoffServiceIds(database);
|
|
||||||
backfillCanonicalWorkItemServiceIds(database);
|
|
||||||
|
|
||||||
const pairedTasksSqlRow = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
|
||||||
)
|
|
||||||
.get() as { sql?: string } | undefined;
|
|
||||||
const pairedTasksSql = pairedTasksSqlRow?.sql || '';
|
|
||||||
const pairedTasksNeedsRebuild =
|
|
||||||
pairedTasksSql &&
|
|
||||||
(pairedTasksSql.includes('task_policy') ||
|
|
||||||
!pairedTasksSql.includes('round_trip_count'));
|
|
||||||
if (pairedTasksNeedsRebuild) {
|
|
||||||
database.exec(`DROP TABLE IF EXISTS paired_tasks`);
|
|
||||||
database.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS paired_tasks (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
chat_jid TEXT NOT NULL,
|
|
||||||
group_folder TEXT NOT NULL,
|
|
||||||
owner_service_id TEXT NOT NULL,
|
|
||||||
reviewer_service_id TEXT NOT NULL,
|
|
||||||
owner_agent_type TEXT,
|
|
||||||
reviewer_agent_type TEXT,
|
|
||||||
arbiter_agent_type TEXT,
|
|
||||||
title TEXT,
|
|
||||||
source_ref TEXT,
|
|
||||||
plan_notes TEXT,
|
|
||||||
review_requested_at TEXT,
|
|
||||||
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
status TEXT NOT NULL DEFAULT 'active',
|
|
||||||
arbiter_verdict TEXT,
|
|
||||||
arbiter_requested_at TEXT,
|
|
||||||
completion_reason TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')),
|
|
||||||
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
|
||||||
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
|
||||||
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status
|
|
||||||
ON paired_tasks(chat_jid, status, updated_at);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const ptSqlRow = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
|
||||||
)
|
|
||||||
.get() as { sql?: string } | undefined;
|
|
||||||
const ptSql = ptSqlRow?.sql || '';
|
|
||||||
if (ptSql && !ptSql.includes('arbiter_requested')) {
|
|
||||||
const pairedTaskCols = getTableColumns(database, 'paired_tasks');
|
|
||||||
const selectPairedTaskColumn = (columnName: string): string =>
|
|
||||||
pairedTaskCols.includes(columnName)
|
|
||||||
? columnName
|
|
||||||
: `NULL AS ${columnName}`;
|
|
||||||
database.exec(`
|
|
||||||
CREATE TABLE paired_tasks_new (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
chat_jid TEXT NOT NULL,
|
|
||||||
group_folder TEXT NOT NULL,
|
|
||||||
owner_service_id TEXT,
|
|
||||||
reviewer_service_id TEXT,
|
|
||||||
owner_agent_type TEXT,
|
|
||||||
reviewer_agent_type TEXT,
|
|
||||||
arbiter_agent_type TEXT,
|
|
||||||
title TEXT,
|
|
||||||
source_ref TEXT,
|
|
||||||
plan_notes TEXT,
|
|
||||||
review_requested_at TEXT,
|
|
||||||
round_trip_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
status TEXT NOT NULL DEFAULT 'active',
|
|
||||||
arbiter_verdict TEXT,
|
|
||||||
arbiter_requested_at TEXT,
|
|
||||||
completion_reason TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
CHECK (status IN ('active', 'review_ready', 'in_review', 'merge_ready', 'completed', 'arbiter_requested', 'in_arbitration')),
|
|
||||||
CHECK (owner_agent_type IN ('claude-code', 'codex') OR owner_agent_type IS NULL),
|
|
||||||
CHECK (reviewer_agent_type IN ('claude-code', 'codex') OR reviewer_agent_type IS NULL),
|
|
||||||
CHECK (arbiter_agent_type IN ('claude-code', 'codex') OR arbiter_agent_type IS NULL)
|
|
||||||
);
|
|
||||||
INSERT INTO paired_tasks_new (
|
|
||||||
id, chat_jid, group_folder, owner_service_id, reviewer_service_id,
|
|
||||||
owner_agent_type, reviewer_agent_type,
|
|
||||||
arbiter_agent_type, title, source_ref, plan_notes, review_requested_at,
|
|
||||||
round_trip_count, status, created_at, updated_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id, chat_jid, group_folder,
|
|
||||||
${selectPairedTaskColumn('owner_service_id')},
|
|
||||||
${selectPairedTaskColumn('reviewer_service_id')},
|
|
||||||
owner_agent_type, reviewer_agent_type,
|
|
||||||
arbiter_agent_type, title, source_ref, plan_notes, review_requested_at,
|
|
||||||
round_trip_count, status, created_at, updated_at
|
|
||||||
FROM paired_tasks;
|
|
||||||
DROP TABLE paired_tasks;
|
|
||||||
ALTER TABLE paired_tasks_new RENAME TO paired_tasks;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_paired_tasks_chat_status
|
|
||||||
ON paired_tasks(chat_jid, status, updated_at);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE paired_tasks ADD COLUMN completion_reason TEXT`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const table of [
|
|
||||||
'paired_executions',
|
|
||||||
'paired_approvals',
|
|
||||||
'paired_artifacts',
|
|
||||||
'paired_events',
|
|
||||||
]) {
|
|
||||||
database.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairedWsSqlRow = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_workspaces'`,
|
|
||||||
)
|
|
||||||
.get() as { sql?: string } | undefined;
|
|
||||||
const pairedWsSql = pairedWsSqlRow?.sql || '';
|
|
||||||
if (pairedWsSql && pairedWsSql.includes('snapshot_source_fingerprint')) {
|
|
||||||
database.exec(`DROP TABLE IF EXISTS paired_workspaces`);
|
|
||||||
database.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS paired_workspaces (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
task_id TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
workspace_dir TEXT NOT NULL,
|
|
||||||
snapshot_source_dir TEXT,
|
|
||||||
snapshot_ref TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'ready',
|
|
||||||
snapshot_refreshed_at TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
CHECK (role IN ('owner', 'reviewer')),
|
|
||||||
CHECK (status IN ('ready', 'stale'))
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_paired_workspaces_task_role
|
|
||||||
ON paired_workspaces(task_id, role);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairedProjSqlRow = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_projects'`,
|
|
||||||
)
|
|
||||||
.get() as { sql?: string } | undefined;
|
|
||||||
const pairedProjSql = pairedProjSqlRow?.sql || '';
|
|
||||||
if (pairedProjSql && pairedProjSql.includes('workspace_topology')) {
|
|
||||||
database.exec(`DROP TABLE IF EXISTS paired_projects`);
|
|
||||||
database.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS paired_projects (
|
|
||||||
chat_jid TEXT PRIMARY KEY,
|
|
||||||
group_folder TEXT NOT NULL,
|
|
||||||
canonical_work_dir TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated New schema changes should be added under src/db/migrations/*. */
|
/** @deprecated New schema changes should be added under src/db/migrations/*. */
|
||||||
|
|||||||
Reference in New Issue
Block a user