Finalize turn-role invariants and legacy migration cutover

This commit is contained in:
ejclaw
2026-04-10 11:44:34 +09:00
parent 7753ee46f0
commit c67bd1c622
45 changed files with 15014 additions and 1381 deletions

View File

@@ -2,27 +2,41 @@ import { Database } from 'bun:sqlite';
import fs from 'fs';
import path from 'path';
import { ASSISTANT_NAME, DATA_DIR, STORE_DIR } from '../config.js';
import { logger } from '../logger.js';
import type { RegisteredGroup } from '../types.js';
import { readJsonFile } from '../utils.js';
import { ASSISTANT_NAME, STORE_DIR } from '../config.js';
import { applyBaseSchema } from './base-schema.js';
import { applySchemaMigrations } from './schema.js';
export interface JsonStateMigrationHooks {
setRouterState(key: string, value: string): void;
setSession(groupFolder: string, sessionId: string): void;
writeLegacyRegisteredGroupAndSyncRoomSettings(
jid: string,
group: RegisteredGroup,
): void;
function setForeignKeys(database: Database, enabled: boolean): void {
database.exec(`PRAGMA foreign_keys = ${enabled ? 'ON' : 'OFF'}`);
}
function assertNoForeignKeyViolations(database: Database): void {
const violations = database
.prepare('PRAGMA foreign_key_check')
.all() as Array<{
table: string;
rowid: number;
parent: string;
fkid: number;
}>;
if (violations.length === 0) {
return;
}
const violation = violations[0]!;
throw new Error(
`Foreign key integrity check failed for ${violation.table}(rowid=${violation.rowid}) referencing ${violation.parent} [fk=${violation.fkid}]`,
);
}
export function initializeDatabaseSchema(database: Database): void {
setForeignKeys(database, false);
applyBaseSchema(database);
applySchemaMigrations(database, {
assistantName: ASSISTANT_NAME,
});
setForeignKeys(database, true);
assertNoForeignKeyViolations(database);
}
export function openPersistentDatabase(): Database {
@@ -42,62 +56,3 @@ export function openInMemoryDatabase(): Database {
export function openDatabaseFromFile(dbPath: string): Database {
return new Database(dbPath);
}
export function migrateJsonStateFromFiles(
hooks: JsonStateMigrationHooks,
): void {
const migrateFile = (filename: string) => {
const filePath = path.join(DATA_DIR, filename);
const data = readJsonFile(filePath);
if (data === null) return null;
try {
fs.renameSync(filePath, `${filePath}.migrated`);
} catch {
/* best effort */
}
return data;
};
const routerState = migrateFile('router_state.json') as {
last_timestamp?: string;
last_agent_timestamp?: Record<string, string>;
} | null;
if (routerState) {
if (routerState.last_timestamp) {
hooks.setRouterState('last_timestamp', routerState.last_timestamp);
}
if (routerState.last_agent_timestamp) {
hooks.setRouterState(
'last_agent_timestamp',
JSON.stringify(routerState.last_agent_timestamp),
);
}
}
const sessions = migrateFile('sessions.json') as Record<
string,
string
> | null;
if (sessions) {
for (const [folder, sessionId] of Object.entries(sessions)) {
hooks.setSession(folder, sessionId);
}
}
const groups = migrateFile('registered_groups.json') as Record<
string,
RegisteredGroup
> | null;
if (groups) {
for (const [jid, group] of Object.entries(groups)) {
try {
hooks.writeLegacyRegisteredGroupAndSyncRoomSettings(jid, group);
} catch (err) {
logger.warn(
{ jid, folder: group.folder, err },
'Skipping migrated registered group with invalid folder',
);
}
}
}
}