refactor: neutralize sqlite snapshot helpers

This commit is contained in:
Peter Steinberger
2026-05-09 05:16:49 +01:00
parent 6d8360018a
commit 2db9ccbeb0
19 changed files with 58 additions and 57 deletions

View File

@@ -134,8 +134,8 @@ The branch already has a real shared SQLite base:
input only; runtime no longer reads or writes TTS prefs JSON files, and the
legacy path resolver lives in the doctor migration module.
- Subagent run recovery and OpenRouter model capability cache runtime modules
now keep SQLite readers/writers separate from doctor-only legacy JSON import
helpers.
now keep SQLite snapshot readers/writers separate from doctor-only legacy JSON
import helpers.
- `src/agents/filesystem/virtual-agent-fs.sqlite.ts` implements a SQLite VFS
over the agent database `vfs_entries` table.
- `src/agents/runtime-worker.entry.ts` creates per-run SQLite VFS, tool artifact,
@@ -147,13 +147,14 @@ The branch already has a real shared SQLite base:
Doctor imports legacy `~/.openclaw/exec-approvals.json`; runtime writes no
longer create or rewrite that file.
- Device identity, device auth, and bootstrap runtime modules now keep their
SQLite readers/writers separate from doctor-only legacy JSON import helpers.
- Web push, APNs, Voice Wake, and Voice Wake routing runtime modules now keep
their SQLite readers/writers separate from doctor-only legacy JSON import
SQLite snapshot readers/writers separate from doctor-only legacy JSON import
helpers.
- Web push, APNs, Voice Wake, and Voice Wake routing runtime modules now keep
their SQLite snapshot readers/writers separate from doctor-only legacy JSON
import helpers.
- Pairing state, plugin binding approvals, and cron job state now follow the
same split: runtime modules expose SQLite-backed operations and narrow
migration writers, while doctor imports/removes the old JSON files through
same split: runtime modules expose SQLite-backed operations and neutral
snapshot helpers, while doctor imports/removes the old JSON files through
`src/commands/doctor/legacy/*` modules.
- Core pairing and cron runtime modules no longer export legacy JSON path
builders. Doctor-owned legacy modules construct `pending.json`, `paired.json`,

View File

@@ -164,7 +164,7 @@ function readPersistentCache(): Map<string, OpenRouterModelCapabilities> | undef
return readSqliteCache();
}
export function writeOpenRouterModelCapabilitiesCacheForMigration(
export function writeOpenRouterModelCapabilitiesCacheSnapshot(
map: Map<string, OpenRouterModelCapabilities>,
env?: NodeJS.ProcessEnv,
): void {

View File

@@ -110,7 +110,7 @@ function normalizePersistedRunRecords(params: {
return out;
}
export function normalizeSubagentRunRecordsForMigration(params: {
export function normalizeSubagentRunRecordsSnapshot(params: {
runsRaw: Record<string, unknown>;
isLegacy: boolean;
}): Map<string, SubagentRunRecord> {
@@ -372,7 +372,7 @@ function writeSubagentRegistryRunsToSqlite(
}, subagentRegistryDbOptions(env));
}
export function writeSubagentRegistryRunsForMigration(
export function writeSubagentRegistryRunsSnapshot(
runs: Map<string, SubagentRunRecord>,
env: NodeJS.ProcessEnv = process.env,
): void {

View File

@@ -2,8 +2,8 @@ import fs from "node:fs";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
parseDeviceAuthStoreForMigration,
writeDeviceAuthStoreForMigration,
parseDeviceAuthStoreSnapshot,
writeDeviceAuthStoreSnapshot,
} from "../../../infra/device-auth-store.js";
function resolveDeviceAuthPath(env: NodeJS.ProcessEnv = process.env): string {
@@ -32,11 +32,11 @@ export function importLegacyDeviceAuthFileToSqlite(env: NodeJS.ProcessEnv = proc
}
throw error;
}
const store = parseDeviceAuthStoreForMigration(parsed);
const store = parseDeviceAuthStoreSnapshot(parsed);
if (!store) {
return { imported: false, tokens: 0 };
}
writeDeviceAuthStoreForMigration(env, store);
writeDeviceAuthStoreSnapshot(env, store);
try {
fs.rmSync(filePath, { force: true });
} catch {

View File

@@ -2,8 +2,8 @@ import fs from "node:fs";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
parseStoredDeviceIdentityForMigration,
writeStoredDeviceIdentityForMigration,
parseStoredDeviceIdentitySnapshot,
writeStoredDeviceIdentitySnapshot,
} from "../../../infra/device-identity.js";
function resolveIdentityPathForEnv(env: NodeJS.ProcessEnv = process.env): string {
@@ -31,11 +31,11 @@ export function importLegacyDeviceIdentityFileToSqlite(env: NodeJS.ProcessEnv =
}
throw error;
}
const stored = parseStoredDeviceIdentityForMigration(parsed);
const stored = parseStoredDeviceIdentitySnapshot(parsed);
if (!stored) {
return { imported: false };
}
writeStoredDeviceIdentityForMigration(filePath, stored);
writeStoredDeviceIdentitySnapshot(filePath, stored);
try {
fs.rmSync(filePath, { force: true });
} catch {

View File

@@ -2,7 +2,7 @@ import { existsSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import {
parseOpenRouterModelCapabilitiesCachePayload,
writeOpenRouterModelCapabilitiesCacheForMigration,
writeOpenRouterModelCapabilitiesCacheSnapshot,
type OpenRouterModelCapabilities,
} from "../../../agents/pi-embedded-runner/openrouter-model-capabilities.js";
import { resolveStateDir } from "../../../config/paths.js";
@@ -47,7 +47,7 @@ export function importLegacyOpenRouterModelCapabilitiesCacheToSqlite(
}
const legacyJsonCache = readLegacyJsonCache(env);
if (legacyJsonCache) {
writeOpenRouterModelCapabilitiesCacheForMigration(legacyJsonCache, env);
writeOpenRouterModelCapabilitiesCacheSnapshot(legacyJsonCache, env);
}
try {
unlinkSync(resolveLegacyJsonCachePath(env));

View File

@@ -3,8 +3,8 @@ import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import { expandHomePrefix } from "../../../infra/home-dir.js";
import {
normalizePluginBindingApprovalsForMigration,
writePluginBindingApprovalsForMigration,
normalizePluginBindingApprovalsSnapshot,
writePluginBindingApprovalsSnapshot,
} from "../../../plugins/conversation-binding.js";
const LEGACY_APPROVALS_PATH = "~/.openclaw/plugin-binding-approvals.json";
@@ -35,10 +35,10 @@ export function importLegacyPluginBindingApprovalFileToSqlite(): {
if (!legacyPluginBindingApprovalFileExists()) {
return { imported: false, approvals: 0 };
}
const file = normalizePluginBindingApprovalsForMigration(
const file = normalizePluginBindingApprovalsSnapshot(
JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown,
);
writePluginBindingApprovalsForMigration(file);
writePluginBindingApprovalsSnapshot(file);
try {
fs.unlinkSync(filePath);
} catch {

View File

@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
normalizeApnsRegistrationStateForMigration,
writeApnsRegistrationStateForMigration,
normalizeApnsRegistrationStateSnapshot,
writeApnsRegistrationStateSnapshot,
} from "../../../infra/push-apns.js";
const LEGACY_APNS_STATE_FILENAME = "push/apns-registrations.json";
@@ -33,11 +33,11 @@ export async function importLegacyApnsRegistrationFileToSqlite(baseDir?: string)
}
throw error;
}
const normalized = normalizeApnsRegistrationStateForMigration(parsed);
const normalized = normalizeApnsRegistrationStateSnapshot(parsed);
if (!normalized) {
return { imported: false, registrations: 0 };
}
await writeApnsRegistrationStateForMigration(normalized, baseDir);
await writeApnsRegistrationStateSnapshot(normalized, baseDir);
await fs.rm(filePath, { force: true }).catch(() => undefined);
return { imported: true, registrations: Object.keys(normalized.registrationsByNodeId).length };
}

View File

@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
writeWebPushRegistrationStateForMigration,
writeWebPushVapidKeysForMigration,
writeWebPushRegistrationStateSnapshot,
writeWebPushVapidKeysSnapshot,
type VapidKeyPair,
type WebPushRegistrationState,
} from "../../../infra/push-web.js";
@@ -45,7 +45,7 @@ export async function importLegacyWebPushFilesToSqlite(baseDir?: string): Promis
try {
const state = JSON.parse(await fs.readFile(statePath, "utf8")) as WebPushRegistrationState;
if (state && typeof state === "object") {
await writeWebPushRegistrationStateForMigration(state, baseDir);
await writeWebPushRegistrationStateSnapshot(state, baseDir);
subscriptions = Object.keys(state.subscriptionsByEndpointHash ?? {}).length;
await fs.rm(statePath, { force: true }).catch(() => undefined);
files += 1;
@@ -60,7 +60,7 @@ export async function importLegacyWebPushFilesToSqlite(baseDir?: string): Promis
try {
const keys = JSON.parse(await fs.readFile(vapidPath, "utf8")) as VapidKeyPair;
if (keys?.publicKey && keys.privateKey) {
writeWebPushVapidKeysForMigration(keys, baseDir);
writeWebPushVapidKeysSnapshot(keys, baseDir);
await fs.rm(vapidPath, { force: true }).catch(() => undefined);
importedVapidKeys = true;
files += 1;

View File

@@ -1,9 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import {
normalizeSubagentRunRecordsForMigration,
normalizeSubagentRunRecordsSnapshot,
resolveSubagentStateDir,
writeSubagentRegistryRunsForMigration,
writeSubagentRegistryRunsSnapshot,
} from "../../../agents/subagent-registry.store.js";
import type { SubagentRunRecord } from "../../../agents/subagent-registry.types.js";
import { loadJsonFile } from "../../../infra/json-file.js";
@@ -44,7 +44,7 @@ function loadLegacySubagentRegistryFile(pathname: string): Map<string, SubagentR
if (!runsRaw || typeof runsRaw !== "object") {
return new Map();
}
return normalizeSubagentRunRecordsForMigration({
return normalizeSubagentRunRecordsSnapshot({
runsRaw: runsRaw as Record<string, unknown>,
isLegacy: record.version === 1,
});
@@ -70,7 +70,7 @@ export function importLegacySubagentRegistryFileToSqlite(env: NodeJS.ProcessEnv
return { imported: false, runs: 0 };
}
const runs = loadLegacySubagentRegistryFile(pathname);
writeSubagentRegistryRunsForMigration(runs, env);
writeSubagentRegistryRunsSnapshot(runs, env);
try {
fs.unlinkSync(pathname);
} catch {

View File

@@ -3,7 +3,7 @@ import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
normalizeVoiceWakeRoutingConfig,
writeVoiceWakeRoutingConfigForMigration,
writeVoiceWakeRoutingConfigSnapshot,
} from "../../../infra/voicewake-routing.js";
function resolveLegacyPath(baseDir?: string) {
@@ -38,7 +38,7 @@ export async function importLegacyVoiceWakeRoutingConfigFileToSqlite(baseDir?: s
throw error;
}
const normalized = normalizeVoiceWakeRoutingConfig(raw);
writeVoiceWakeRoutingConfigForMigration(normalized, baseDir);
writeVoiceWakeRoutingConfigSnapshot(normalized, baseDir);
await fs.rm(filePath, { force: true }).catch(() => undefined);
return { imported: true, routes: normalized.routes.length };
}

View File

@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import {
normalizeVoiceWakeConfigForMigration,
writeVoiceWakeConfigForMigration,
normalizeVoiceWakeConfigSnapshot,
writeVoiceWakeConfigSnapshot,
} from "../../../infra/voicewake.js";
function resolveLegacyPath(baseDir?: string) {
@@ -37,8 +37,8 @@ export async function importLegacyVoiceWakeConfigFileToSqlite(baseDir?: string):
}
throw error;
}
const normalized = normalizeVoiceWakeConfigForMigration(raw);
writeVoiceWakeConfigForMigration(normalized, baseDir);
const normalized = normalizeVoiceWakeConfigSnapshot(raw);
writeVoiceWakeConfigSnapshot(normalized, baseDir);
await fs.rm(filePath, { force: true }).catch(() => undefined);
return { imported: true, triggers: normalized.triggers.length };
}

View File

@@ -58,12 +58,12 @@ export function storeDeviceAuthStore(params: {
return params.store;
}
export function parseDeviceAuthStoreForMigration(raw: unknown): DeviceAuthStore | null {
export function parseDeviceAuthStoreSnapshot(raw: unknown): DeviceAuthStore | null {
const store = DeviceAuthStoreSchema.safeParse(raw);
return store.success ? store.data : null;
}
export function writeDeviceAuthStoreForMigration(
export function writeDeviceAuthStoreSnapshot(
env: NodeJS.ProcessEnv | undefined,
store: DeviceAuthStore,
): void {

View File

@@ -226,11 +226,11 @@ export function loadDeviceIdentityIfPresentForEnv(
}
}
export function parseStoredDeviceIdentityForMigration(value: unknown): StoredDeviceIdentity | null {
export function parseStoredDeviceIdentitySnapshot(value: unknown): StoredDeviceIdentity | null {
return parseStoredIdentity(value);
}
export function writeStoredDeviceIdentityForMigration(
export function writeStoredDeviceIdentitySnapshot(
filePath: string,
stored: StoredDeviceIdentity,
): void {

View File

@@ -396,7 +396,7 @@ async function persistRegistrationsState(
);
}
export function normalizeApnsRegistrationStateForMigration(
export function normalizeApnsRegistrationStateSnapshot(
parsed: unknown,
): ApnsRegistrationState | null {
if (!parsed || typeof parsed !== "object") {
@@ -419,7 +419,7 @@ export function normalizeApnsRegistrationStateForMigration(
return { registrationsByNodeId: normalized };
}
export async function writeApnsRegistrationStateForMigration(
export async function writeApnsRegistrationStateSnapshot(
state: ApnsRegistrationState,
baseDir?: string,
): Promise<void> {

View File

@@ -103,14 +103,14 @@ async function persistState(state: WebPushRegistrationState, baseDir?: string):
);
}
export async function writeWebPushRegistrationStateForMigration(
export async function writeWebPushRegistrationStateSnapshot(
state: WebPushRegistrationState,
baseDir?: string,
): Promise<void> {
await persistState(state, baseDir);
}
export function writeWebPushVapidKeysForMigration(keys: VapidKeyPair, baseDir?: string): void {
export function writeWebPushVapidKeysSnapshot(keys: VapidKeyPair, baseDir?: string): void {
writeOpenClawStateKvJson<OpenClawStateJsonValue>(
WEB_PUSH_SCOPE,
WEB_PUSH_VAPID_KEY,

View File

@@ -295,7 +295,7 @@ export async function setVoiceWakeRoutingConfig(
return next;
}
export function writeVoiceWakeRoutingConfigForMigration(
export function writeVoiceWakeRoutingConfigSnapshot(
config: VoiceWakeRoutingConfig,
baseDir?: string,
): void {

View File

@@ -66,7 +66,7 @@ export async function setVoiceWakeTriggers(
return next;
}
export function normalizeVoiceWakeConfigForMigration(raw: unknown): VoiceWakeConfig {
export function normalizeVoiceWakeConfigSnapshot(raw: unknown): VoiceWakeConfig {
const updatedAtMs = (raw as Partial<VoiceWakeConfig> | undefined)?.updatedAtMs;
return {
triggers: sanitizeTriggers((raw as Partial<VoiceWakeConfig> | undefined)?.triggers),
@@ -74,7 +74,7 @@ export function normalizeVoiceWakeConfigForMigration(raw: unknown): VoiceWakeCon
};
}
export function writeVoiceWakeConfigForMigration(config: VoiceWakeConfig, baseDir?: string): void {
export function writeVoiceWakeConfigSnapshot(config: VoiceWakeConfig, baseDir?: string): void {
writeOpenClawStateKvJson<OpenClawStateJsonValue>(
VOICEWAKE_SCOPE,
VOICEWAKE_CONFIG_KEY,

View File

@@ -342,7 +342,7 @@ function createApprovalRequestId(): string {
return crypto.randomBytes(9).toString("base64url");
}
export function normalizePluginBindingApprovalsForMigration(
export function normalizePluginBindingApprovalsSnapshot(
value: unknown,
): PluginBindingApprovalsFile {
const parsed = value as Partial<PluginBindingApprovalsFile> | undefined;
@@ -372,7 +372,7 @@ export function normalizePluginBindingApprovalsForMigration(
function loadApprovalsFromSqlite(): PluginBindingApprovalsFile {
try {
return normalizePluginBindingApprovalsForMigration(
return normalizePluginBindingApprovalsSnapshot(
readOpenClawStateKvJson(
APPROVALS_KV_SCOPE,
APPROVALS_KV_KEY,
@@ -385,7 +385,7 @@ function loadApprovalsFromSqlite(): PluginBindingApprovalsFile {
}
}
export function writePluginBindingApprovalsForMigration(file: PluginBindingApprovalsFile): void {
export function writePluginBindingApprovalsSnapshot(file: PluginBindingApprovalsFile): void {
writeOpenClawStateKvJson<OpenClawStateJsonValue>(
APPROVALS_KV_SCOPE,
APPROVALS_KV_KEY,
@@ -398,7 +398,7 @@ export function writePluginBindingApprovalsForMigration(file: PluginBindingAppro
}
async function saveApprovals(file: PluginBindingApprovalsFile): Promise<void> {
writePluginBindingApprovalsForMigration(file);
writePluginBindingApprovalsSnapshot(file);
}
function getApprovals(): PluginBindingApprovalsFile {