---
name: two-memory-roots-diverged
description: "Two nearly-disjoint spt-core memory stores exist (.claude\\projects\\ and .ccs\\instances\\bigscreen\\projects\\); which one loads is set by the session root, so a memory can read stale when the writes simply went to the other mind"
metadata: 
  node_type: memory
  type: project
  originSessionId: 88df48e9-d18d-42cc-b535-aa2cd6cce078
  modified: 2026-08-04T02:44:42.616Z
---

There are **two spt-core memory stores on this box**, and which one a session wakes up with is
decided by the harness root, not by me:

- **Store A** — `C:\Users\decid\.claude\projects\C--Users-decid-Documents-projects-spt-core\memory\`
  153 files. The one written for several days up to 2026-08-03 00:21 (operator-confirmed).
- **Store B** — `C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\`
  329 files. Loaded by the CCS `bigscreen` instance; loaded by the 2026-08-03 02:00 session.

⭐⭐ **They are nearly disjoint — 5 shared filenames out of 153 + 329.** Not two copies that drifted:
two separate minds. 148 of A's files (all release notes v0.47–v0.52 with counters, the infra-register
ruling, the IR-18 lane assignment, ~140 craft rules) exist nowhere in B.

**Why: for several days the work ran under the `.claude\projects\` root** (store A) while B — the
long-lived store, rich up to ~2026-07-30 — sat untouched. Nothing was lost; it was filed in the
other mind.

**Why this matters:** every "latest = X" / "current state" memory carries an unstated assumption
that it is the ONLY record. When the root forks, an index line reads exactly as stale as one that
was merely neglected. I diagnosed the v0.46.0-vs-v0.52.0 gap as silent decay and was **wrong** —
see [[v0121-published]] for the corrected mechanism.

**How to apply:**
1. **When a memory reads behind reality, check for a second store BEFORE concluding neglect.**
   `find /c/Users/decid -maxdepth 6 -name MEMORY.md` — one command, discriminates the twins.
   A symptom shared by two causes cannot pick between them ([[a-predicate-without-its-tool-is-not-evidence]]).
2. **Never assume the loaded index is complete.** If a topic feels thinner than the work done on it,
   the depth may be in the other root — go read it rather than re-deriving from scratch.
3. **Cross-file the durable rules.** A lesson worth keeping belongs in whichever store loads next,
   which is unknowable at write time — so write it to both.
## ✅ UNION-MERGED 2026-08-03 (operator-directed)

Both roots now hold an **identical 478-file set and one shared index** (`diff -rq` clean). Method,
so a future session can audit it:

- Backup of both stores taken first, to the session scratchpad (`mem-backup-20260803-023514/`).
- Missing files copied **both ways with `cp -n`** — never overwriting, so no version was clobbered.
- The **5 same-name conflicts** (`MEMORY.md`, `alarm-every-test-run`, `v0250/v0310/v0460-published`)
  were read as pairs and merged by hand. Every one turned out **complementary, not contradictory** —
  different sessions recording different facets. The other root's text is appended **verbatim**
  under a marked heading rather than summarized, because a summarizing pass is exactly where the
  detail that made the second copy worth keeping disappears.
- Indexes merged the same way: both taxonomies kept whole, not interleaved.

⚠ **Not done: dedupe/compaction.** The merged index is ~37KB and certainly restates some rules
under two names. Left deliberately — collapsing two vocabularies is a judgement pass, not a
mechanical one. **Do not read a missing cross-reference as a missing memory until you have checked
both halves of the index.**

⚠ **Two apparent conflicts resolved, one still open.** The v0.25.0 / v0.31.0 date disagreements
between the halves were the **local-vs-UTC artifact** (box is PDT = UTC-7, so an evening cut carries
the next day's UTC date) — same class as the `ls -l`-is-local trap in
[[commune-drops-orphaned-box-wide]]. Still unresolved: **v0.48.0's publish time**, `gh` says
`12:35:31Z`, the release note says `10:16:13Z` — both UTC, 2h19m apart, not a timezone artifact.

**Merging does not stop the forking.** Which root a session loads is still set by the harness, so
the two will drift apart again the moment work runs under the other one. Re-check with
`diff -rq <A> <B>` when a memory reads stale.

## ✅ COMPACTED 2026-08-03 — the union merge left a SECOND, different defect

The union merge fixed disjointness and created a size defect: the merged index was **37577 bytes
against a 24.4KB read limit**, so every session that loaded it had its TAIL SILENTLY DROPPED. Not a
tidiness problem — the entries at the end of the file were invisible to every reader, with no error.

**Measured after each pass** (the measurement is the point; a pass that "looks smaller" is not a
verdict):

| pass | bytes | KB | verdict |
|---|---|---|---|
| 0 | 37577 | 36.7 | over the limit — tail dropped |
| 1 | 26200 | 25.6 | **STILL OVER.** A 30% cut that leaves it broken is worth zero |
| 2 | 19712 | 19.2 | under the limit — truncation defect fixed |
| 3 | 19343 | 18.9 | end of the compaction pass |
| 4 | 20721 | 20.2 | after restoring 11 hazard pointers (below) |

**The advisory target of 17.1KB was NOT reached, and that is reported rather than rounded to.** The
floor is structural: **295 pointers × ~55 bytes of pure markdown-link syntax ≈ 15.5KB before a
single word of hook.** Getting materially below that means dropping pointers, not prose. An
arithmetic floor is a measurement a reader can act on; "hit the target" would have told nobody
anything and would have been worse if false.

### ⭐⭐ THE LAST STALE COPY IS EVIDENCE UNTIL THE DIFF IS EXTRACTED

Compaction ran on B. A still held the **pre-compaction** index — and after B had been overwritten
twice, `refs(A) - refs(B)` was the **only** way left to compute which pointers had been dropped.
**The mirror destroys A.** Mirroring at the moment it was authorised, rather than after extracting
the diff, would have turned a diff into a recollection: "41 dropped" as a memory instead of a
computed list.

Generalise past memory roots: **a stale, superseded, or about-to-be-overwritten copy is an
INSTRUMENT until the measurement that needs it has been taken.** Same shape as keeping
`.worktrees/ir18` alive until its acceptance verdict lands, or keeping a red build tree until the
delta test that reads it has run. Sequence the destruction after the extraction, and re-check the
licence at the moment it is SPENT, not the moment it was granted (`in-A-not-in-B` was re-run
immediately before the copy — still 0 — and A's hash was confirmed unchanged from session start,
proving the only delta being overwritten was mine).

### ⭐⭐ THE DROP TEST IS MECHANISM-CAN-RECUR, NOT WHEN-IT-HAPPENED

The first pass dropped 41 index lines as "closed history". doyle re-read them and refused the
classification for a subset: **an entry describing a recurrable MECHANISM is a hazard wearing a
date.** A milestone cannot happen again; a Windows UAC elevation path, a PTY program-resolution
rule, a `TimedOut` that poisons a client, an inline write on a drain thread — all of those are live
in shipping code, and the write-up is often the only place the diagnosis lives.

**11 pointers restored** on that test (real-harness/mock-gating escapes, Windows spawn defects,
pump/update-wedge mechanisms). **30 stayed dropped** as genuine events — shipped milestones,
superseded release notes. Two of doyle's own seven candidates FAILED the test on re-read and were
left out with the evidence quoted, because the test is applied, not agreed with.

**The header's "grep this dir" is a mitigation, NOT a substitute:** nobody greps for a defect they
do not know exists, which is exactly the population a hazard write-up serves.

### ⚠ THE INDEX IS NOT THE POPULATION — 183 files still have no pointer

Counted from disk, not recalled: **478 topic files, 295 index pointers, 183 files with no index
line.** Only 41 of those came from this compaction; **~150 were orphaned by EARLIER passes and have
never been mechanism-tested at all.** Their names read like live mechanisms
(`win-netstream-redrive-flake`, `presence-dead-shows-online`, `stale-broker-attach-wedge`,
`clap-doc-comment-leaks-req-tag`, `commune-drop-dir-is-per-endpoint`, …). Treat the orphan set as an
unaudited backlog, not as history.

### ⛔ THE COMPACTION NAG IS WRONG — DO NOT COMPLY (doyle ruling, 2026-08-03)

A `PostToolUse` hook fires on **every** edit to `MEMORY.md`: *"approaching the 24.4KB read limit.
Compact it to under 17.1KB now: keep one line per entry, move detail into topic files, and merge or
drop stale entries."* You will meet it the moment you touch the index. **Do not obey it, and say out
loud that you did not.**

- **17.1KB is unreachable, not merely hard.** ~295 pointers × ~55 bytes of pure markdown-link syntax
  is a **~15.5KB floor before one word of hook**. The index is ALREADY one line per entry with the
  detail in topic files — both of the nag's first two levers are already pulled.
- **The read limit is the real constraint, and it is not being hit.** 21333 bytes against 24.4KB.
  Nothing is truncating. The functional defect (pass 0's silently-dropped tail) is FIXED; the nag is
  calibrated to an advisory target that was never reachable.
- **So the only remaining lever is "drop stale entries"** — i.e. re-running the exact
  mechanism-can-recur misclassification that cost a morning to undo (see the section above).
  Compliance re-breaks the file.

⭐⭐ **AN AUTOMATED NAG IS A CLAIM, AND A CLAIM CAN BE WRONG.** It fires with no knowledge of why the
file is the size it is. **The failure mode of obeying it quietly is invisible**: you ship a smaller
index and a worse one, and nobody ever gets a red to look at. Treat a hook's instruction the way you
treat any other unsourced assertion — check its premise before you act on it, and report the refusal
with the arithmetic rather than just declining. Same family as
[[report-measurement-never-issue-direction]]: agreeing with an instrument IS direction.

### ⚠ RULING PREMISE DECAYED AND WAS RE-DERIVED 2026-08-03 (doyle, same day)

The ⛔ above rests on "the read limit is not being hit — 21333 bytes against 24.4KB". Sessions kept
appending; measured 25433 bytes later the same day — **over the 24986-byte limit, tail silently
dropping again** (the invisible line was the Active-work 🏁 row). The nag stayed refused — 17.1KB is
still unreachable and no pointer was dropped — but the FOURTH lever the ruling never needed at
21.3KB was used: **section split to a sub-index**, the file's own HAZARD-INDEX precedent. "Boxes /
CI / infra" (3257B, 10 lines) moved VERBATIM to `CI-INFRA-INDEX.md`; one pointer line remains. Result
22392 bytes, ~2.5KB margin. Discipline going forward: the margin, not the nag, is the constraint —
measure `wc -c` after any index append, and when above ~24KB split another consult-class section
(Release ledger is the next candidate) rather than dropping or summarizing entries. The ⛔'s
arithmetic refusal of 17.1KB stands unchanged.

**Stays a MECHANISM, not a headcount.** Today's file counts and today's equality are one-time facts
about 2026-08-03; the durable claim is that *nothing keeps the roots equal* and *nothing keeps the
index complete*, because writes land in whichever root the SESSION root selects and compaction
drops pointers silently. Nobody has established which sessions resolve where. Re-derive the numbers
with `diff -rq` and an orphan count before relying on any of them.

## ✅ THIRD ROOT — ALT-PROFILE MIGRATION 2026-08-23 (operator-directed)

Operator moved doyle's sessions to the **claude-spt:alt adapter profile**, whose memory root is
`C:\Users\decid\.claude-spt\accounts\alt\projects\C--Users-decid-Documents-projects-spt-core\memory\`.
Both old stores were union-merged INTO that root at bring-up (this copy is the merged one):

- Fork state at merge: A(.claude) 487 files, B(.ccs bigscreen) 642, **155 only in B, 2 only in A,
  35 shared-but-differing** — the 2026-08-03 merge had NOT held, as this file predicted.
- Method (same as 2026-08-03): backup of both stores FIRST →
  `~\.claude-spt\accounts\alt\backups\mem-migration-20260823\{claude-root,ccs-root}\`.
  Base = B (the richer, sub-index-split structure). A-only files added with `cp -n`. For each of
  the 33 differing topic files: newer-mtime copy taken, and the loser's unique non-frontmatter
  lines appended VERBATIM under a marked `[root-merge 2026-08-23]` heading (7 files carried real
  deltas; the rest differed only in `modified:` stamps). Indexes hand-merged: HAZARD-INDEX = B
  (superset), MEMORY.md = B + the A-only tag-send pointer, RELEASE-INDEX ledger advanced to
  v0.60.0 c95 @`517c9f6f` (A had it, B stopped at v0.59.0).
- ⚠ **A's monolithic MEMORY.md rev (23116B, 2026-08-23 05:33) was NOT line-audited** against B's
  sub-indexes — A kept the pre-split layout, so most of its 46 "unique" index lines are pre-split
  duplicates, but post-fork EDITS to those lines (e.g. gh-run "LIST REJECTS jobs") may exist only
  in the backup copy. Dedupe/audit pass remains open; the backup is the instrument.
- **Old roots left INTACT** — other agents / non-alt sessions on this box may still load them.
  Which root a session loads is still set by the harness. There are now THREE candidate roots;
  when a memory reads stale, `find /c/Users/decid -maxdepth 7 -name MEMORY.md` and diff before
  concluding neglect.

## ✅ FOURTH FINDING — alt AND shared ARE ONE STORE BY HARDLINK (deployah, 2026-09-09, measured)

`accounts\alt\projects\…\memory\` and `shared\projects\…\memory\` are **not two copies that happen
to agree — they are the same files, hardlinked.** Measured, not inferred:

- Both paths are REAL directories (`Get-Item -Force`: `attrs=Directory`, no ReparsePoint, empty
  `Target`) — so the junction test this file's earlier sections teach you to run says "two dirs"
  and is MISLEADING here. The directories are separate; the FILES inside are shared.
- `fsutil file queryfileid` returns the **same NTFS File ID** on both sides, and
  `fsutil hardlink list` on the alt path names the shared path. 5/5 sampled files identical
  (`MEMORY.md`, `GATE-TEST-INDEX.md`, and three topic files); 805 files on each side.
  ⚠ **That is a SAMPLE, not a census** — 5 of 805. Do not upgrade it to "the whole store" without
  running the fileid comparison across the set.

**So a write to either path lands in both, and the fork risk this file was created for does not
apply to THIS pair.** (It still applies to the older `.claude\projects\` and `.ccs\instances\…`
roots, which are genuinely separate stores — there are now FOUR candidate paths but only THREE
distinct stores.)

⚠ **THE HARDLINK IS THE FRAGILE PART, AND NOTHING WARNS YOU WHEN IT BREAKS.** A hardlink survives
in-place edits (append, seek-and-write). It does **NOT** survive **write-by-replace** — write a
temp file, then rename/move it over the target — which is how a great many editors, formatters and
"safe write" helpers save. The moment any tool saves one of these files that way, that filename
silently becomes an INDEPENDENT copy: both paths still exist, both still look right, and the two
minds start drifting again with no error and no diff until someone thinks to look.

**How to apply:**
- Prefer append/in-place edits to these files. After any tool-driven rewrite of `MEMORY.md` or a
  sub-index — especially a scripted move/split pass, which reconstructs the whole file — **re-check
  the fileid pair** and re-link if it split.
- Equal content is NOT evidence of a live link (two fresh copies are equal on day zero, and that is
  exactly the state a just-severed link is in). **Compare the fileid, not the hash.**
- The durable claim of this whole file is unchanged and is reinforced, not retired: *nothing keeps
  the roots equal.* Here they are kept equal by an OS-level detail that any ordinary save can
  dissolve. Re-derive before relying on it.
