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).
Summary
transform_mode: "rust"cannot be entered on any project whosememoriesrows contain asuperseded_by_memory_idpointing at a row absent from that project's authority seed set. The module records each unresolvable pointer as a pending memory reference, andauthority_finish_preparerefuses thememoriesdomain handoff while any exist. Nothing clears them, so every retry fails identically.Every transform pass degrades:
Operator-visible symptom: empty MC sidebar plus a "reconnecting" notification —
materialized = 0on 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):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, insideseed_memory_snapshots):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):Where the pointers come from
Normal upstream operation.
merge/curatesetsuperseded_by_memory_idon 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:
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:Detection query (per project):
Note the
project_pathpredicate — 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
memoriesrows 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:
Not one counter moved. LKG parking held; every pass fell back to
served_from=rawwithapplied=false. The store rejects and writes nothing, which is correct.Fix
Host-side, in
authoritySeedRows: null asuperseded_by_memory_idwhose 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:
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
crates/byte-identical to upstream (fork features are TypeScript-only, fork-lane migrations ≥10000)cargo test --workspace1150 passed / 0 failed / 4 ignoredhealth ok,consecutive_error_count 0)probe-subc-transport, 30 samples, p50 0.509 ms, exit 0)transform_mode: "ts"restores normal operation immediatelyRelated: #375 / #376 (config-threading bug that blocked the transport before this layer was reachable).