From 1b78a8015a77af687d0500aabe75a0d0e3cafd87 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 08:13:33 +0100 Subject: [PATCH] fix: keep state database handles isolated --- src/state/openclaw-state-db.test.ts | 21 +++++++++++++++++ src/state/openclaw-state-db.ts | 36 +++++++++++++++-------------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/state/openclaw-state-db.test.ts b/src/state/openclaw-state-db.test.ts index 75fd5480733..87baebc2048 100644 --- a/src/state/openclaw-state-db.test.ts +++ b/src/state/openclaw-state-db.test.ts @@ -110,6 +110,27 @@ describe("openclaw state database", () => { expect(fs.existsSync(databasePath)).toBe(true); }); + it("keeps cached handles open when another state path is opened", () => { + const firstPath = path.join( + createTempStateDir(), + "state", + `first-${process.pid}-${Date.now()}.sqlite`, + ); + const secondPath = path.join( + createTempStateDir(), + "state", + `second-${process.pid}-${Date.now()}.sqlite`, + ); + + const first = openOpenClawStateDatabase({ path: firstPath }); + const second = openOpenClawStateDatabase({ path: secondPath }); + + expect(first.db.isOpen).toBe(true); + expect(second.db.isOpen).toBe(true); + expect(openOpenClawStateDatabase({ path: firstPath })).toBe(first); + expect(readSqliteNumberPragma(first.db, "user_version")).toBe(1); + }); + it("uses savepoints for nested write transaction rollback", () => { const stateDir = createTempStateDir(); const options = { env: { OPENCLAW_STATE_DIR: stateDir } }; diff --git a/src/state/openclaw-state-db.ts b/src/state/openclaw-state-db.ts index d31074f23ea..7415eb789ad 100644 --- a/src/state/openclaw-state-db.ts +++ b/src/state/openclaw-state-db.ts @@ -68,7 +68,7 @@ export type RecordOpenClawStateBackupRunOptions = OpenClawStateDatabaseOptions & manifest: Record; }; -let cachedDatabase: OpenClawStateDatabase | null = null; +const cachedDatabases = new Map(); type OpenClawStateMetadataDatabase = Pick< OpenClawStateKyselyDatabase, @@ -150,14 +150,14 @@ export function openOpenClawStateDatabase( ): OpenClawStateDatabase { const env = options.env ?? process.env; const pathname = resolveDatabasePath(options); - if (cachedDatabase && cachedDatabase.path === pathname) { - return cachedDatabase; + const cached = cachedDatabases.get(pathname); + if (cached?.db.isOpen) { + return cached; } - if (cachedDatabase) { - cachedDatabase.walMaintenance.close(); - clearNodeSqliteKyselyCacheForDatabase(cachedDatabase.db); - cachedDatabase.db.close(); - cachedDatabase = null; + if (cached) { + cached.walMaintenance.close(); + clearNodeSqliteKyselyCacheForDatabase(cached.db); + cachedDatabases.delete(pathname); } ensureOpenClawStatePermissions(pathname, env); @@ -178,8 +178,9 @@ export function openOpenClawStateDatabase( throw err; } ensureOpenClawStatePermissions(pathname, env); - cachedDatabase = { db, path: pathname, walMaintenance }; - return cachedDatabase; + const database = { db, path: pathname, walMaintenance }; + cachedDatabases.set(pathname, database); + return database; } export function runOpenClawStateWriteTransaction( @@ -273,17 +274,18 @@ export function recordOpenClawStateBackupRun(options: RecordOpenClawStateBackupR } export function closeOpenClawStateDatabase(): void { - if (!cachedDatabase) { - return; + for (const database of cachedDatabases.values()) { + database.walMaintenance.close(); + clearNodeSqliteKyselyCacheForDatabase(database.db); + if (database.db.isOpen) { + database.db.close(); + } } - cachedDatabase.walMaintenance.close(); - clearNodeSqliteKyselyCacheForDatabase(cachedDatabase.db); - cachedDatabase.db.close(); - cachedDatabase = null; + cachedDatabases.clear(); } export function isOpenClawStateDatabaseOpen(): boolean { - return cachedDatabase?.db.isOpen === true; + return Array.from(cachedDatabases.values()).some((database) => database.db.isOpen); } export const closeOpenClawStateDatabaseForTest = closeOpenClawStateDatabase;