diff --git a/packages/plugin/src/hooks/magic-context/rust-mode-transform.test.ts b/packages/plugin/src/hooks/magic-context/rust-mode-transform.test.ts index 912f25514c..2c46381871 100644 --- a/packages/plugin/src/hooks/magic-context/rust-mode-transform.test.ts +++ b/packages/plugin/src/hooks/magic-context/rust-mode-transform.test.ts @@ -4220,3 +4220,98 @@ describe("raw fallback refusal copy and early abort", () => { expect(parkedMessages).toEqual([ENGINE_RECONNECTING_USER_MESSAGE]); }); }); + +describe("authoritySeedRows — supersede pointer resolution (issue #377)", () => { + // The store records a pending memory reference for any seeded row whose + // superseded_by_memory_id it cannot resolve, and authority_finish_prepare + // rejects the memories-domain handoff while any pending references exist. + // A target outside the seed set can never resolve, so the pending survives + // the resolution sweep and blocks rust mode permanently. + function seedDb(): ContextDatabase { + const db = new Database(":memory:") as ContextDatabase; + initializeDatabase(db); + return db; + } + + function insert( + db: ContextDatabase, + project: string, + content: string, + status: string, + ): number { + const now = Date.now(); + db.prepare( + `INSERT INTO memories + (project_path, category, content, normalized_hash, source_type, + seen_count, retrieval_count, first_seen_at, created_at, updated_at, + last_seen_at, status) + VALUES (?, 'ARCHITECTURE', ?, ?, 'agent', 1, 0, ?, ?, ?, ?, ?)`, + ).run(project, content, `hash-${content}`, now, now, now, now, status); + return Number( + (db.prepare("SELECT last_insert_rowid() AS id").get() as { id: number }).id, + ); + } + + it("drops a supersede pointer whose target is absent from the seed set", () => { + const db = seedDb(); + try { + const project = "git:seed-test"; + const survivor = insert(db, project, "survivor", "active"); + const superseded = insert(db, project, "superseded", "archived"); + const orphaned = insert(db, project, "orphaned", "archived"); + + // Resolvable: target is in the same seed set. + db.prepare("UPDATE memories SET superseded_by_memory_id = ? WHERE id = ?").run( + survivor, + superseded, + ); + // Unresolvable: target id never existed in this project. + db.prepare("UPDATE memories SET superseded_by_memory_id = ? WHERE id = ?").run( + 999_999, + orphaned, + ); + + const rows = __rustModeTransformTest.authoritySeedRows(db, project, "memories"); + const byId = new Map( + rows.map((row) => [ + Number((row as { source_row_id: unknown }).source_row_id), + (row as { snapshot: Record }).snapshot, + ]), + ); + + // The dead link is dropped so the module never records a pending reference. + expect(byId.get(orphaned)?.superseded_by_memory_id).toBeNull(); + // The resolvable pointer is preserved verbatim — this must stay surgical, + // not a blanket null of the column. + expect(byId.get(superseded)?.superseded_by_memory_id).toBe(survivor); + } finally { + closeQuietly(db); + } + }); + + it("drops a supersede pointer whose target belongs to another project", () => { + const db = seedDb(); + try { + const project = "git:seed-a"; + const foreign = insert(db, "git:seed-b", "foreign-target", "active"); + const local = insert(db, project, "local", "archived"); + db.prepare("UPDATE memories SET superseded_by_memory_id = ? WHERE id = ?").run( + foreign, + local, + ); + + const rows = __rustModeTransformTest.authoritySeedRows(db, project, "memories"); + const snapshot = ( + rows.find( + (row) => Number((row as { source_row_id: unknown }).source_row_id) === local, + ) as { snapshot: Record } + ).snapshot; + + // The seed set is project-scoped, so a cross-project target is + // equally unresolvable module-side. + expect(snapshot.superseded_by_memory_id).toBeNull(); + } finally { + closeQuietly(db); + } + }); +}); diff --git a/packages/plugin/src/hooks/magic-context/rust-mode-transform.ts b/packages/plugin/src/hooks/magic-context/rust-mode-transform.ts index 6521234525..3bde4847b0 100644 --- a/packages/plugin/src/hooks/magic-context/rust-mode-transform.ts +++ b/packages/plugin/src/hooks/magic-context/rust-mode-transform.ts @@ -927,6 +927,15 @@ function authoritySeedRows( ) .all(projectPath, projectPath); const memoryRows = snapshots.filter(isRecord); + // A `superseded_by_memory_id` pointing outside this seed set can never resolve + // module-side: the store records it as a pending memory reference, and the + // resolution sweep only clears pendings whose target later appears in + // mc_memories. A target that is absent here is absent for good (its row was + // hard-deleted after an archive), so the pending would survive to + // authority_finish_prepare and permanently reject the memories-domain handoff. + // Dropping the dead link here keeps the gate meaningful for the case it exists + // to catch: a target the host DID send that the module failed to ingest. + const seededIds = new Set(memoryRows.map((row) => Number(row.id))); const mappings = domain === "memories" ? getMemoryVerifications( @@ -937,16 +946,22 @@ function authoritySeedRows( return memoryRows.map((snapshot) => { const id = Number(snapshot.id); const mapping = mappings.get(id); + const resolvedSnapshot = + domain === "memories" && + snapshot.superseded_by_memory_id != null && + !seededIds.has(Number(snapshot.superseded_by_memory_id)) + ? { ...snapshot, superseded_by_memory_id: null } + : snapshot; const seededSnapshot = domain === "memories" && mapping ? { - ...snapshot, + ...resolvedSnapshot, mapping: mapping.hasSentinel ? null : mapping.files, mapping_origin: mapping.mappingOrigin, } : domain === "notes" && snapshot.project_path == null - ? { ...snapshot, project_path: projectPath } - : snapshot; + ? { ...resolvedSnapshot, project_path: projectPath } + : resolvedSnapshot; return { source_row_id: snapshot.id, snapshot: seededSnapshot }; }); } @@ -3006,6 +3021,7 @@ export async function runRustModeTransform( export const __rustModeTransformTest = { applyNativeMessagesVerbatim, + authoritySeedRows, contentSnapshotsFor, snapshotTags: { array: LKG_SNAPSHOT_ARRAY,