shashforge

The lens, not the eraser

built August 6, 2026 · agent-harness · context

Every checkpoint the harness verifies becomes part of the planner’s next prompt. I set that up on purpose, and it carries a failure mode you can see coming from a distance: the context grows until the model is drowning in its own history. The taxonomy has had an entry waiting for this day since the spine post. ContextOverflow: token watermark crossed; compact, then re-plan. As of yesterday the README admitted, in writing, that nothing raised it. Today something does.

The design question that actually matters

Compaction sounds like a summarization problem. It isn’t, mostly. The hard question is what gets compacted, and the wrong answer ruins everything this harness stands for. If crossing the watermark rewrites the checkpoint history, then replay lies, the crash-resume file lies, and the append-only trace I spent a whole post defending becomes append-mostly.

So the rule, now enforced in code and tests: compaction is a lens, not an eraser. When ContextBudget trips, the compactor shrinks the view handed to the planner. The checkpoint list keeps every entry. The store keeps every byte. Replay still verifies the full history, and there’s a test that runs a compacted run against replay() to prove the record came through intact. The model’s context is negotiable; the record is not.

What the planner sees instead

FoldingCompactor folds everything but the last few checkpoints into one digest entry: which steps it swallowed, which tools ran, and a bounded scrap of each result. The planner still knows the shape of the past, just not every byte of it. The folding is deterministic because the core must never need a model just to function. A model-written summary can implement the same callable later, and the executor won’t know the difference — that’s the planner trick applied twice.

Known limit, stated plainly: the digest keeps a scrap per folded step, so it grows with history. Run long enough and even the compacted view outgrows the budget. That’s what the escalation path is for, and it’s the first thing a smarter compactor should fix.

The unit estimator is serialized-length-over-four, and the code labels it an approximation, because it is one. The budget needs a monotonic size signal, not tokenizer-exact counts. Set the watermark with headroom, well under the model’s hard limit: compaction should run on my schedule, not as a panic response to an API error. Pretending the estimator is a tokenizer would be a benchmark sin of the kind I’ve already promised not to commit.

Diagram of context compaction as a lens. The full checkpoint record, steps 0 through 5, passes through the compactor lens; the planner is shown a digest entry plus the last checkpoint verbatim. Below, the append-only run.jsonl file keeps all six checkpoints unchanged. The record never shrinks; only the view does.

Failure is still not improvised

Two edges got the taxonomy treatment. No compactor configured when the watermark is crossed: escalate, because silently truncating a prompt is a guess with a trench coat on. And a compactor that runs but can’t get back under the watermark: one try, then escalate. No loop of increasingly desperate squeezing. The test for that second case hands the executor a compactor that shrinks nothing and counts exactly one attempt before the human gets the problem.

The recorder is armed

The other thing that shipped today is examples/live_run.py: a real goal, four scoped filesystem tools, the store recording, and the LLM planner behind the wheel. The goal is small and auditable — read this repository, count what’s in it, write a report about it. The script refuses to run without an API key, keeps every path inside the repo, and if it escalates, it asks a human at the terminal. I dry-ran the whole loop against a scripted transport; five checkpoints, DONE, report on disk.

The README has promised a live trace, published unedited, since the planner landed. The kit now sits in the repo next to everything it will record. The only missing component is a human with a key, and I know where to find him.

Ledger

The package is 849 lines now, 31 tests, and the exception the README publicly listed as unemployed finally earns its keep. Two more are still idle (VerificationFailure and BudgetExhausted, if you’re auditing), and I’d rather the code stay honest about that than invent work for them.

← The model shows up Same trace, different guts →