From a0f548c0a189dd69ad6f2c90c7a35bac203b4ba5 Mon Sep 17 00:00:00 2001 From: Tehan Date: Fri, 28 Aug 2026 20:31:04 +0200 Subject: [PATCH] fix(rust): drop supersede pointers outside the authority seed set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `authoritySeedRows` seeds every memory for a project with no status filter, forwarding `superseded_by_memory_id` verbatim. When that target is absent from the seed set, the module records a pending memory reference it can never resolve: the resolution sweep in `seed_memory_snapshots` only clears pendings whose target later appears in `mc_memories`, and a hard-deleted row never will. The pending then survives to `authority_finish_prepare`, which rejects the memories-domain handoff outright: store: storage backend: authority prepare complete rejected: 2 unresolved pending memory references Every transform pass degrades to `served_from=raw` with `materialized=0`, so m[0]/m[1] are never built — the operator sees an empty sidebar and a reconnecting notification, and rust mode can never be entered. These pointers are produced by ordinary merge/curate activity: the losing row records `superseded_by_memory_id`, the winning row is later archived and eventually deleted, and the pointer outlives it. Nothing in the TS path dereferences it, so it stays inert and invisible until the module's store resolves it. Fixed host-side rather than by relaxing the Rust gate, because the host can distinguish two cases the store cannot: - target absent from the seed set -> dead link, drop it here - target present in the seed set but missing module-side -> genuine ingest failure, pending fires and the gate correctly rejects Relaxing the gate would collapse both into "ignore" and lose the protection it exists to provide. Measured on one install: 7 of 40 projects affected, 2-25 rows each. The change is surgical — on the affected project it drops exactly the 2 dead links and preserves all 61 resolvable pointers. --- .../magic-context/rust-mode-transform.test.ts | 95 +++++++++++++++++++ .../magic-context/rust-mode-transform.ts | 22 ++++- 2 files changed, 114 insertions(+), 3 deletions(-) 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,