Skip to the content.

Context shedding

Who this is for: anyone running a long agent session (Claude Code, a coding agent, any loop that keeps talking to a model) who wants to know what fak’s default “trim the middle” saving actually is — in plain terms, and honestly.

The problem, in one picture

Every turn, your agent sends the entire conversation so far to the model. Turn 1’s message, turn 2’s, the tool output from turn 3, all of it — every time. The model has no memory between calls, so the transcript is the memory, and it gets re-sent from scratch on every turn.

So the cost of one turn is not “the new thing you just said.” It’s “everything that has ever been said, again.” A 200-turn session pays for its early turns two hundred times.

The provider softens this with a prompt cache: if the front of the request is byte-for-byte identical to last turn, the provider serves that prefix from its cache at roughly a tenth of the price. That helps a lot — but only for the unchanged front. The growing middle of the transcript keeps shifting as new turns land, so it keeps missing the cache and keeps costing full price, every turn, forever.

That middle is the money leak. Shedding is how fak plugs it.

What shedding does

When the resident history grows past a budget (48,000 tokens by default), fak’s compaction lever trims it back down. It keeps three things and drops one:

That’s the “trim from the middle out.” The head stays cached, your recent context stays intact, and the dead weight in the middle stops riding along on every future turn.

One compaction fire on the outbound request body: the protected prefix (through the first cache breakpoint) is copied byte-for-byte so its cache discount survives, the stale middle is dropped to a one-message stub carrying a restore handle, and the recent window is untouched — the "after" bar is visibly shorter than "before".

The same picture as a plain-text twin, so it reads even where the image doesn’t load:

COMPACTION: ONE FIRE, ON THE WIRE   [outbound request body]
legend: █ protected prefix (cache hit ~0.1x)  ▓ stale middle (cache miss, full price)  █ recent window (working set)  ░ stub + restore handle

before │███████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████████████████│ ~62,000 tok  (over the 48K budget -> fires)
 after │███████████████░██████████████████████                              │ ~34,600 tok  (prefix + window untouched)

The prefix band is copied by a byte splice (a memcpy, never a re-serialize), so the provider sees a prefix that is bytes-equal to last turn and keeps the cache discount on it. The stale middle collapses to that one-message stub; the recent window rides through unchanged.

The guarantees

The trim is built to fail safe, because a botched rewrite of the request body is worse than no trim at all:

Why this beats summarizing

Most agent products “compact” by asking the model to write a summary of the old turns and then throwing the originals away. That’s lossy and self-certified: the summary might drop the one file path, rejected approach, or “don’t touch X” constraint that matters ten turns later, and nothing can tell you it did. Worse, the summary is new bytes in the middle of the prompt — which can bust the very prompt cache you were trying to save. (The full case against summarize-and-discard is in the built-in compaction audit.)

Shedding is different in kind. It doesn’t rewrite history into prose — it drops a span and remembers where it went. The head the provider is caching is never rewritten, so the discount survives. And because the dropped span is kept addressable (not summarized into oblivion), it can be restored by reference rather than reconstructed from a lossy recap. Summarizing is guessing what mattered; shedding is deferring the question until something actually asks.

What survives a drop: the pin and the tombstone

“Drops a span and remembers where it went” is only honest if you can say what the stub remembers. Two things ride in it, depending on what was in the middle:

WHAT THE STUB CARRIES — the one message that stands in for the dropped middle

 a pinned goal is present    [fak:goal] keep the invoice parser backward-compatible   <- hoisted VERBATIM
 (the fidelity path)         [fak] compacted 12 earlier turn(s) ... detail omitted        (nothing lost)

 an unmarked first task      [fak] compacted 12 earlier turn(s) ... detail omitted
 (the oriented fallback)     [fak] originating task (compacted): id=9f3c...a1 "port the
                             Q4_K decode kernel to AVX2 and prove it bit-exact"
                                  |__ id is a callable sha256 handle:
                                      fak_context_restore(id=9f3c...a1) -> pages the full turn back, verbatim
                                      fak_context_spans()               -> lists what is still restorable

The goal pin. If a message is tagged [fak:goal] — a standing instruction like an acceptance criterion, a “keep X backward-compatible”, the actual task the loop is working toward — and it happens to fall in the compactible middle, fak hoists it out verbatim and sets it beside the stub instead of dropping it. A goal you’d lose to a badly-timed compaction survives in full. On the pin path the stub carries no tombstone: nothing was lost, so there’s nothing to leave a marker for.

