ADR 0016 · Implemented; mechanism works as designed; does not lift benchmark above frozen baseline

Consolidation during replay — refreshing rehearsed templates' last-match ticks

May 25, 2026

Decision 0016: Consolidation during replay

Date: 2026-05-25 Status: Implemented; mechanism works as designed; does not lift benchmark above frozen baseline Builds on: ADR 0015 (LRU eviction) Motivated by: ADR 0015’s negative result — eviction alone shuffled capacity without preserving anything

Context

ADR 0015 introduced LRU eviction so that the context-cell pool would no longer be write-once. The empirical result was that LRU alone made the benchmark numbers worse than the frozen ADR 0012 baseline: with no mechanism to identify which templates are still being used, LRU thrashed, and the test-time probe (which observes 4 prompt tokens per fact) was itself evicting trained templates. The single-sentence summary from ADR 0015 was: eviction alone shuffles capacity but preserves nothing.

This ADR adds the missing half. During every excitation tick on the replay path (the system “dreaming”), scan the replay buffer for occurrences of each context cell’s template as a contiguous K-token subsequence. Any cell whose template appears in the buffer gets its _context_last_match_tick refreshed to the current tick. Templates that are being rehearsed by recent experience stay “warm” and are protected from LRU eviction; templates that have fallen out of recent rehearsal eventually become eviction candidates.

This is the consolidation-by-replay mechanism that several earlier ADRs implicitly assumed existed: rehearsal during quiet periods strengthens what should be remembered, neglect weakens what isn’t relevant anymore.

Decision

Trigger

Consolidation runs once per excite() call on the replay path, after the replay’s preference and connection updates and immediately before the tick counter advances. It does not run on the fallback (buffer-empty) path — there’s nothing to consolidate from. It does not run during observe() — observe already updates last-match ticks for live matches against the current window.

Mechanism

for each context cell c with a claimed template T:
    if T appears as a contiguous K-token subsequence in _observation_buffer:
        _context_last_match_tick[c] = self.tick

Implementation uses numpy.lib.stride_tricks.sliding_window_view to build a (buffer_len − K + 1, K) view of the buffer and then a row-wise equality check against each template. For a default buffer of 256 entries with K = 4 and ~64 context cells, this is roughly 16,000 element comparisons per excite tick — negligible at the system’s natural tick rate.

Configuration

context_template_consolidation: bool = True (default on). Default-on is safe even when ADR 0015’s eviction is off: nothing reads _context_last_match_tick in that case, so refreshing it is a silent no-op. Setting the flag to False is for tests that want to verify dream ticks make no observable change to the template ledger.

What this fixes and what it does not

Fixes (mechanically)

  • The LRU policy can now distinguish rehearsed templates from stale ones. With consolidation on, templates that recur in the replay buffer have their ticks refreshed during dream phases; templates that don’t, age out. The relative ordering used by ADR 0015’s eviction now reflects rehearsal patterns rather than just claim order plus live-window-match recency.
  • Pure observe-only workloads still work. Consolidation is a no-op outside excite(), so workloads that never dream behave identically to ADR 0015 + ADR 0012.

Does not fix (empirically)

The full benchmark — frozen baseline vs. ADRs 0015+0016 combined with 200 dream ticks between training and probe — disconfirmed the hope that the combination would lift the substrate above the frozen baseline:

TaskFrozen (ADR 0012)Full (0015+0016)Transformer
Task 1 memorization (N=2048)36.1% ± 3.920.0% ± 3.6100%
Task 2 held-out generalization3.3%16.7%73.3%
Task 3 cycle100%100%100%
Task 4 A after phase 2 (preservation)62.2% ± 7.212.2% ± 2.543.3%
Task 4 B after phase 2 (learning)5.6%16.7%100%
Task 5 K=512 adaptation4.4% (flat)17.8% (climbs)100%

The two architectures fail differently. The frozen substrate is rigid: it preserves whatever fit in 64 cells during phase 1 (Task 4 A = 62%) and refuses to learn new material (Task 4 B = 6%, Task 5 flat). The full substrate is plastic: it adapts to new material (Task 5 shows a climbing curve, Task 2 lifts from 3 → 17%) but loses old material in the process (Task 4 A drops 62 → 12%). Neither beats the other; together they describe a trade-off the architecture cannot escape with policy changes alone.

The wall is the capacity ceiling itself. With 36 arithmetic facts × multiple sliding 4-windows each, the active working set is structurally larger than a 64-cell pool can hold. ADR 0015 lets the substrate cycle through its capacity; ADR 0016 lets the cycling prefer rehearsed material over forgotten material. Neither manufactures more capacity. The honest read: this is no longer a memory-management problem; it’s a representation problem — the architecture needs templates that compress many surface variants into a single cell.

What “implemented” means here

The mechanism is correct (verified by direct unit tests on _context_last_match_tick post-excite()) and the policies are now composable. The mechanism is honest about what it can and cannot do (this ADR documents both). The default for context_template_consolidation stays at True so the mechanism is available to anyone who turns on context_template_eviction; the default for context_template_eviction remains False because the combined system trades preservation for plasticity without strictly beating either extreme.

Behavioral predictions

Tests in tests/test_context_cells.py:

  1. test_consolidation_enabled_by_default — default is True.
  2. test_consolidation_refreshes_template_tick_when_pattern_in_buffer — an excite() tick refreshes the last-match tick of any context cell whose template appears in the buffer, even if no live match against the current observation window has happened.
  3. test_consolidation_skips_templates_absent_from_buffer — templates not in the buffer are not refreshed.
  4. test_consolidation_disabled_is_noop_in_excite — with the flag off, excite() does not touch last-match ticks.
  5. test_consolidation_keeps_rehearsed_template_above_unrehearsed_in_lru — direct invariant: after consolidation runs on a buffer containing pattern A but not pattern B, the last-match tick of A’s cell is strictly greater than B’s.

Open questions deferred

  • Hierarchical / generalized templates. The next architectural unlock probably isn’t a better cache policy. It’s templates that subsume surface variants — one cell encoding “any digit + any digit = …” rather than 36 cells each encoding one specific instance. ADR 0014’s slot cells were the first attempt; the benchmark showed they don’t actually generalize (3% on held-out facts). A real fix likely needs learnable slot positions and a way for the substrate to discover which positions vary across observations.
  • Weighted consolidation. Currently any match in the buffer refreshes the tick to the current value. A frequency-weighted scheme would refresh proportionally to how many times the template appears in the buffer, providing finer-grained protection. Probably worth trying before discarding the policy axis entirely.
  • Selective replay. The current replay sampler (ADR 0004) is recency-weighted across the whole buffer. A consolidation-aware sampler could preferentially replay tokens that belong to patterns claimed by context cells, accelerating their tick refreshes. Speculative; might compound the existing trade-off rather than relieve it.
← All decisions