Nine stages, nine amnesiacs: context isolation as an architecture

Dex never lets a session get long enough to rot. Every stage is a brand new agent with an empty context window, state travels through the filesystem, and the unit of everything — execution, resume, storage, the UI — is the stage. Including what that costs.

Every long agent session dies the same way, and it never looks like dying. It looks like agreement.

Somewhere past the hundred-thousandth token the model stops pushing back. It re-reads a file it read forty minutes ago. It forgets the constraint you gave it in turn four — not dramatically, just quietly, the way you forget a name at a party. It marks a task complete with total confidence and a test suite it never ran. The prose is still fluent, the tone is still crisp, the bullet points still line up. That is exactly the problem. Fluency is the last thing to go.

The industry’s answer is a bigger context window, which is the right answer if you believe the failure mode is capacity. I don’t. A long session is not a bucket filling up; it is a room getting messier, and past a certain point the model spends more of itself navigating the mess than doing the work.

Dex takes the opposite bet: never let a session get long enough to rot. It runs a project as a sequence of discrete stages, and every single stage is a brand new agent with an empty context window. Nine stages, nine amnesiacs. None of them has ever met the others.

AGENTS — ONE FRESH CONTEXT WINDOW EACH no context flows sideways specify agent #4 plan agent #5 tasks agent #6 implement agent #7 verify agent #8 FILESYSTEM — READ AT THE START OF A STAGE, WRITTEN AT THE END spec.md tasks.md learnings.md git refs

State does not travel through the context window. It travels through disk. That is Ralph Wiggum’s idea, not mine, and it is the whole trick: each stage reads what it needs, works inside the sharp part of a fresh window, and writes its results back.

The nine, and what each one leaves behind

An architecture built on amnesia only works if every stage’s output is a file someone else can read cold. So the useful way to describe the loop is not by what each stage does but by what it leaves on disk:

Stage Runs Leaves behind
clarify once, interactively full_plan.md — the clarified goal, produced by Q&A with you
constitution once constitution.md — project-wide principles every later spec must obey
gap analysis every cycle one decision: what this cycle is for
specify per feature spec.md — user stories and acceptance criteria
plan per feature plan.md — technical approach, architecture decisions
tasks per feature tasks.md — dependency-ordered, individually actionable
implement per phase of tasks.md code, and the commits that carry it
verify per feature a structured verdict — build, tests, criteria, blocking vs minor
learnings per feature appended insights that later cycles read

Around those sit the mechanical steps that are not really agent work — prerequisites, branch creation, a one-off manifest extraction, and a completion step at the end.

Four of the nine are spec-kit’s, and Dex does not reimplement them; the whole specify stage prompt is one line of /speckit-specify. The three that are not spec-kit’s are the three that make it a loop rather than a workflow: gap analysis decides what a cycle is for, verify provides the backpressure that lets a cycle fail, and learnings is the only channel through which a run influences its own future.

The ordering is not arbitrary either. Every stage’s input is the previous stage’s committed output, which is what makes the amnesia survivable — nothing in the chain requires knowing how the previous artifact came to exist, only what it says.

The stage is the unit of everything

This is the part that took a dedicated spec to get right, and the mistake is common enough to be worth naming precisely.

Cycles were originally the unit of the loop. Stages were an implementation detail of a cycle. That was fine right up until stages appeared in the UI as a row of things with their own names and their own progress — at which point the user’s mental model became “stages” and the engine’s model was still “cycles”, and every bug living in that gap was invisible to me and infuriating to anyone using it.

The clearest symptom was Stop. Click it at a cycle boundary and everything behaved. Click it anywhere else and: specify had finished, plan had not started, and on resume the loop counted the aborted cycle as complete, began a fresh one, and left the spec directory specify had just written orphaned on disk. Never planned against, never implemented — a folder you paid four dollars for and could never use.

The fix is to make the stage the unit of resume as well as execution: persist lastCompletedStage, and let a resume re-enter the middle of a cycle against the same spec directory rather than restarting from the top. That is the RESUME_AT_STEP path, and it is deterministic — lastCompletedStage is a recorded fact, not something a model is asked to infer.

Once the stage is genuinely the unit, the rest of the architecture falls out of it. A stage is one query() call. A stage is one commit. A stage is one row in the timeline, one entry in the trace, one resume point. Those are not four coincidences; they are one decision, and the places Dex was buggy were exactly the places where one of the four had not caught up with the others.

What context isolation costs

Every stage re-pays its read cost. This is the direct and unavoidable price. An agent that remembers can answer a follow-up question for free; an amnesiac has to re-derive the situation from disk each time. Across a nine-stage cycle the same plan.md may be read four times by four different agents. Context isolation trades tokens spent remembering for tokens spent re-reading, and that trade is excellent precisely when a session would otherwise degrade — and simply wasteful when it would not.

Anything not written down is lost, including things worth keeping. Artifacts survive. Reasoning does not. When the plan agent considered three approaches and rejected two for good reasons, plan.md records the winner; the two rejections evaporate with that agent’s context. The implement agent may then cheerfully rediscover a dead end that was explored and dismissed twenty minutes earlier, because nothing in the pipeline captures why not. Human handoffs have the same failure, and humans compensate with a corridor conversation. There is no corridor here.

Nine stages is a lot of fixed overhead. A feature that is genuinely twenty lines still pays for gap analysis, specify, plan, tasks, implement, verify and learnings. The ceremony can cost more than the work. Dex earns its keep on multi-feature projects running unattended for hours; for “add a field to this form”, opening Claude Code and typing is strictly better.

And the stage boundary is where meaning gets lost. Every handoff is a serialisation, and every serialisation drops something. That problem turned out to be sharp enough — and expensive enough — to deserve its own post, which is the next one.

The takeaways, minus the product

Strip Dex away and three things survive that I would apply to anything letting an agent run unattended:

  1. Context isolation beats context size. Design for a fresh brain, not a bigger one. The fresh brain is cheaper, more predictable, and gets better for free every time the model does.
  2. State that outlives a process belongs on disk — and if your users already have git, you already have versioned, branchable, shareable state with a twenty-year track record. Don’t invent a second one.
  3. Pick one unit and make everything agree with it. Execution, storage, resume and display should all be granular at the same level. Every place they disagree is a bug you have not found yet.

Dex is a desktop app — Electron, React, the Claude Agent SDK, no database, all state in files and refs — and it is on GitHub. It is early in the way that matters: it works, it has built real features unattended, and I still find something wrong with it most weeks.

Next: structured handoffs — between two agents, prose is a wire protocol, and a catastrophically bad one.