Skip to content

Authority drain rolls memory importance/source_type back to stale mirror snapshots — same null-then-restore class as #379, and it fires even when zero transforms succeed #391

Description

@iceteaSA

Summary

Enabling rust mode and reverting caused 46 memories to have importance silently rolled back to older values, and 4 archived memories to return to active. No transform ever succeeded in that window — every pass failed in state_sync (that part is #390). The drift came purely from activation + drain.

This is the same shape as #379 (mirror projection writes NULL as a transient placeholder, something else is supposed to restore it) for a different field. #379 fixed supersede pointers by translating pending references on drain. importance/source_type instead restore from mirror_live_memory_rows.full_row_snapshot, and that snapshot can be stale.

Evidence

Fingerprints before the flip and after the revert, against a pre-flip .backup:

                 PRE-FLIP     POST-REVERT
memories count   3185     →   3185          unchanged
supersede ptrs   704      →   704           unchanged
supersede HASH   836b146c →   836b146c      identical  ← #379 holding
memory content   9ccdc18a →   9ccdc18a      identical
classification   cb148fb7 →   63813205      CHANGED
memories active  1803     →   1807          +4

Diff against the backup: importance differs on 46 rows, status on 4, content on 0, seen_count on 0.

The new value equals the mirror snapshot in every case:

868: backup=72  live=70  snapshot=70
875: backup=56  live=60  snapshot=60
878: backup=68  live=75  snapshot=75
880: backup=65  live=70  snapshot=70
881: backup=55  live=62  snapshot=62

via

JOIN mirror_identity identity
  ON identity.domain='memories' AND identity.context_row_id = m.id
JOIN mirror_live_memory_rows live
  ON live.module_project = identity.module_project
 AND live.module_row_id  = identity.module_row_id

classified_at on those rows is still 2026-08-28 04:12 — nothing re-classified them.

Ruled out

  • Dreamer. Zero dream_runs rows in the window. Last classify-memories ran 4h earlier (classified=45 changed=33), before the pre-flip backup was taken.
  • Module store writing directly. mc_memories holds 245 rows with ids 981–1225 and an mtime predating the flip. The changed host ids (868, 875, …) map to it only through mirror_identity, which is exactly the repair join.
  • Successful transforms. There were none — applied=false, served_from=raw, module=0.0 ms on every pass.

Mechanism

repairNullClobberedMemoryRows (context-authority.ts:1670) selects:

WHERE (memory.source_type IS NULL OR memory.importance IS NULL)
  AND live.full_row_snapshot IS NOT NULL

and restores both fields from the snapshot. The observed rows were non-null before and after (72 → 70), so the only consistent sequence is:

  1. mirror projection writes importance = NULL as a transient placeholder (the function's own comment describes this: "sparse mapping records overwrote source_type and importance with null before the mirror retained full snapshots");
  2. the repair restores from full_row_snapshot;
  3. the snapshot predates recent host-side classification, so the restore is a rollback.

The repair is doing exactly what it says. The problem is that the snapshot is treated as authoritative for a field the host has since updated, and there is no recency comparison — no classified_at/updated_at check between snapshot and row.

status (4 rows, archived → active) is not touched by this function, so the mirror apply path appears to have a second instance of the same class. I did not isolate that one.

Why it matters more than the numbers suggest

  • It fires on a failed activation. Nothing in the logs suggests memory was touched; the visible story is "rust mode didn't work, reverted cleanly."
  • It is invisible to timestamp-keyed checksclassified_at is untouched, and the repair sets updated_at = Date.now(), so a "what changed recently" query shows a bump with no indication that the content of the change was a rollback.
  • Counts are stable (3185 → 3185, 704 → 704). Only a content hash over scope|shareable|importance catches it. That is exactly why rust mode: superseded_by_memory_id silently lost when authority is dropped with pending mirror references outstanding #379 needed a paired count+hash fingerprint to detect, and the same instrument caught this.

Suggested direction

Either make the null-placeholder unobservable (write the projection in a transaction that restores before commit, so a partially-drained store never persists NULL), or make the restore recency-aware (compare snapshot vintage against the row's classified_at/updated_at and prefer the newer). The first is stronger — it removes the window rather than arbitrating it.

More generally: this is the second field found in the null-then-restore pattern. A sweep for other columns the mirror projection nulls transiently would be worth more than fixing importance alone.

Environment

Upstream master with #376/#378/#379/#381 present; ck-mc rebuilt at matching generation. Store at schema v82, 3185 memories, 704 supersede pointers. Pre-flip .backup retained; drift is reversible from it.

#379 itself held perfectly — 704 pointers and an identical hash across the same drain that produced this. This is the neighbouring field, not a regression of that fix.

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