Andrew Fribush

← All posts

Confabulation Is a Cache-Coherence Bug

· December 21, 2026 · 8 min read

One of my long writing projects with a model had the whole standard agent-continuity stack. It kept an event log in git with a session history beside it. It wrote a compaction artifact at the end of every session. It had a materialized map of the project, a tree for superseded and mirrored material, a register of what was still undecided, and provenance for each session. The project was hand-built over 38 sessions and 66 commits, and it arrived at that convergent design independently. The compaction artifact still spent a section every session re-auditing specifics the model had made up.

The stack was correct, and the bug was in what the next session read first. The workflow has three layers. The conversation is the only place the model reasons, and it holds a possibly stale copy of the project. The project files are the truth. The history is append-only and records what happened and why. Compaction freezes the conversation, stale lines included, into history, and the next session rehydrates from history instead of from the project. Confabulation in long projects is a cache-coherence bug: nothing invalidates the model's copy when the project changes, and nothing stops a dirty, unverified line from being read back as truth.

The projects here are all mine, and all in one structural family: long fiction projects, contest entries and scripting work, each a set of files a model edits across many sessions. I designed the fix from this diagnosis and built it as a command-line tool over git and files, with no database and no daemon. I ran it on three of those projects, and the context loaded into every session dropped by about 80, 90 and 66 percent.

Why the standard fixes could not work

The usual responses to a model that forgets and invents are better prompts, a bigger context window, a hard rule, and a feedback file, and none of them stops it. On this project prompting was the experiment, and the project escalated to the last two. It had a hard rule against confabulating, and then a dedicated feedback file telling the model not to carry an earlier session's confabulations forward as canon. It still leaked.

Put each of those in cache terms and the failure is predictable. A hard rule is an instruction that lives in the conversation, which is the cache. A feedback file about past confabulations is one more cache line, loaded alongside the stale ones it describes. A bigger context window is a bigger cache, so it holds more stale lines for longer. None of the four changes which layer the next session reads from, and none of them invalidates anything when a project file changes.

The same defect reproduced across other fiction projects, across contest entries, and across scripting work. Since the bug shows up in prose and in code, the cause is the workflow and not the genre or the language. All three work the same way: a session reasons, a summary is written, and the next session starts from the summary.

The arrow that was missing

fig. 2.25.1 · three layers, and the next session reads the wrong one: nothing runs from the project back to the conversation to mark its copy dead.
NOTHING RUNS FROM THE PROJECT BACK TO THE CONVERSATION TO MARK ITS COPY DEADONE BOX, ONE LAYER; INK ARROW: DONE EVERY SESSION; THIN GREY: ONCE AT START; DASHEDACCENT: NEVER DONEINITIAL LOAD, SESSION START ONLYINVALIDATE WHEN A PROJECT FILECHANGES: MISSINGCOMPACTION WRITES CONVERSATIONINTO HISTORYNEXT SESSION READS HISTORY INTOCONVERSATIONLAYER 2 · PROJECTDURABLE, MUTABLETHE TRUTHLAYER 1 · CONVERSATIONEPHEMERAL, VOLATILEWHERE THE MODEL REASONSLAYER 3 · HISTORYDURABLE, APPEND-ONLYWHAT HAPPENED AND WHYMODELREADS

source: cairn DESIGN.md "Root cause", lines 109-125 (12dd72d) · threeLayers.data.ts

The conversation is ephemeral. It lives until compaction, it's the only place the model reasons, and it holds a cached copy of the project that may already be stale. The project is durable and mutable: the files that say what is true right now. The history is durable and append-only: what happened, in what order, and why. In the original workflow the next session also read the history as truth, and that was the bug.

Now trace the arrows. The model reads the conversation. At the end of a session, compaction writes the conversation into history, and it writes the stale lines and the invented lines too, because it can't tell them apart from the real ones. The next session loads history into its conversation. The project feeds the conversation once, at the start, when the files are opened. Say a project file then changes, in the middle of a session or between two of them. Nothing runs from the project back to the conversation to mark the old copy dead, so the conversation keeps using it. A cache that writes back and never invalidates does the same thing.

fig. 2.25.2 · every failure the project logged is one cache-coherence bug: a stale read, a dirty write-back, the log read as the database, the wrong home.
EVERY FAILURE THE PROJECT LOGGED IS ONE CACHE-COHERENCE BUGONE ROW, ONE FAILURE THE PROJECT LOGGED; GREY: WHAT HAPPENED IN THE THREE LAYERS;ACCENT: ITS CACHE NAMESUPERSEDED PROSE LEAKINGLAYER 2 CHANGED; LAYERS 1 AND 3 STILL HOLD THE OLD BODY= STALE READ AFTER A MISSED INVALIDATIONCONFABULATION LAUNDERINGA DIRTY LAYER-1 LINE WRITTEN INTO LAYER 3, READ BACK AS TRUTH, NEVER CHECKEDAGAINST LAYER 2= DIRTY WRITE-BACK READ AS CLEANEVENT LOG AS TRUTHTHE SESSION READS LAYER 3 FOR WHAT IS TRUE, NOT LAYER 2= READING THE CACHE LOG INSTEAD OF THE DATABASESCOPE BY CWDTHE WORKING DIRECTORY DECIDES WHICH LAYER 2 THE MODEL COHERES TO= COHERENCE AGAINST THE WRONG HOME

