fix(mnemopi): bound busy timeout on every owned bank

Extended the shutdown busy-timeout clamp from the retain bank to all owned banks so a locked shared bank (per-project-tagged) cannot stall teardown for the default 5s SQLite timeout.

Fixes #7351
This commit is contained in:
roboomp
2026-08-02 05:34:58 +00:00
parent 82095f0c12
commit b9bdbcec03
2 changed files with 21 additions and 8 deletions
+7 -4
View File
@@ -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<void> {
@@ -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) });
@@ -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();