Skip to content

9 min read

The ledger is working memory, not only crash recovery

On this page

Nothing kills this run. It reaches iteration twenty-two, three hundred and eighty thousand cumulative tokens, and step nine of twelve, and it is still going. At step nine the model looks at internal/httpx/client.go, sees a variadic parameter that no caller in the current tree omits, decides the variadic form is dead weight, and changes the signature to take the policy positionally. Thirty call sites in packages it has not touched break instantly. Step nine's check fails with sixty-two compiler errors, none of which have anything to do with step nine.

The variadic parameter was a decision made at step two, for exactly the reason chapter one gave: it keeps the tree compilable while callers migrate in batches, and it is deleted at step twelve when nothing needs it. That reasoning was in the conversation at iteration three. By iteration fourteen the raw transcript had reached two hundred and sixty thousand tokens against a two-hundred-thousand-token window, automatic compaction had fired, and the summary it produced retained the shape of the task — extract auth, several steps done, tests passing — and dropped a sentence about a variadic parameter that looked, to a summariser, like an implementation detail.

The run then did what a competent engineer with amnesia would do. It read the code, found something that appeared unnecessary, and removed it. There was no bad reasoning anywhere in the trace; the premise the reasoning needed had been evicted.

Reconstruction is a per-step operation

The previous chapter built a reconstruction for resumes: read the ledger, re-run the certifying checks, work out where you are. Everything about the step-nine failure says that a run twenty-two iterations deep is in the same epistemic position as a resumed run, minus the process death. Both are working from a plan they do not remember writing. The difference is that the resumed run knows it has forgotten, and the long run does not.

So the reconstruction moves to the head of every step. Before the model is asked to do step nine, the harness assembles a context from durable sources and hands it over as the working set, rather than letting the step inherit whatever the previous twenty-one iterations left in the window.

python
def build_step_context(ledger, step, git, budget=14_000):
    """Assemble the working set for one step from durable state only."""
    parts = []

    # 1. the task and its standing constraints — never summarised, never dropped
    parts.append(section("TASK", ledger["task"], ledger["invariants"]))

    # 2. what remains, so the step knows what it must not pre-empt
    remaining = [s for s in ledger["steps"] if s["status"] != "passed"]
    parts.append(section("REMAINING PLAN",
                         [f'{s["id"]}: {s["title"]}' for s in remaining]))

    # 3. what is already certified, one line each, with the check that certified it
    done = [f'{s["id"]}: {s["title"]} — certified by `{s["check"][-1]}` @ {s["evidence"]["tree"]}'
            for s in ledger["steps"] if s["status"] == "passed"]
    parts.append(section("CERTIFIED", done))

    # 4. the tree as it is right now, read fresh, not remembered
    parts.append(section("TREE",
        git("status --short"),
        git(f'log --oneline {ledger["base"]}..HEAD'),
        git(f'diff --stat {ledger["base"]}..HEAD')))

    # 5. this step, its check, and what went wrong last time
    parts.append(section("CURRENT STEP", step, step.get("last_failure")))

    # 6. the files this step is allowed to touch, current contents
    parts.append(section("FILES", read_paths(step["touches"], budget_left(parts, budget))))

    return "\n\n".join(parts)          # ~11.4k tokens steady state on this task

Six sections, and the fifth and sixth are the only ones that change size materially between steps. The first three come out of the ledger and cost around eight hundred tokens for a twelve-step plan. The tree section is three git commands, roughly four hundred tokens on a migration of this size. The bulk is the files, which are read from disk at the start of the step rather than recalled, so the model is never reasoning about a version of a file that has since changed under it.

The invariants list in section one is the field that would have prevented the step-nine failure, and it is worth being blunt about how it gets populated. It is a short list of standing constraints written when the plan is made and appended to when a step establishes something later steps must respect — "the variadic form of httpx.Do stays until s12", "internal/httpx is not deleted until every caller is migrated", "shipped tests under testdata/ are read-only". A model asked to produce this list at plan time produces a mediocre one, so the reliable version makes it part of the step-completion protocol: a step cannot be marked certified until the run has answered whether it introduced anything later steps must not undo. That is one extra question per step, and it converts a class of failure that is invisible until it detonates into a line of text.

What gets left out, and why that is the whole trick

The tempting addition to build_step_context is a compacted summary of the conversation so far, on the reasoning that more context cannot hurt. It hurts in a specific way: the summary is a lossy rendering of exactly the material the ledger holds losslessly, so including both means the window carries two accounts of the same events, one authoritative and one approximate, and the approximate one is longer. When they disagree — and they will, because a summary written at iteration fourteen describes a tree that has moved on — there is nothing in the prompt to say which wins.

The transcript itself is worse, for the arithmetic reason. Left in, the prompt at iteration twenty is around 190,000 tokens; assembled from durable state it is around 11,400 and does not grow with iteration count. Over twenty-two iterations that is the difference between a cost curve that is quadratic in steps and one that is linear, and it shows up in latency before it shows up on the bill, since time to first token scales with prompt length and a 190,000-token prompt makes every step feel slow.

