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