diff --git a/packages/coding-agent/src/mnemopi/state.ts b/packages/coding-agent/src/mnemopi/state.ts index 0f3efdfd4..57d781c19 100644 --- a/packages/coding-agent/src/mnemopi/state.ts +++ b/packages/coding-agent/src/mnemopi/state.ts @@ -650,10 +650,13 @@ export class MnemopiSessionState { * episodic promotion / embedding for the LAST few turns is skipped, * and `maybeRetainOnAgentEnd` has already retained earlier turns). */ - #boundRetainBusyTimeout(timeoutMs: number): void { - // SQLite lock waits block the JS thread, so a Promise race cannot interrupt them. + #boundOwnedBusyTimeout(timeoutMs: number): void { + // SQLite lock waits block the JS thread, so a Promise race cannot interrupt + // them. consolidate() flushes every owned bank, so bound each one — not just + // the retain bank — or a locked shared bank (per-project-tagged) still stalls + // teardown for Mnemopi's default 5s busy timeout (#7351 review). const busyTimeoutMs = Math.max(1, Math.floor(timeoutMs)); - this.memory.beam.db.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`); + for (const memory of this.scoped.owned) memory.beam.db.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`); } async dispose(options: { consolidate?: boolean; timeoutMs?: number } = {}): Promise { @@ -670,7 +673,7 @@ export class MnemopiSessionState { const { timeoutMs } = options; const boundedTimeoutMs = timeoutMs !== undefined && timeoutMs > 0 ? timeoutMs : undefined; const deadline = boundedTimeoutMs !== undefined ? performance.now() + boundedTimeoutMs : undefined; - if (boundedTimeoutMs !== undefined) this.#boundRetainBusyTimeout(boundedTimeoutMs); + if (boundedTimeoutMs !== undefined) this.#boundOwnedBusyTimeout(boundedTimeoutMs); const consolidatePromise = this.consolidate({ full: false, extract: false, sleep: false }).catch( (error: unknown) => { logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) }); diff --git a/packages/coding-agent/test/memory-tools.test.ts b/packages/coding-agent/test/memory-tools.test.ts index c10ae7295..f0f39d096 100644 --- a/packages/coding-agent/test/memory-tools.test.ts +++ b/packages/coding-agent/test/memory-tools.test.ts @@ -18,6 +18,7 @@ import { HindsightSessionState } from "@oh-my-pi/pi-coding-agent/hindsight/state import { mnemopiBackend } from "@oh-my-pi/pi-coding-agent/mnemopi/backend"; import { loadMnemopiConfig, type MnemopiBackendConfig } from "@oh-my-pi/pi-coding-agent/mnemopi/config"; import { + getMnemopiScopedDbPaths, getMnemopiSessionState, loadMnemopi, loadMnemopiCore, @@ -710,14 +711,23 @@ describe("Mnemopi backend lifecycle", () => { registeredMnemopiState = undefined; }); - it("bounds synchronous SQLite lock waits during final retention (#7351)", async () => { - const config = makeMnemopiConfig({ baseBank: "test-bank" }); + it("bounds synchronous SQLite lock waits on every owned bank during final retention (#7351)", async () => { + // per-project-tagged owns a project retain bank AND the shared bank; lock the + // shared bank so a retain-only busy-timeout fix would still stall teardown. + const config = makeMnemopiConfig({ + scoping: "per-project-tagged", + bank: "project-alpha", + globalBank: "default", + }); const entries = [ { type: "message", message: { role: "user", content: "hello" } }, { type: "message", message: { role: "assistant", content: [{ type: "text", text: "done" }] } }, ]; - const state = registerMnemopiState(config, { entries: () => entries }); - const lock = new Database(config.dbPath); + const state = registerMnemopiState(config, { cwd: "/work/project-alpha", entries: () => entries }); + const ownedDbPaths = getMnemopiScopedDbPaths(config); + const sharedDbPath = ownedDbPaths.find(dbPath => dbPath === config.dbPath); + expect(sharedDbPath).toBeDefined(); + const lock = new Database(sharedDbPath!); lock.exec("BEGIN IMMEDIATE"); const started = performance.now();