context-fold
v0.3.0
Published
Deterministic, reversible in-session context compaction for the Pi coding agent — folds stale tool output out of the model's view, never out of the session.
Maintainers
Readme
context-fold
Deterministic, reversible context compaction for the Pi coding agent. Long agentic sessions stay under budget by folding stale content — mostly long chains of tool calls — out of the model's view. Every fold is reversible, indexed, and computed without a model call. The core is written to be harness-agnostic and can be adapted to other coding harnesses with some work.
Requirements: Node ≥ 22.19.0 and Pi ≥ 0.80.4.
pi install npm:context-foldThe idea
Context management is annoying, and I know plenty of people who are too lazy to summarize and hand
off to a new session. They let context grow unmanaged right up until they smash /compact. This
system came out of iterative research over various compaction methods, and it is an attempt at
economically optimizing context over a long session — eating as few cache-read hits as possible
until you decide to end the session or the work is done.
Why deterministic
The published evidence points the same way from several directions.
Deterministic masking of stale tool output matches or beats LLM summarization on agentic coding tasks, at equal or lower cost (The Complexity Trap, SWE-agent, Anthropic's context editing).
LLM summaries lose exactly what matters. File and identifier trails are the weakest-preserved category even in good production summarizers (Factory.ai). In one fixed-interval math experiment, 40.4 % of post-summary answer-state transitions went from correct to wrong — even though summarization was net positive overall (Self-Compacting Agents). A summary can also fabricate instructions that then become post-compaction "ground truth" (claude-code #46602).
For precise recall, retrieval over raw stored history beats an in-context summary by a wide margin (MemGPT, LongMemEval). But grep only finds what lexically matches (NoLiMa) — which is why every fold emits a deterministic index of exact tokens rather than a paraphrase.
The system in short
1. Per-turn: the fold ladder. Once usage crosses ~45 % of the context window, a
fold event masks stale tool_result and thinking blocks. User intent, assistant conclusions, and
the record of every action are never touched. Fresh tool results are always delivered in full at
least once.
2. The floor. Eventually no more tool calls can be masked. At that point context-fold says so rather than churning. What remains is the irreducible floor, and it cannot compress past it.
3. Hard compaction. Pi decides when this fires. By default (CONTEXTFOLD_COMPACT=det)
context-fold intercepts it and hands Pi a summary rendered verbatim from the session's seed index,
so Pi's LLM summarization never runs. CONTEXTFOLD_COMPACT=native opts back into Pi's stock
behavior.
At this stage the raw messages do leave live context — that is what compaction is. What survives is
the index, the spool, and Pi's session file, all on disk and all reachable through recall. The
loss is bounded and reversible rather than lossy and final. There is no paraphrase step and nothing
that can hallucinate. The extension warns you after a second forced compaction; it is worth running
a handoff well before that, at a definable task finish line.
4. Handoff. (manual, /fold-handoff) Writes a seed file for starting a fresh session: the
same verbatim index plus the goal you state.
On the thresholds. Every percentage above is a default, not a tuned constant. The right first-fold point trades the initial cache write against how many times you compact over a session, and I have not settled it. All of them are environment variables (see Configuration) and all of them may move in a future minor release.
What it does
Discrete fold events
Between fold events the context is append-only. Rewriting history invalidates the provider's prompt-cache suffix, so mutations are batched at points where that cost is paid once:
- First fold when usage crosses ~45 % of the context window (25 % when telemetry shows the session has never had a live cache read, since there is no warm prefix worth protecting).
- A fold event masks stale
tool_result/thinkingblocks outside the protected tail to their deterministic digests, keeping detected risk lines verbatim, and commits them as a frozen layer whose bytes never change again. The context head stays byte-identical turn over turn, which is what keeps prefix caches warm. - Each further event needs at least a ladder step (~12 % of the window) of maskable mass. Crossing
the absolute budget cap (
min(200k, 0.75 × window)) folds immediately.
Spool-backed recovery
Every committed ladder fold writes the exact masked block to a versioned, sha256-verified spool
envelope before the raw message can leave live history. The model sees a deterministic
{#code FOLDED} digest and can retrieve the original through recall or restore it through
unfold. This happens only when context pressure folds stale material; context-fold never hides a
fresh result before its first delivery.
The seed index
Every fold event appends one deterministic record to seed-index.jsonl in the session spool
directory (spec: docs/SEED_INDEX_SPEC.md): files touched, commands run, error lines in every
spelling the lexicon knows (lowercase failed, npm ERR!, …), exact identifiers and numbers
harvested from the masked output, first lines of user messages, and byte-extent spans into the
spool. Extraction is pure regex — same input, byte-identical output.
Getting detail back
recall search=<term>— one sweep over every folded block, with matching lines grouped by code. A detail lost somewhere behind N pointers costs one call, not N.recall <code>, with optionalgrep=<term>orlines=<a-b>— whole or partial retrieval, token-capped so a recall can never re-flood what folding saved.unfold <code>— sticky re-expansion. The block stays expanded and is never re-masked.
Recall works live, after resume, and after hard compaction: masked content resolves from the spool even once the raw message has left history.
Status and advisories
Measured prompt-cache telemetry (per-message cacheRead/cacheWrite) drives the advisories below,
in price-agnostic input-token equivalents — fee ratios are near-constant across vendors, with
cache read ≈ 0.1× input.
- Cold detection — an expected-warm turn that read zero cached tokens gets one stderr notice
with the re-billed size and a
/newsuggestion. /context-foldstatus — fold position (usage %, the next-fold gauge), cache hit ratios, and flags: folds committed but not observed on the wire, a second forced compaction, irreducible context past half the window, cold with a large carry, and recall churn. Advisory only; nothing blocks.- Footer status line (TUI) — a persistent one-line summary in Pi's footer (
⧉ context-fold ×3 · ~41k tok masked · next fold: 3.1k/9.6k maskable · cache avg 66%), updated as fold events fire. Purely visual: nothing is added to the transcript or the model's context, and headless modes are unaffected. The middle segment is the ladder's trigger gauge, showing whichever fold condition is actually binding — below the entry threshold it names it (next fold at 45% ctx); once usage is past the threshold, which is permanent from then on, it tracks maskable mass toward the next fold step (next fold: 3.1k/9.6k maskable, counting up from 0 right after a fold as new observations land).⚠ no more folds possible (over budget)appears only in the terminal state where the irreducible tail and roots exceed the budget.cache avgis the whole-session cache hit ratio, unlike Pi'sCH, which is the last turn only. - Fold cost accounting — once a fold event has fired, the status reports both sides: tokens masked per turn against tokens the provider re-prefilled because the fold moved the prefix, plus the running net. A fold rewrites history from the earliest masked block forward, so that re-prefill is a real cost this extension causes, and reporting only the savings would be dishonest accounting. It is charged to the single turn carrying the new bytes, because every later turn reads them back from cache. The cost side needs a provider that reports cache writes: Anthropic and Bedrock Converse do, while the Codex route reports cached reads only and Pi hardcodes Google's write to zero. Where writes are unreported the line says so instead of showing a zero — "nothing was rewritten" and "this provider never says" are different facts.
Guarantees
- History is never mutated. Folding exists only in the per-call outgoing copy; the session file keeps every raw payload.
- Nothing is destroyed. Ground truth lives in the session file and the spool. Every
{#code}handle resolves throughrecall/unfolduntil spool GC ages it out (default 14 days). - Tool pairs cannot orphan. Folding is in-place content substitution and never changes the
message count, so a
tool_callcan never lose itstool_result. Structural, not policed. - Failure signals survive compression at every fidelity level — the error lexicon is deliberately broad and any-case.
- No model is ever called. Folding, digests, compaction, and the handoff seed are all deterministic. Nothing this extension produces is a paraphrase.
- Fail-open, bounded blast radius. A defect costs one result's folding, one block's fidelity,
or one turn's folding — never the turn itself.
CONTEXTFOLD=0disables everything per session. - Deterministic core. The pure core has no clock, no randomness, and no I/O; all disk I/O lives in the adapter.
Limitations
- Token counts are estimates. The estimator is a uniform ~4-characters-per-token heuristic, not a per-model tokenizer, so every threshold in this document is approximate. It drives budget decisions well enough; do not read it as billing truth.
- Images are invisible to the budget math. A tool result carrying non-text parts (screenshots, rendered pages) is never folded, so nothing is ever lost — but its real token cost is not counted either. Image-heavy sessions read as further from the fold threshold than they are, so folding starts later than it should.
- The tool names are generic. The extension registers
recallandunfoldas global tools. If another extension registers the same names, one will shadow the other. - Spool GC judges other sessions by file age. The sweep at session start deletes sibling spool
directories whose newest file is older than the retention window. Live sessions refresh a
heartbeat file each turn, so an idle-but-running session is safe; a session whose process is
suspended for longer than the window can still lose its spool to a freshly started sibling.
CONTEXTFOLD_SPOOL_RETAIN_DAYS=0disables the sweep. - Primarily exercised against one model family. Development and testing have mostly used
gpt-5.6-solvia the openai-codex provider. Folding only reads Pi's usage numbers and message shapes, so other providers should work; fold cost accounting is the one feature with a known provider dependency, since it needs reported cache writes. The figures quoted above come from individual runs rather than a repeatable harness. - Folding changes what the model sees. A pointer is not the payload. Agents handle this well in
practice, since the teaching text explains the contract, but if you see an agent confused by a
{#code FOLDED}marker,CONTEXTFOLD=0turns everything off for a session. - Pre-1.0. The on-disk formats are versioned but not frozen. While the major version is
0, a change to fold timing or to the seed-index record shape is a minor bump, documented inCHANGELOG.md.
Known integrations
Findings from running context-fold beside other Pi extensions. The common theme: a fold can be committed and correct locally yet still be discarded or deferred downstream, which is why the extension watches provider usage for exactly that.
@howaboua/pi-codex-conversiondefers folds to user-turn boundaries. Its cached WebSocket continuation answers a mid-chain prefix change by sending only the pending tool output as a delta against the server-held previous response, so a fold's rewrite of older history stays local for the rest of that tool chain. At the next user message there is no pending tool output, the changed prefix forces a full resend, and provider-reported input drops all at once. Folding still works — recall, the spool, and compaction are unaffected — but a long autonomous tool chain can approach the provider's context limit before any fold takes effect on the wire.codex-litedoes not rewrite context. Its dialect mode replaces Pi's stock tools and appends prompt guidance. There is no fold bypass in that pairing; fresh shell output reaches the model before it can age into a ladder fold. Focused shell commands still reduce context growth.- Current Pi chains
contexttransforms as middleware. Pi 0.83.0 and 0.84.1 are verified. On older builds that predate transform chaining, context-fold must load after another context rewriter.session_before_compactstill selects one compaction result. Full versioned collision table indocs/pi-api-surface.md. - Do not load the package twice.
pi install npm:context-foldplus a-e npm:context-foldflag registersrecall/unfoldtwice and fails loudly at load with a tool-name conflict. Installed or-e, pick one.
The wire watchdog. Because every one of these failure modes is invisible in the extension's own
output, the telemetry checks the outcome instead: a fold that masked tokens strictly shrinks the
outgoing prompt, so if the next turn's provider usage reads the whole pre-fold prompt back from
cache, the rewrite provably never reached the wire. When that happens the extension warns once per
session on stderr and raises a flag in /context-fold and the footer status line.
Install
# Try it for one session, without installing:
pi -e npm:context-fold
# Install persistently:
pi install npm:context-foldFrom a clone, point Pi at the checkout instead: pi -e /path/to/context-fold.
Configuration
| Var | Default | Meaning |
|---|---|---|
| CONTEXTFOLD | (on) | Master kill switch: 0/off = the extension registers nothing this session. |
| CONTEXTFOLD_FOLD_AT | 0.45 | First fold when usage ≥ this fraction of the context window. |
| CONTEXTFOLD_FOLD_STEP | 0.12 | A fold event must save at least this fraction of the window (spaces events). |
| CONTEXTFOLD_COLD_FOLD_AT | 0.25 | First-fold threshold when no live cache read has ever been observed. |
| CONTEXTFOLD_BUDGET_FRACTION | 0.75 | Budget = this fraction of the context window… |
| CONTEXTFOLD_BUDGET_CAP | 200000 | …capped at this absolute ceiling (attention degrades at absolute depth). 0/off disables. |
| CONTEXTFOLD_TAIL | 20000 | Protected-tail target — the newest ~N tokens never fold (clamped to half the budget). |
| CONTEXTFOLD_COMPACT | det | Hard-compaction answer: det = deterministic seed-index summary; native = Pi stock. |
| CONTEXTFOLD_RECON_TOKENS | 18000 | Reconstruction estimate used by the reset flag (input-token equivalents). |
| CONTEXTFOLD_SPOOL_RETAIN_DAYS | 14 | Spool GC window at session start. 0/off = never delete. |
| CONTEXTFOLD_DEBUG | (off) | One-line fold/cache summary to stderr each turn. |
| CONTEXTFOLD_DUMP | (unset) | Debug/e2e seam: write each turn's outgoing (folded) view to this JSON path. |
Verification
npm install && npm run typecheck && npm test # unit + integration suitescripts/e2e-ladder.sh # live: fold event fires, index emitted, head byte-stable, buried value recalled
scripts/e2e-resume.sh # live: folds survive a session restartThe live scripts drive real Pi sessions against a real provider, so they cost money and need
provider auth plus python3. They load the working copy explicitly, so they test the checkout
rather than an installed build. Override the model with E2E_PROVIDER / E2E_MODEL. These are
liveness checks, not benchmarks — they assert that folding happens and survives, not how much it
saves.
Develop
npm install
npm run typecheck
npm testThe core (src/core/*) has zero harness dependencies; the Pi adapter (src/adapters/pi/*) owns
all I/O and hook wiring. Architecture notes are in DESIGN.md, the index format in
docs/SEED_INDEX_SPEC.md, and the Pi APIs this leans on in docs/pi-api-surface.md.
There is no build step: Pi loads the TypeScript source directly through jiti, so the package ships
src/ as-is and installs no dependencies of its own.
typebox and @earendil-works/pi-coding-agent are declared as optional peer dependencies. Pi
injects them at runtime — never bundle a copy.
Provenance & license
MIT. The pure core is ported from Accordion (pinned commit
0c22434), stripped of UI coupling and hardened since; the discrete fold ladder, seed index,
spool-backed recovery, and advisor layers are original to this project.
Much of this documentation was drafted with an LLM and edited by hand. The design decisions, thresholds, and measurements are mine.
