From 31d13ec6212e6f37cd006bcde79de78d266c4a63 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 14 Jun 2026 07:13:09 +0000 Subject: [PATCH] fix(coding-agent): skipped mixed-cwd legacy mnemopi banks Only recall legacy mnemopi banks when every working_memory row is scoped to the active cwd, so per-project recall cannot surface sibling project rows from an ancestor-derived bank. Fixes #2412 --- packages/coding-agent/src/mnemopi/config.ts | 23 +++++++++++-------- .../test/mnemopi-bank-derivation.test.ts | 10 +++++++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/coding-agent/src/mnemopi/config.ts b/packages/coding-agent/src/mnemopi/config.ts index 7c6b51647..a137b994e 100644 --- a/packages/coding-agent/src/mnemopi/config.ts +++ b/packages/coding-agent/src/mnemopi/config.ts @@ -172,10 +172,10 @@ function projectBankSegment(projectRoot: string): string { /** * Discover sibling banks under `/banks/` whose `working_memory` rows - * already carry the active `cwd` in `metadata_json.$.cwd`, and add them to - * the recall set. This rescues memories stranded by a previous, less-stable - * bank derivation (#2412) without changing the write target — only recall is - * widened. + * all carry the active `cwd` in `metadata_json.$.cwd`, and add those safe + * single-cwd banks to the recall set. This rescues memories stranded by a + * previous, less-stable bank derivation (#2412) without recalling mixed-cwd + * legacy banks wholesale under per-project isolation. * * Robust by design: a missing banks directory, unreadable bank dir, or * corrupt SQLite file is silently skipped. Scanning is capped at @@ -202,19 +202,24 @@ export function extendRecallWithLegacyBanks( if (scanned >= LEGACY_BANK_SCAN_LIMIT) break; scanned++; const candidate = path.join(banksDir, entry.name, "mnemopi.db"); - if (bankHasCwd(candidate, cwdAbs)) extras.push(entry.name); + if (bankOnlyHasCwd(candidate, cwdAbs)) extras.push(entry.name); } return extras.length === 0 ? resolved : [...resolved, ...extras]; } -function bankHasCwd(dbPath: string, cwd: string): boolean { +function bankOnlyHasCwd(dbPath: string, cwd: string): boolean { let db: Database | undefined; try { db = new Database(dbPath, { readonly: true }); const row = db - .query("SELECT 1 FROM working_memory WHERE json_extract(metadata_json, '$.cwd') = ? LIMIT 1") - .get(cwd); - return row !== null; + .prepare<{ matching: number; unsafe: number }, [string, string]>(` + SELECT + SUM(CASE WHEN json_extract(metadata_json, '$.cwd') = ? THEN 1 ELSE 0 END) AS matching, + SUM(CASE WHEN json_extract(metadata_json, '$.cwd') IS NULL OR json_extract(metadata_json, '$.cwd') <> ? THEN 1 ELSE 0 END) AS unsafe + FROM working_memory + `) + .get(cwd, cwd); + return (row?.matching ?? 0) > 0 && (row?.unsafe ?? 0) === 0; } catch (error) { logger.debug("Mnemopi: legacy bank probe failed", { dbPath, error: String(error) }); return false; diff --git a/packages/coding-agent/test/mnemopi-bank-derivation.test.ts b/packages/coding-agent/test/mnemopi-bank-derivation.test.ts index 37695be5d..8bf16fcd1 100644 --- a/packages/coding-agent/test/mnemopi-bank-derivation.test.ts +++ b/packages/coding-agent/test/mnemopi-bank-derivation.test.ts @@ -100,7 +100,7 @@ describe("computeMnemopiBankScope (#2412)", () => { }); describe("extendRecallWithLegacyBanks (#2412)", () => { - it("adds a sibling bank when working_memory rows tag the active cwd", () => { + it("adds a sibling bank only when all working_memory rows tag the active cwd", () => { const activeCwd = "/home/user/projects/myrepo"; createBankFixture("legacy-A", [{ session_id: "old", cwd: activeCwd }]); createBankFixture("unrelated-B", [{ session_id: "other", cwd: "/some/other/place" }]); @@ -110,6 +110,14 @@ describe("extendRecallWithLegacyBanks (#2412)", () => { expect(extended).not.toContain("unrelated-B"); }); + it("skips mixed-cwd legacy banks because recall cannot filter rows by cwd", () => { + const activeCwd = "/home/user/projects/safe-child"; + createBankFixture("mixed-cwd-legacy", [{ cwd: activeCwd }, { cwd: "/home/user/projects/sibling-child" }]); + const extended = extendRecallWithLegacyBanks(["active-bank"], mainDbPath, activeCwd); + expect(extended).toContain("active-bank"); + expect(extended).not.toContain("mixed-cwd-legacy"); + }); + it("ignores banks already in the recall set", () => { const cwd = "/home/user/projects/already-in-set"; createBankFixture("already-in-set", [{ cwd }]);