Skip to content

rust mode: supersede pointers outside the authority seed set permanently block prepare (memories domain) #377

Description

@iceteaSA

Corrected 2026-08-28. The first version of this issue named the wrong blocking set (73 dangling pointers repo-wide) because I reasoned from module-state-sync's ["active","permanent"] filter — the wrong code path. The authority seed path has no status filter. The mechanism below is unchanged; the scoping and numbers are corrected, and the fix shape changed as a result. Original text kept in the comments for the record.

Summary

transform_mode: "rust" cannot be entered on any project whose memories rows contain a superseded_by_memory_id pointing at a row absent from that project's authority seed set. The module records each unresolvable pointer as a pending memory reference, and authority_finish_prepare refuses the memories domain handoff while any exist. Nothing clears them, so every retry fails identically.

Every transform pass degrades:

store: storage backend: authority prepare complete rejected: 2 unresolved pending memory references
→ lkg_miss → decision=error → served_from=raw → applied=false
mc_rust_park_transition failure_passes=3 pass_count=3 park_count=1

Operator-visible symptom: empty MC sidebar plus a "reconnecting" notification — materialized = 0 on every pass, so m[0]/m[1] are never built.

Mechanism

The host seeds every memory for the project, with no status filter (rust-mode-transform.ts:907, authoritySeedRows):

db.prepare("SELECT * FROM memories WHERE project_path = ? ORDER BY id ASC").all(projectPath)

The store records a pending reference when a seeded row names a supersede target it cannot resolve in mc_memories (crates/mc-store/src/lib.rs:14948-14958, inside seed_memory_snapshots):

if let (Some(target), None) =
    (canonical_target_source_row_id, superseded_by_memory_id)
{
    pending_upsert.execute(...)   // named but unresolvable → record pending
} else {
    pending_delete.execute(...)
}

A resolution sweep at the end of the same function (:15068-15080) clears pendings whose target has since been inserted — so batch-ordering is already handled, and any pending surviving to finish is genuinely unresolvable.

The gate (:14035-14047):

if domain == "memories" {
    let pending: i64 = tx.query_row(
        "SELECT COUNT(*) FROM mc_authority_pending_memory_references
          WHERE context_store_uuid = ?1 AND project = ?2 AND domain = ?3", ...)?;
    if pending > 0 {
        return Err(AuthorityTransitionError::UnresolvedPendingReferences { count: pending });
    }
}

Where the pointers come from

Normal upstream operation. merge / curate set superseded_by_memory_id on the losing row; the winning row is later archived and eventually hard-deleted; the pointer outlives its target. Nothing in the TS path dereferences it, so it is inert and invisible locally — the module's store is the first consumer that resolves it.

Our blocking pair:

src status superseded_by category
25 archived 265 CONFIG_VALUES
26 archived 265 CONFIG_VALUES

Memory 265 no longer exists. Count matches the reported error exactly (2).

Scope — measured, per project

The blocking set is per project, since the seed set is per project_path:

project (truncated)             blocking   seeded
git:37cda6db14b9a5e2ee7ce426          25      363
git:5170eb7cfa860b23998c11ae          17      302
git:84f4a4335f0514ae090fa2bf          13      448
git:c93b0a08dc394c7fd52e377d          10      148
git:4b0ea68d7af9a6031a7ffda7           4      179
git:93eea8cdeebd2eb57f476137           2      245   ← ours, matches the error
git:f78f6db52b23c81d58dae387           2      122

7 of 40 projects affected on this install

Detection query (per project):

SELECT m.project_path, COUNT(*)
FROM memories m
WHERE m.superseded_by_memory_id IS NOT NULL
  AND NOT EXISTS (
    SELECT 1 FROM memories t
     WHERE t.id = m.superseded_by_memory_id
       AND t.project_path = m.project_path)
GROUP BY m.project_path;

Note the project_path predicate — a target in a different project is equally unresolvable, since the seed set is project-scoped. (On this install that count happens to be 0, but the general case needs it.)

Why it is structural, not transient

No repair path exists: nothing rewrites supersede pointers, and the targets are permanently gone. The resolution sweep can only clear pendings whose target appears in mc_memories, which will never happen. Confirmed across two attempts.

Not fork-specific — these are plain memories rows written by upstream's own merge/curate flow. A fresh install has none, which is likely why it hasn't surfaced; any long-lived install with an active dreamer accumulates them.

The failure is clean — no data loss

Full before/after fingerprint after a failed prepare:

schema_sha         7784d878b140f4f5…  ==  7784d878b140f4f5…
memories           3101 == 3101
compartments       3191 == 3191
session_meta       5004 == 5004
notes               264 == 264
compartment_events  911 == 911
quick_check          ok == ok

Not one counter moved. LKG parking held; every pass fell back to served_from=raw with applied=false. The store rejects and writes nothing, which is correct.

Fix

Host-side, in authoritySeedRows: null a superseded_by_memory_id whose target is not in the seed set being sent.

This is better than relaxing the Rust gate, because the host can distinguish two cases the store cannot:

  • target absent from the seed set → dead link, can never resolve → drop it here
  • target present in the seed set but missing module-side → genuine ingest failure → pending fires, gate correctly rejects

Relaxing the gate would collapse both into "ignore", losing the protection it exists to provide. PR follows.

Alternative if you'd rather keep the seed verbatim: a host-side migration nulling dangling pointers, leaving the gate strict. Happy to do that shape instead.

Environment

  • fork on upstream master; crates/ byte-identical to upstream (fork features are TypeScript-only, fork-lane migrations ≥10000)
  • module built from the same tree, cargo test --workspace 1150 passed / 0 failed / 4 ignored
  • subc daemon 0.10.0; module registered and healthy throughout (health ok, consecutive_error_count 0)
  • transport connectivity verified independently first (probe-subc-transport, 30 samples, p50 0.509 ms, exit 0)
  • reverting to transform_mode: "ts" restores normal operation immediately

Related: #375 / #376 (config-threading bug that blocked the transport before this layer was reachable).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions