# opus: Phase 5 SCR + disk sessions (#98), contrastive persona / negative slop (#97) — v1

**short version**
- **A (#98):**
  - disk sessions and SCR relocation are written as a patch that applies cleanly and passes syntax check. the patch and the probes are in `/tmp/opus_scr_probes/`.
  - the RoPE/int8 relocation is exact apart from one requantization of k's dims 0..63: attention-score noise +11% over fresh int8.
  - Δ must be a multiple of 4 because of the pooled indexer blocks.
  - the GDN staleness has a closed-form bound for its direct term. on the real gates, ~3/4 of the heads forget B within ~100
    tokens, and 104 heads keep it for > 1,000.
  - nothing in A ran on the GPU. the test harness (`--scr-test`) is in the patch.
- **B (#97):**
  - on the real L47 dumps the base model **already doesn't produce the rejected slop** at these decision points: p(rejected opener)
    0.0003, p(closer set) 0.0000. the negative half of the objective is pushing on non-events.
  - the exact head-side solver is built, its gradient is checked, and i ran it four ways. it memorizes the 49 train replies but
    doesn't transfer the voice: held-out reply CE is flat or worse.
  - every config breaks the 0.01 KL bar on a held-out chat document (0.017–0.075).
  - so the voice belongs in Phase 3's real-forward LoRA, and the rejected side has to be mined from the model's own samples.

## A. Phase 5 (#98): disk sessions + SCR on the hybrid stack

### A.0 what the window actually holds (checked against `window.cpp` / `qsa.metal` / `gdn.metal`)
| state | layout (source) | bytes |
|---|---|---|
| GDN recurrence, 36 layers (`l % 4 != 3`) | `gst_[gi]` f32 `S[(i*48 + h)*128 + j]`, i = key dim, j = value dim (gdn.metal L9; init L556) | 3,145,728 each |
| GDN conv history | `ghist_[hp_][gi]` f32 `[10240 ch][3]` oldest first; ping-pong by `hp_` (L557–558, fold L1675) | 122,880 each |
| PLE history | `pleHist_` f32 `[10240 ch][9]` (L541; shifted in `commit` L1786–1792) | 368,640 |
| PLE token window | `ss.ple_prev[2]` (host `SessionState`, **not** in `checkpoint_save`) | 8 |
| QSA K, 12 layers | `kq_[qi]` int8 `[cell][2 kv][256]`, `ksc_` half `[cell][2][4]` (one scale per 64 dims) | 512 + 16 / cell |
| QSA V | `vq_` / `vsc_`, same shapes, no RoPE | 512 + 16 / cell |
| indexer | `pooled_` f32 `[cell/4][128]` (complete blocks only), `tail_` f32 `[4][128]` raw keys (slot `cell & 3`), `dead_` f32 `[128]` (cell 0 only) | 128 / cell + 2,560 |
| MTP layer (if loaded) | `mkq_ … mpooled_`, same QSA layout (L1862–1864) | same |

- total fixed = 36·(3 MiB + 120 KiB) = **112.22 MiB of GDN**, + PLE + tails/deads = **118,069,248 B (112.60 MiB)**; per cell **14,208 B**
  (12 · (1,056 + 128)).
- the existing `checkpoint_save` (L1688) stores only the running part (GDN + ghist + PLE + tails + deads); its comment says why: "the KV and
  pooled keys are positional", i.e. `--serve` assumes they're still in the buffers. a **disk** session must also carry
  `kq/vq/ksc/vsc[0, n)`, `pooled[0, n/4)` and `ss.ple_prev`, or the restored model has lost its two-token PLE n-gram context.
- RoPE, exactly (`norm_rope` L61–81, prep L104–147): position = cell (`pos_base` is 0 on all five `QsaArgs` sites), NEOX pairs
  `(i, i+32)`, i < 32, angle `cell · base^(−2i/64)`, base = 1e7 (read from the GGUF). **K is rotated before its int8 quantization**
  (L134 then L136–147), so the rotated dims 0..63 are exactly scale group 0; dims 64..255 and scales 1..3 never see a position.
  `pooled[b]` is `rope(rms(mean of 4 raw keys) · ik_norm, 4b)`, also dims 0..63 only, and stays f32. `dead` is unrotated.
  q / qidx are rotated at the query's own position at run time, so nothing about them is cached.

### A.1 disk session save / load
`/tmp/opus_scr_probes/window_session_scr.patch` (applies with `patch -p1 -d vendor/strata`, dry-run checked; both patched TUs pass
`clang++ -fsyntax-only -std=gnu++20 -Wall -Wextra` with the exact build.ninja include set, only pre-existing warnings):
- `Window::session_save(path, ids, n_cells)` / `session_load(path, ids, n_cells)` + C exports `window_session_save/load`.
  file = `scr::SessionHeader` (magic, version, flags PLE/MTP, n_cells, model shard size + tensor count + rope base, `ple_prev`,
  payload size) + token ids + the sections in a fixed order, each 16 KiB-aligned, read/written **straight into the MTLBuffer
  storage** (shared memory) with 8 MiB `pread`/`pwrite` pieces over 4 threads. no `ConversationCheckpoint` vectors, no second copy.
  written to `path.partial` then renamed. load checks magic/model/ctx/PLE/MTP/exact file size before touching anything and `reset()`s on
  a short read.
- save forces the pending lazy fold first (as `checkpoint_save` does); load sets `pend_fold_ = 0`, `last_T_ = 0` and writes the
  committed conv history into `ghist_[hp_]` (the parity the next window reads), exactly as `checkpoint_restore`.
- refuses M-RoPE (vision) sessions: `mrope_` comes from the server per request and isn't the window's to save.
- CLI: `--save-session f` saves after the prefill of the first `n_prompt − 1` tokens; `--load-session f` replaces that prefill
  and checks the stored ids equal this prompt's prefix. both print `STRATA_SESSION … ms`.

**exactness:** the loaded buffers are bitwise the saved ones and every kernel after that is deterministic, so the continuation is
bitwise the continuous run's, **with two conditions**:
1. compare against a continuous run that also passed `--save-session` (the forced fold puts the fold in its own command buffer;
   same kernel, same inputs, should be bitwise anyway, but it removes the doubt);
2. `STRATA_METAL_MOCE_BONUS=0`. with λ > 0 routing depends on which experts are resident, and a fresh process starts cold, so the
   loaded run routes differently for its first tokens. that's routing drift, not session drift, but it will show up as KLD ≠ 0.

test (parent, GPU, not run by me):
```
X="--metal-gguf …/K256-q2_0-healed-metal.gguf --ple-gguf … --tokens-file data/m5_val_prompts/prompt_2048tok_ops.txt --n-prompt 1984 --n-gen 64"
STRATA_METAL_MOCE_BONUS=0 strata_window_cli $X --save-session /tmp/s.bin --logits-out /tmp/ref.bin
STRATA_METAL_MOCE_BONUS=0 strata_window_cli $X --load-session /tmp/s.bin --logits-ref /tmp/ref.bin
```
expect `mean_kld=0.000000 top1_agree_pct=100.00 delta_ppl=0.0000`, and `STRATA_SESSION load_ms` vs the first run's `prefill_ms`.

**bytes and time** (probe [6]; the read rate is an assumption, measure it):

| cells | file | at 5 GB/s | at 3 GB/s |
|---|---|---|---|
| 1,024 | 126.5 MiB | 26.5 ms | 44.2 ms |
| 4,096 | 168.1 MiB | 35.3 ms | 58.8 ms |
| 8,192 | 223.6 MiB | 46.9 ms | 78.2 ms |
| 32,768 | 556.6 MiB | 117 ms | 195 ms |
| 131,072 | 1,888.6 MiB | 396 ms | 660 ms |

- so "< 50 ms" holds to ~8K cells cold if the SSD does ≥ 5 GB/s, and always from the page cache. the floor is the **112.6 MiB of
  GDN state**, which every session pays even at 10 tokens. that's the thing to shrink if many small sessions are parked:
  - an fp16 or bf16 GDN state halves it, but it's no longer bitwise. measure its KLD before using it.
  - for the "many turns of one conversation" case, keep one full session plus per-turn deltas of only the new cells (14 KB/token),
    with the GDN state of the latest turn only.
- vs re-prefill: 2K tokens at the M5's prompt rate is seconds; 168 MiB is tens of ms. the session is 2–3 orders of magnitude cheaper.

### A.2 SCR relocation: the exact byte/RoPE rules
`scr::relocate_layer` (scr_state.hpp, used by `Window::scr_relocate`, one thread per QSA layer, plus the MTP layer if loaded),
for a span of n cells moved by Δ = c_dst − c_src:
- **kq group 0 (dims 0..63)**: `x = q · half(ksc[.][.][0])` → rotate each pair `(i, i+32)` by `Δ·θ_i` (cos/sin in double, as the
  engine's table) → requantize with qsa_prep's rule (`sh = half(amax/127)`, `q = clamp(rint(x / float(sh)), ±127)`), new
  `ksc[.][.][0]`. rotation composes exactly (`R(Δθ)R(pθ) = R((p+Δ)θ)`), so the only loss is the second rounding.
- **kq dims 64..255, ksc[1..3], vq, vsc**: plain byte moves. (probe [1]: 0 mismatches over 20,000 rows.)
- **pooled**: complete blocks inside the span, `b → b + Δ/4`, dims 0..63 rotated by Δ in f32. **Δ must be ≡ 0 (mod 4)**: otherwise
  every block of the span would mix 4 different cells, and the raw indexer keys aren't stored (only their normed, rotated mean),
  so it can't be rebuilt. the patch refuses Δ % 4 ≠ 0; the harness pads B' (the test pads with `\n`, id 198) to make |B'| ≡ |B|
  (mod 4).
- **the junction block** (the block holding c_dst when c_dst % 4 ≠ 0) mixes B' and C cells: it isn't moved, and the splice
  must recompute at least `(4 − c_dst % 4) % 4` of C's first cells (`scr_min_junction`). the B' prefill writes its first part.
- **tail_** (raw keys of C's last partial block) stays valid because `c mod 4` is preserved by Δ ≡ 0 (mod 4). but every prefill
  writes it, so it has to be restored after the junction recompute. that comes for free with `checkpoint_restore(S_ABC)`.
  `dead_` depends only on cell 0 ∈ A and never changes.
- walk order: ascending when Δ < 0, descending when Δ > 0, so no source row is overwritten before it's read.
  (probe [3]: overlapping spans both ways, |Δ| = 1,200 < n = 1,001, plus the Δ % 4 refusal: 0 failures.)

numbers (probe [1]–[2], keys with lognormal per-dim gains, p ≈ 30,000, Δ = −2,048):
- group-0 RMS error vs the exact float row: fresh prep 1.081e-2, relocated 1.504e-2 (**1.39×**, the √2 of two independent
  roundings). group-0 bytes equal a fresh prep at p+Δ for 76.5% of values, the scale for 46.5%.
- attention score `q·k/16` RMS error vs float: fresh int8 1.51e-2, relocated 1.67e-2 (**+11%**; score RMS 2.90). the
  relocation's added noise is ~0.5% of a score's size, far below any staleness effect.
- pooled: max |relocated − fresh| = 1.9e-6 on values up to 13.8: f32 rounding only.
- the exact alternative (an fp16 shadow of group 0 to re-rotate from) costs +256 B/cell/layer (+22% KV). not worth it at +11% score noise.

### A.3 SCR splice order and what "keep the post-edit GDN" means here
the GDN state is one recurrent state, not a per-position cache, so `[A B C] → [A B' C]` has only these choices:

| variant | GDN running state after the splice | C's KV | cost |
|---|---|---|---|
| fresh (reference) | `S_{AB'C}` exact | recomputed | prefill \|B'\| + \|C\| |
| **keep** (CLM / Connectome rule, PLAN §2.5 #4) | `S_ABC`: has read B, never B' | relocated; first J cells recomputed after B' | prefill \|B'\| + J |
| gdnex (diagnostic) | `S_{AB'C}` exact (from the fresh run) | relocated (+J) | n/a: isolates KV staleness |
| lossy | `S_{AB'C[:J]}`: never read C past J | relocated (+J) | prefill \|B'\| + J |

splice procedure (`--scr-test`, in the patch):
1. load the `[A B C]` session (or be there already);
2. `scr_relocate(|A|+|B| → |A|+|B'|, |C|)`;
3. `checkpoint_restore(S_A)`, the turn-boundary checkpoint at |A| the harness already has; `ple_prev` of A;
4. prefill `B' ++ C[:J]` at |A| (fresh KV for B' and the junction, GDN goes S_A → S_{AB'C[:J]});
5. keep: `checkpoint_restore(S_ABC)` (GDN, ghist, PLE history, tails); `ple_prev` = C's last two tokens (token ids are never stale);
6. continue at |A|+|B'|+|C|.

rows printed: `orig` (no edit, the old context: tells you how much B → B' matters at all), keep / gdnex / lossy × J ∈ {0, 8, 16}
(raised to the junction minimum), and `lossy J=|C|`, which **is** the fresh prefill and must give KL 0 (built-in sanity row). each row
has mean / p90 / max KL vs fresh, top-1, PPL, load / relocate / prefill ms and the speedup vs the fresh `B' + C` prefill.
```
strata_window_cli --metal-gguf … --ple-gguf … --ctx 16384 --n-gen 128 \
  --scr-a A.ids --scr-b B.ids --scr-bp Bsummary.ids --scr-c C.ids --scr-d D.ids --scr-j 0,8,16
```
suggested spans: A = system + 2 turns (~1.5K), B = a long tool output (~3K), B' = its fold summary (~200), C = the last 2–4 turns
(~1.5K), D = the real next assistant turn (128). run it with `STRATA_METAL_MOCE_BONUS=0`, then once at the deployed λ. add the loop
check (PLAN §2.7 #13): free-generate 256 tokens from the keep J=0 state and count repeated 4-grams.

### A.4 the part of GDN staleness that has a closed form
from gdn.metal L89–119: `S' = g (I − b k kᵀ) S + b k vᵀ` with `|k| = sqrt(Σcv²/(Σcv²+eps)) ≤ 1`, `b = sigmoid ∈ (0,1)`,
`g = exp(softplus(α + dt_h) · ssm_a_h)`. two runs over the **same** C inputs from different start states differ by
`D' = g (I − b k kᵀ) D`, and `‖I − b k kᵀ‖₂ = 1`, so per value head

  **‖S_ABC − S_{AB'C}‖_F ≤ exp(Σ_{t∈C} log g_t) · ‖S_AB − S_AB'‖_F**  (direct term)

(probe [4] with gdn.metal's exact update: ratio to the bound ≤ 0.9966 over 64 steps.) the indirect term, C's own k/v/α/β computed under B
instead of B', has no closed form. that one is what `keep` vs `gdnex` measures.

the real gates (probe [5], `ssm_a` and `ssm_dt.bias` from the healed GGUF, at a neutral input α = 0), as half-lives
`ln 2 / −log g` in tokens over the 36 × 48 = 1,728 value heads:
- min 0.01, p10 1.1, **median 18**, p90 499, p99 3,746;
- **414 heads > 100 tokens, 104 > 1,000**, three at ~2·10⁶ (L01, L08, L24: no decay at all).
- so for ~3/4 of heads the direct staleness is gone within ~100 tokens of C. a small set of long-memory heads keeps "having read
  B" indefinitely. that's the CLM framing ("more faithful to its computational past"), and it's where drift over many folds would
  accumulate (PLAN §2.9 #1).
- α is input-dependent, so this is a prior, not a measurement. the per-head `Σ log g` can be accumulated exactly on the host from
  `alL_` (it's host-visible) if we want a live staleness meter.
- **a possible exact fix for the direct term (not built):** "GDN difference transport":
  `S_corr = S_ABC + Φ_C (S_AB' − S_AB)`. it runs only the homogeneous recurrence `D ← g(I − b k kᵀ)D` over C, and only for the long-memory
  heads (Σ log g > −7, i.e. not already decayed by 1e-3), with C's k/b/g kept at prefill time.
  - storage ≈ 256 B/token per k-head kept (fp16 k + b + g), so ~50 heads ≈ 13 KB/token. needs `S_AB` (a turn-boundary checkpoint) and
    `S_AB'` (free from step 4).
  - only worth building if the test shows `keep` ≫ `gdnex`.

## B. Phase 4/3 (#97): contrastive negative-slop + chuuni/VN persona on the exact head side

### B.1 the exact forward and what an output-side edit can and can't touch
from the L47 dumps (`R_post47` = streams after layer 47's FFN write), per row t:
`xn_c = R_c ⊙ w_onorm,c · rsqrt(mean R_c² + 1e−6)`, `lo = silu(W_odown · xn / 4)`, `z = ¼ Σ_c xn_c ⊙ σ(W_oup,c · lo)`, `ℓ = W_out z`
(audit v3 §3.1; `output_hc_*` through their Q8_0 copies, `W_out` Q6_K).
- **`z` doesn't depend on `output.weight` at all.** so with teacher forcing, an edit of a row set S of `W_out` is exact at
  every position, and linear: `ℓ'_v = ℓ_v + ΔW_v·z` for v ∈ S, everything else untouched.
- the only coupling is the partition function:
  `LSE' = logaddexp(LSE_rest, LSE_{v∈S}(ℓ_v + ΔW_v z))`, with `LSE_rest` the logsumexp over the rows outside S, computed once.
- one full-vocab pass (`LSE_rest`, the S logits) makes every later step cost |rows| × |S| × 2560. no 248K-wide product per step.
- the specials (`<|im_end|>`, `<|im_start|>`, `<think>`, `</think>`, `<|endoftext|>`) are kept **out of S**, so their change is
  exactly `Δlog p_special = −(LSE' − LSE)`. "no special-token rise" is then the constraint **LSE' ≥ LSE − ε** at every
  non-terminal row. that's the v3.1 failure mode (freed mass going to `<|im_end|>`) turned into one scalar per row.
- `output_hc_down/up` are exact too, but they act as per-dim gates in (0, 1) on all of `xn`, on every token. that's a poor lever for
  adding new directions, and every edit there reaches the whole vocab. `blk.47.ffn_down_shexp` (v3.2's tensor) is exact as well, but it
  shifts R, i.e. all 248K logits through the HC mixer. L40–46 aren't exact (later attention/GDN/routing), as noted in v3.

### B.2 the objective (implemented: `/tmp/opus_scr_probes/persona_head_solver.cpp`)
over ΔW_S (|S| × 2560, Q6_K-round-tripped for scoring as shipped). p = base, p' = edited:
```
J = w_nll   · mean_{t ∈ chosen replies, train}  [ LSE'_t − ℓ'_{t,y_t} ]                        voice SFT (exact NLL)
  + w_m     · mean_{t = step0, train}           softplus(m − (ℓ'_{chosen0} − ℓ'_{rej0}))      contrastive margin at the shared prefix
  + w_ul    · mean_{t = step0}                  −log(1 − Σ_{o ∈ O \ y} p'_o)                  unlikelihood, opener slop set O
  + w_close · mean_{t = reply end}              −log(1 − Σ_{c ∈ Cl} p'_c)                     unlikelihood, closer set Cl
  + w_kl    · mean_{t ∈ anchors, train}         [ Σ_{v∈S} p_v(−Δ_v) + LSE'_t − LSE_t ]        = KL(p ‖ p') exactly
  + w_g     · mean_{t: y_t not special}         relu(LSE_t − LSE'_t − ε)²                     specials may rise ≤ e^ε
  + λ ‖ΔW_S‖²,       optional ΔW_S ← ΔW_S (I − Q Qᵀ), Q = top-r of the train anchors' Σ z zᵀ
```
- the gradient wrt the S logits is the usual `p' − onehot` / `σ(·)` / `(p'_v[v∈O] − P_O p'_v)/(1 − P_O)` / `p' − p` / `−2h p'`,
  then `∂J/∂ΔW = Gᵀ Z`. finite differences agree to ~1e−4 relative on 8 entries (`fdcheck=1`).
- the margin is taken only at step0. that's the one place where chosen and rejected share their whole prefix, so it's a true same-z
  preference. later rejected tokens sit in different contexts that weren't dumped. that's also why token-level UL beats sequence DPO
  here: DPO's `Σ log π'/π_ref` over a rejected reply would push down every ordinary token inside a slop sentence (v3 §5 item 3 all over
  again).
- the sufficient condition for KL: `KL(p‖p') ≈ ½ Var_{v∼p}(Δℓ_v)`, so the null-space projection bounds anchor damage by the anchor
  energy outside Q.

### B.3 measured on the real dumps (p4_contrastive_vn, 3,698 rows + 5 anchor traces; `/tmp/opus_scr_probes/persona/run*`)
**first, the data:** at the 70 step0 positions the base model already gives the rejected opener **p = 0.0002–0.0003** (train/held-out
mean), and the whole opener-slop set ("Hello", "Certainly", "Great", "Sure", "You", "Here", …) **0.001**. at the 63 turn ends,
the closer set (" Let", " Please", " Feel", " Hope", " Happy") has **p ≈ 0.0000**, and `<|im_end|>` 0.85–0.94.
- **the negative half has nothing to push on**: the UL loss starts at 0.0010, and the margin loss (0.49 at iteration 0) is at 0 within 25
  iterations.
  same failure as v3 §3 item 2: `rejected_slop` was written by another model and isn't this model's behaviour.
- what the base *does* get wrong at step0: **argmax is a special token in 14/70** (12 × `<think>`, 2 × `<|im_end|>`). the trace uses
  `</think>\n` (198) before the reply; check whether Phase 3 was trained on `</think>\n\n` (271), the usual template. a mismatch there
  would explain it.

| run | config | \|S\| | held-out reply CE (907 tok) | held-out p(chosen0) | train reply CE | KL cal / collat (train anchors) | KL ftpo_heldout / **blind chat** (held-out anchors) |
|---|---|---|---|---|---|---|---|
| base | – | – | 2.593 | 0.146 | 2.825 | – | – |
| run1 | full objective, 300 it, lr 3e−4, w_kl 20 | 2,917 | **2.823 (worse)** | 0.220 | 0.041 | 0.0015 / 0.0017 | 0.030 / **0.075** |
| run2 | + null r=256 (87% of anchor energy), w_kl 100, 150 it | 2,917 | 2.692 | 0.186 | 0.240 | 0.0038 / 0.0053 | 0.031 / **0.073** |
| run3 | + ridge 1e−2, S = targets + top-8 at step0 only | 1,129 | 2.581 | 0.181 | 0.256 | 0.0026 / 0.0026 | 0.024 / **0.051** |
| run4 | as run3, NLL at step0 only (opener steering) | 1,654 | 2.599 | **0.208** | 2.718 | 0.0019 / 0.0012 | 0.0086 / **0.0168** |

(no argmax flips to a special token on the plain anchors. on the chat ones it's 0–8 per 1,098 rows. full per-row data in
`run*/rows.tsv`; the shipped Q6_K row patch is `run*/output_weight_rows.q6k`, format `[n][row bytes] {id, 2,100 B}×n`.)
- **it memorizes; it doesn't learn a voice.** train reply CE 2.83 → 0.04–0.26, held-out reply CE 2.59 → 2.58–2.82 (flat at best).
  49 replies are a lookup table for 1–3K rows of `W_out` given the z's.
- **it also leaks into chat.** the plain-text anchors stay ≤ 0.005, but held-out *chat* documents reach 0.017–0.075. the persona
  contexts share z directions with any chat turn, and 256 null directions cover only 87% of the anchor energy (chat z's are high-rank).
- the one effect that transfers is run4's step0 steering: held-out p(chosen0) 0.146 → 0.208, argmax-special 6 → 3 of 21. but blind-chat
  KL is 0.0168 > 0.01.
- **so against the falsifier: every head-side persona objective i ran exceeds 0.01 KL on a held-out chat document.** the 3-domain eval
  texts (ops/chat_json/ja) aren't dumped, so their KL isn't measured here. ops_tools/ja are closer to the plain anchors (≤ 0.005), but
  chat_json is chat. don't ship any of these.

### B.4 what i'd do instead
1. **mine the rejected side from this model's own samples** (PLAN §7 phase 4). run the 70 prompts at T = 0.8, 8 samples each, and keep
   the samples whose spans match the slop lexicon / narration patterns; those spans are the rejected tokens, in their real contexts.
   - only then does UL/FTPO have mass to remove. the base already doesn't say "Certainly!" here, so the slop that does exist will be
     different (narration before tool calls, hedged closers, …).
   - dump those sample traces through the engine (`STRATA_METAL_DUMP_MOE`) and the same solver scores them exactly.
2. **voice (lowercase, kaomoji, chuuni/VN banter, R-Tuning abstention) belongs in Phase 3's real-forward LoRA.** it's content-level and
   needs the whole network.
   - an exact output-side map can't generalize it from 49 replies.
   - use chosen-NLL plus the mined rejected tokens as token-level UL (not sequence DPO, for the reason in B.2), with the 3-domain KL
     anchors + a chat-formatted anchor document as a held-out bar.
3. **the step0 problem is a template question first.** check `</think>\n` vs `</think>\n\n` against the Phase 3 SFT data before
   training anything on it.
   - if it's real, the exact zero-collateral fix is a **position-conditioned sampler bias**: at the first assistant token after
     `</think>` + newline, add −B to `<think>`/`<|im_end|>`.
   - it touches no other position (the same reasoning as the v3 §6.3 prefix penalty).
4. **if a head-side lexical edit is still wanted**, reuse this solver with S = only the mined slop tokens + their top-8 natural
   alternatives, chat anchors in the KL term, and the same bars as v3.2: (a)–(g) plus blind-chat KL ≤ 0.005. keep w_g on: it's what
   keeps `<|im_end|>` from collecting the freed mass, by construction.

### B.5 a nuance on the Q6_K round trip
re-quantizing unchanged Q6_K rows isn't idempotent: ggml's scale search lands elsewhere. at ΔW = 0 the 2,917 round-tripped rows alone
give KL 0.0002–0.0005 (fdcheck run, iteration 0). so ship original bytes for every row whose edit is below the Q6_K step; only
truly edited rows should be re-quantized.

## files
- `/tmp/opus_scr_probes/window_session_scr.patch`: `patch -p1 -d vendor/strata`. new `metal/src/engine/scr_state.hpp`; `window.cpp`
  (session save/load, `scr_relocate`, exports, `rope_base_`); `strata_window_cli.cpp` (`--save-session`, `--load-session`, `--scr-test`).
  dry-run applies cleanly; both TUs pass `clang++ -fsyntax-only -std=gnu++20 -Wall -Wextra` with the build.ninja flags (only the
  pre-existing warnings). **not built or run on the GPU.**
- `/tmp/opus_scr_probes/scr_state.hpp`: header-only layout, file header, relocation, gate math (same file as in the patch).
- `/tmp/opus_scr_probes/scr_probe.cpp` (+ binary `scr_probe`): CPU checks [1]–[6] quoted above (`./scr_probe <gguf>`, < 1 s, exit 0).
- `/tmp/opus_scr_probes/persona_head_solver.cpp` (+ binary): the B.2 solver. `persona/spec.txt`, `persona/vn_roles.txt` (row roles of the
  contrastive trace), `persona/vocab.txt`, `persona/run{1..4}/` (rows.tsv, Q6_K row patch), `persona/fd/` (gradient check). each run ~1–2
  min CPU (Accelerate), no GPU.

## what's still open (needs the GPU, parent's call)
1. session round trip: KLD 0 with `MOCE_BONUS=0`, and the measured load ms at 1K / 4K / 8K cells.
2. `--scr-test` on a real fold (A.3): keep vs gdnex vs lossy at J ∈ {0, 8, 16}, the sanity row at 0, speedup, and the 256-token loop check.
   the decision rule: if keep ≈ gdnex, GDN staleness is negligible and SCR ships with J = 8–16; if keep ≫ gdnex, build the
   long-memory-head transport (A.4) or re-ground the GDN after N folds.
3. Phase 4: mined rejected samples (B.4.1), and the template check (B.4.3).
