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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user