source: cairn DESIGN.md "Root cause", lines 125-132 (12dd72d) · cacheFailures.data.ts

Every failure the project logged is one of these. Superseded prose leaks because the project changed and the conversation and the history still hold the old version, which is a stale read. An invented detail becomes accepted fact when it is written into history from a dirty conversation line and read back next session as though it had been checked. That is a dirty write-back read as clean. Treating the event log as the project is reading the cache log instead of the database. And when the working directory decides which project the model thinks it's in, the cache is cohering against the wrong home.

The model would need three discriminators to avoid these: live or dead, confirmed or unconfirmed, this project or all my writing. They were always written as English sentences inside the files, and a model can ignore an English sentence. An invalidation can only fire on a field it can read, and while the status lived in prose there was no field.

The v1 mistake and its correction

My first design said compaction should be built from events and the project projection only. That was wrong. It would have thrown away the one thing compaction exists to keep, which is why a choice was made, which alternative was rejected, and what the next session was supposed to do. None of that has a home in the project files.

The corrected design tags every event as one of two kinds. A fact change says something in the project changed. A decision records a choice, a rejected alternative, or the intent for the next session. Compaction keeps both kinds in history, each tagged with the session that produced it, and nothing is thrown away.

History is readable for why, and never as authority for what is true. A fact enters canon only if it round-trips through the project layer with a live status and survives the linter. The reasoning stays in full. The only thing cut is the path from "written in a session" to "treated as canonical", and every laundered confabulation took that path.

Here's the shape of it, reduced to the three layers as data. This is illustrative, not the shipped code.

TypeScript
type Event = { session: number; kind: 'fact' | 'decision'; text: string };const conversation: string[] = [];             // layer 1: volatile, where the model reasonsconst project = new Map<string, Entity>();     // layer 2: the truth, status per entityconst history: Event[] = [];                   // layer 3: append-onlyfunction compact(session: number, events: Event[]) {  history.push(...events.map((e) => ({ ...e, session }))); // keep facts and rationale}function rehydrate(): string[] {  const what = [...project.values()].filter((e) => e.status === 'live' && lint(e));  return what.map(render);                      // facts from the project, never from the log}function why(topic: string): Event[] {          // rationale: fetched on demand, read as "why"  return history.filter((e) => e.kind === 'decision' && e.text.includes(topic));}

The rehydrate function is where the original workflow went wrong. It read facts from history, and in this version facts come only from project. Decisions stay in history and come back only when asked for, labeled as rationale.

What follows for the tools

The resident context, the part loaded into every session, becomes a small, scoped projection of the live entities in the project layer. An index on disk lists every entity for a one-line lookup, and the full bodies are fetched only when needed. A cache should be small and fresh, and on the original project the resident surface dropped from about 16,633 tokens to about 3,254. A linter guards the write into canon. It flags an invented name, meaning one that appears nowhere in what has already been written, before it commits. It also flags a sentence that asserts something on a topic still marked undecided, and one that names a superseded entity.

A separate judge from a different model family checks a generated claim against the canon it cites. That is the only way to catch a distortion of a fact that is correctly in canon. I ran it on the original project's three traced distortions, against the files as they were actually committed. It caught all three and quoted the passage each one contradicted. The hardest case sat in one file of about 23,500 tokens that held both the invented version and its later correction, and the judge quoted the correction. The original project did this check by hand, in a section of its compaction artifact every session. The linter and the judge are the validation steps, and the projection is the invalidation the original stack never had.

The ceiling

The design document says it directly: this version makes the failure rare and auditable, and doesn't make it impossible. A model can still reason wrongly over a fresh, correct cache, and no file structure prevents that. What the structure removes is the laundering. An invented detail can no longer become truth by being summarized, because a summary is history and history is never the source for what is true.

Limits

All of these projects are mine, and they belong to one structural family: files a model edits over many sessions with summaries between them. The cache framing is a design frame that accounts for every failure the project logged. It isn't a measured model of what happens inside the model. The mechanical fixes (the projection, supersession, the linter, recall of the right body or its tombstone) fire correctly on three real projects. The behavioral claim is that the model heeds those signals in the middle of a real writing session instead of confabulating anyway. That is still a bet, and only weeks of real sessions on those projects will settle it.

Andrew Fribush, software and computer engineer in Austin. I build consequential systems where software meets institutions: finance, healthcare, commerce, government data, and the mechanisms that govern AI agents. Taking new engagements: hello@andrewfribush.com.

I write when a system teaches me something. Get new posts by email: no tracking, one-click unsubscribe.