Context compaction is silently destroying your LLM agent's memory
Tl;dr: Long-session LLM agents lose governing rules, todos, and decisions when context compaction runs — and the loss is usually silent. I built a zero-dependency Python library (memory-anchor) that snapshots that state verbatim before compaction and re-injects it after, plus a CLI to audit how much a compaction actually destroyed.
The problem nobody measures
Run any agent long enough and it hits the context window. The fix everyone reaches for is compaction: old turns get summarized into a paragraph and the summary replaces them. Cheap summarizers — especially the fast flash models everyone uses to keep costs down — flatten detail. In my own agent's logs:
- a governing rule got paraphrased (the agent started behaving differently and nobody noticed why)
- a pending todo vanished ("what was I doing?")
- a decision's rationale was rewritten (settled questions got re-litigated)
- a verification path disappeared (half-verified work got reported as done)
That last one is the scary one. After a 35% compaction pass, my agent forgot it had delegated a subtask and never collected the result. Not a hallucination — a silent state loss.
Why existing memory systems don't cover this
The current memory stack (mem0, Letta, Hindsight, and friends) remembers facts — entities, preferences, retrieval chunks. None of them guarantee that the rules and work state governing this session survive a compaction byte-for-byte. Research-backed systems like MemGPT treat memory as a tiered store, and Claude's own compaction is a black box — you don't get to see what it threw away.
There are components everywhere, but no off-the-shelf, framework-agnostic answer. So I built one.
What it does
preserve(ctx) ─► manifest (4 lists, verbatim) ─► [summarizer runs] ─► recover(ctx) ─► recovery block injected at head of messages
Four small pieces, pure stdlib, zero dependencies:
-
StateManifest — rules / todos / decisions / progress, serialized to JSON, with incremental
merge(): done todos never resurrect, superseded decisions never reappear. - MemoryStore — atomic (tmp+rename) local JSON persistence, per-session indexed.
- RecoveryInjector — rebuilds the recovery block; L1 (immutable rules) is never trimmed.
- CompactableMemory — a two-line facade:
from pathlib import Path
from memory_anchor import CompactableMemory
mem = CompactableMemory(base_dir=Path(".memory"))
mem.preserve(ctx) # before compaction
messages = mem.recover(ctx, messages, summary) # after compaction
The part I'm most proud of: auditing compaction damage
v0.3 added cam judge — point it at your pre-compaction manifest and the summary that replaced it, and it classifies every item as verbatim (survived), paraphrased (semantics preserved), or lost. It's a CI gate, not a vibes check:
cam judge --before manifest.json --after summary.txt --min-verbatim 90
# exit code 1 if fewer than 90% of items survived byte-for-byte
The numbers from my own system are not flattering to the status quo. Running the same 8KB briefing through two compressors:
| compressor | verbatim survival | outcome |
|---|---|---|
| extractive (built-in) | ~60% | 2 governing rules silently lost |
| caveman (aggressive) | 100% | all items survived byte-for-byte |
The judge didn't just measure the damage — it exposed it. That extractive pass was silently eating rules every single run, and nothing in the pipeline was looking.
Try it
git clone https://github.com/44334433/memory-anchor
cd memory-anchor && pip install -e . && pytest # 31 tests
Or script it into your cron/CI without Python:
cam before my-session --rule "R1|never paraphrase governing rules|100" \
--todo "ship v0.2|pending|run the drill"
# ...compaction happens...
cam after my-session --messages messages.json --budget 2000
Honest limitations
- This is not a semantic memory system. It preserves work state you explicitly declare — it won't infer what matters on your behalf (yet).
-
judgeuses diff-based matching, not semantics. A truly reworded rule may count as "paraphrased" rather than "lost" — the threshold (45%) is a documented heuristic. - It's framework-agnostic by design: bring your own summarizer, hook the two calls wherever your pipeline compacts.
If your agent has ever "forgotten" something after compaction, you already know this pain. Anchor the state that must survive — and start measuring what your summarizer is destroying. Star/watch the repo if you want to see the framework adapters (LangChain, Claude Code, OpenHands) land — I'm holding off until real integrations ask for them.
Top comments (2)
Compaction gets dangerous when it preserves conclusions but drops provenance. For coding agents, "we decided X" is weaker than "we decided X because file Y behaved this way on date Z." Without the why and the source, the compressed memory can steer future code while becoming impossible to challenge.
Great point — that's exactly the failure mode I've been circling. Just pushed v0.3.1: decisions now carry
source+evidencefields alongsiderationale, andcam judgegrades provenance as part of the decision. The matched text is decision + rationale + source + evidence, so a summary that keeps "we decided X" but drops "because file Y behaved this way on date Z" scores below verbatim and fails the audit gate. The asymmetry you describe is real: a decision without its provenance isn't just weaker information, it becomes unquestionable — there's nothing left to challenge it with. Thanks for the feedback, this landed as a feature within hours.