Three strategies for keeping a long task inside its budget are usually on the table, and they are not interchangeable.

Automatic compactionRolling summaryAssembled from ledger
What is keptmodel's choice at compaction timesummariser's choice, every N turnsfields you specified, all of them
Determinismnone — same run compacts differentlynonesame inputs give the same context
Cost per stepgrows to the window, then resetsgrows between summariesflat, ~11k tokens
Fails byevicting a premise nobody flaggeddrift, as summaries summarise summariesomitting a field you never thought to record
Recovers a dead processnonoyes — same mechanism as resume
Effort to buildnone, it is in the harnesslowdesigning the schema and the write protocol

Automatic compaction is not a competitor so much as a backstop, and the Claude Cookbook's compaction notebook is a good place to see it working on a real multi-hour task. It handles the material the ledger does not model — a long tool output, an exploratory read of a file that turned out to be irrelevant — and it should be present underneath an assembled context, doing its job on whatever accumulates within a single step. What it must not be is the mechanism by which knowledge crosses from step two to step nine. Anthropic's write-up on effective harnesses for long-running agents makes the same division: the harness owns the durable state, and the model owns the reasoning inside a step.

Who writes the fields decides how much they are worth

Once the ledger is the working set, the question of who populates it stops being bookkeeping. There are two authors available and they should not have the same permissions.

The harness owns everything derived from execution. It runs the certifying check, so it writes status, the exit codes, the durations and the tree reference, and the model never gets a tool that sets a step to passed. Giving the model that tool reintroduces the original problem in a new place: a model that has spent four attempts on a stubborn step and is asked whether it is done is under exactly the pressure that produces an optimistic answer, and the whole point of a certifying check is that the answer comes from a command rather than a judgement.

The model owns the fields that require reading the situation — the step titles, the touches globs when the plan is revised, the hypothesis about a failure, the notes and invariants. Those are genuinely useful and genuinely unverifiable, which is why they are quarantined in fields that no policy reads as truth. A hypothesis feeds the next attempt's context; it never feeds the decision about whether a step succeeded.

The split has a pleasant consequence for the assembled context. Every line in the CERTIFIED section can name the command and exit code behind it, because a harness wrote it from an observed process result, so the model reading its own history at step nine is reading evidence rather than its own earlier optimism. A run that has been marking its own homework for twenty-one iterations produces a CERTIFIED section that is a chain of unverified claims, and assembling context from it is a more efficient way of being wrong.

The failure this design still has

Assembling context from the ledger means the run knows precisely what the ledger records and nothing else. That is a real loss and not a hypothetical one. At step three the model discovered that internal/api/session.go has a second, undocumented caller path reached through a reflection-based router, which is why a naive signature change breaks in a way the compiler does not catch. If that observation was not written into the invariants or the step's notes, it is gone at step nine as thoroughly as the variadic decision was, and the assembled context is not merely failing to carry it — it is confidently presenting a complete-looking picture that omits it.

Compaction at least leaves a summary that might contain the observation by accident. An assembled context contains what the schema has fields for, which makes the schema the thing that has to be right, and schemas are wrong in the direction of the things nobody anticipated. The practical mitigation is a free-text notes field per step with a hard cap, carried forward for certified steps, and an explicit prompt at step completion asking what a later step would need to know. It is unreliable in the way all model-authored metadata is unreliable, and it is still much better than nothing, because a wrong note is visible in the context while a missing premise is not.

There is a second thing the ledger cannot do, which is stand in for reading the code. Section six exists because the one-line summary "s05: move session middleware registration into auth.Register — certified" is enough to know that a thing happened and nowhere near enough to edit the file it happened in. A harness that hands the model only ledger entries produces a run that reasons fluently about a codebase it has not looked at, and the failure mode is edits that are plausible against the summary and wrong against the file.

Where this is heading is isolation rather than assembly. If a step's context can be built from durable state, the step does not need to run in the same conversation as its neighbours at all — it can run in a fresh one, in a subagent — a separate conversation the harness opens for a single step and collects a result from — returning only a ledger entry and a diff. That is the offload-and-isolate pattern the context engineering work of the past couple of years has converged on, and it makes the per-step context bound structural instead of a matter of discipline. It also makes the ledger's schema the entire interface between steps, which sharpens the limitation above rather than removing it.

Whichever way the context is built, the run can now go on for a long time without losing itself, and that is a mixed gift. The step-nine run reached iteration twenty-two and three hundred and eighty thousand tokens; with a flat per-step context it could reach fifty iterations without any of the symptoms that used to make a long run obviously unwell. Nothing built so far can tell you whether iteration twenty-two was worth running.

Long-horizon code tasks — read the current version at https://daviddominguez.dev/learn/en/long-horizon-code-tasks/the-ledger-is-working-memory-not-only-crash-recovery

Back to top