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
This commit is contained in:
roboomp
2026-06-14 07:13:09 +00:00
parent e29555e814
commit 31d13ec621
2 changed files with 23 additions and 10 deletions
+14 -9
View File
@@ -172,10 +172,10 @@ function projectBankSegment(projectRoot: string): string {
/**
* Discover sibling banks under `<dbDir>/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;
@@ -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 }]);