The originating-task tombstone. The common case has no pin — the first user turn is the task, unmarked. Rather than launder it into a bare “compacted 12 turns,” fak leaves a bounded excerpt of that first turn (whitespace collapsed to one line, capped so it stays a low-volume, cache-untouched addition) plus a content-address handle. The excerpt says what was dropped; the handle says how to get all of it. You are reading a live instance of this right now: the top of this very session shows a [fak] originating task (compacted): id=… "…" line — that is the tombstone, doing its job.

Getting a dropped span back: restore

The handle in the tombstone is not decoration — it is callable. It is a sha256 content-address (the same addressing scheme fak uses across recall and its context planner), and two MCP tools resolve it:

Restore is trust-gated, not a blanket undo. A span that an operator sealed (quarantined) or explicitly tombstoned is refused, not resurrected; an unknown or already-evicted handle returns a clean miss. So “get it back” never becomes a way to launder poisoned or withdrawn context back into the prompt.

This is what turns the trim from a gamble into a deferral. You don’t have to be right about what was dead weight. Drop the middle; if a later turn reaches for something in it, the handle is sitting right there in the stub, and you page the span back in instead of thrashing on a lossy recap. The dropped context is deferred, not destroyed.

What it saves — and how to read the number honestly

Here is the honest part, because it’s easy to get wrong (we got it wrong once, in public, and corrected it — see the retraction below).

The real saving is per-turn recurrence. A token you shed from the middle is a token you would otherwise have re-sent and re-paid for on every remaining turn. Drop it once, and every future turn is a little cheaper. Over a long session those per-turn savings add up — that’s the genuine win, and it grows with how long the session runs and how much middle there is to trim.

But “tokens shed” is not the same as “distinct tokens saved.” Here’s the subtlety. fak doesn’t keep a compacted copy of the transcript — the client (Claude Code) re-sends its full, uncompacted history every turn, so fak re-trims from scratch each time compaction fires. If a session’s compaction fires seven times, the same aged middle turns get dropped — and counted — on all seven fires. The running total of “tokens shed” therefore re-counts the same tokens once per fire.

So a raw compaction_shed_tokens of, say, 747,000 on a single session does not mean fak removed 747,000 distinct tokens — a session whose live context never exceeds ~300,000 tokens can’t have 747,000 distinct tokens to remove. It means fak trimmed roughly 107,000 tokens per fire, seven times, as the middle kept regrowing and getting re-trimmed.

The honest way to state the win:

A retraction, on the record. An earlier version of the README and benchmark authority claimed fak’s own share “climbs from ~15% to ~75% as the session gets long,” citing a session that reported 75% by exactly the mistaken math above. A forensic audit found the numerator double-counted the re-trimmed middle across fires. We corrected it the same day. The lesson is baked into this page: shed per fire, never the session sum; never ratio a cumulative shed against a cumulative cache-read. Provenance and the code pointers are in BENCHMARK-AUTHORITY.md (the compaction row).

Was the shed content pure value, or did it hurt?

Shedding only saves money if the tokens you dropped were genuinely dead weight. If a later turn reaches for something in the shed span — re-reads a file whose content was dropped, cites a result that’s gone — the model can get confused, and the “saving” cost you a turn of thrashing instead.

This is the same shape as a classic operating-system question: when is it safe to free a page? The answer there is reference counting — you free a page when nothing live points at it anymore, and freeing one that’s still referenced is a use-after-free bug. Context shedding wants the same discipline: a span is safe to shed when no later turn still references it, and shedding a still-referenced span is the context equivalent of a use-after-free — the deterministic signature of the model getting confused.

That reference-counting view of context health — turning “was the model getting confused?” into a mechanical, checkable signal rather than a fuzzy judgment call — is where fak’s context work goes next. It’s tracked as its own line of work; this page is the what and why of the trim itself.

Try it

fak guard -- claude          # compaction is on by default at a 48K budget
fak cachevalue report        # the owner-attribution + per-fire compaction health

Tune or disable the trim with fak guard --compact-history-budget <tokens> (0 turns it off). Every fak info line shows the live provider X% + fak Y% split, so you can watch the two savings — the provider’s cache discount and fak’s own trim — side by side, honestly, without either one claiming the other’s number.

To keep a standing instruction safe across compactions, start the message that states it with [fak:goal] — the pin hoists it out verbatim rather than letting it fall into a dropped middle. And if a later turn needs a span that was shed, its handle is in the stub: call fak_context_restore(id=…) to page it back, or fak_context_spans to list what is restorable.

See also