context-fold
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.
Package details
Install context-fold from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:context-fold- Package
context-fold- Version
0.5.1- Published
- Sep 8, 2026
- Downloads
- 723/mo · 117/wk
- Author
- middlewatch
- License
- MIT
- Types
- extension
- Size
- 284.1 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
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. Built for Pi and supported on Pi only.
Requirements: Node ≥ 22.19.0 and Pi ≥ 0.80.4.
pi install npm:context-fold
The 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
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). This 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 compaction removes the raw messages from live context. What survives is the index
and Pi's session file, both on disk and both reachable through recall_folded. 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. Consider running a handoff
well before that, at a 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. Interactively it then offers, behind one
confirmation, to start the replacement session directly. The seed becomes the first user
message and the new session opens idle, spending nothing until you type. Decline (or run
headless) and the flow stays write, review, /new, paste.
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.
Ledger-backed recovery
Pi's session file is append-only, so every raw payload stays in it for the life of the session,
through hard compaction and resume. A committed ladder fold therefore records the masked block's
identity and a sha256 of its exact bytes rather than copying the payload. Recall re-locates the
original in the session ledger and verifies it against that sha before serving it. The model sees a
deterministic {#code FOLDED} digest and can retrieve the original through recall_folded 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 under
<sessionDir>/context-fold/<sessionId>/ (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 recovery spans naming each folded block's ledger anchor, extent, and fold-time
sha256. Extraction uses pure regex and produces byte-identical output for the same input.
Starting fresh with the seed index
/new starts an empty session. It does not import the previous session's seed index or fold
handles. To carry indexed context forward, run this in the old session:
/fold-handoff <goal for the next session>
The command writes <sessionDir>/context-fold/<sessionId>/handoff-<sessionId>.md and offers to
open a replacement session with that seed as its first user message. Accept, then type your next
instruction. Creating the seed and opening the session make no model request.
To review the seed first, decline the switch, open the file at the printed path, then run /new
and paste its contents or ask the new agent to read that path. If you already ran /new without
making a seed, use /resume to return to the old session and run /fold-handoff there.
The seed contains a bounded rendering of existing fold and compaction records, so recent work
that has not been indexed may be absent. Include the next task in the goal. The complete index
remains in seed-index.jsonl beside the seed. For a saved session, raw content remains in its
parent session file, whose path the seed includes when Pi provides it. Old fold codes do not
resolve through the new session's recall_folded or unfold; read the parent files directly or
resume the parent session to use those handles.
Getting detail back
recall_folded search=<term>: one sweep over every folded block, with matching lines grouped by code. Finding a detail lost behind N pointers takes one call rather than N.recall_folded <code>, with optionalgrep=<term>orlines=<a-b>: whole or partial retrieval, token-capped so a recall can never re-flood what folding saved. When the tool recorded its own full-output file (a truncated bash result), grep and line reads answer from that file, so recall reaches even the bytes truncation dropped before folding ever saw them.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 Pi's append-only session file even once the raw message has left live context.
Status and advisories
- Cache inactivity warning: in interactive Pi, a session carrying at least 20k tokens warns
on resume or when its configured idle interval expires:
cache may have expired: ~85k tok may rebill. Consider /fold-handoff. The clock starts at the last successful response for the selected provider and model, restored from the active session branch. Typing, tool activity, and failed responses do not refresh it. Each idle interval gets one notice. A model with no recorded activity instead sayscache unverified. The token count is an estimate, including after compaction; elapsed time does not prove cache expiry or guarantee that the provider will bill the whole context again. Hosts that cannot estimate the retained context skip prediction while its size is unknown. - Observed cold input: after the agent settles, a final response with zero cached reads and
at least 20k input tokens can trigger
session cold: rebilled ~40k tok as fresh input. Consider /fold-handoff. Detection requires at least two responses observed since the extension loaded and excludes the first response after a fold. The notice appears at most once per cold streak and reports input already processed. Interactive notices use Pi's renderer; headless notices use stderr. /context-foldmenu: choose Status, Settings, or Discard retained images. Escape or Close dismisses it. Status shows fold position (usage %, the next-fold gauge), cache hit ratios, and advisory 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. See Configuration for settings.- 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. The line is purely visual. It adds nothing to the transcript or the model's context and leaves headless modes 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, because "nothing was rewritten" and "this provider never says" are different facts.
For continuity when starting fresh, use the seed handoff workflow.
Optional send confirmation. Enable Confirm potentially cold prompts in
/context-fold → Settings to offer Keep draft or Send anyway before an interactive prompt
reaches the provider. Escape keeps the draft. A model change during confirmation also keeps the
draft and requires another submission. The same inactivity and 20k-token thresholds
apply. Automation, RPC, and prompts queued during streaming bypass this confirmation.
An advisory or selector failure also lets input proceed, while an explicit cancellation consumes
the input without sending it.
Cancelled text returns to the editor, including pasted-image file paths. Structured images
remain in memory for the next interactive prompt in the same session, even if you edit the text.
A notice lists the retained image count. To clear them, choose Discard retained images in
/context-fold.
Session navigation, reload, and shutdown discard them. The extension never resets a session or
submits a handoff automatically.
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 is Pi's session file, and the extension writes no copy of
it. Every
{#code}handle resolves throughrecall_folded/unfoldfor as long as the session file exists, verified against a sha256 recorded at fold time. - Tool pairs cannot orphan. Folding is in-place content substitution and never changes the
message count, so a
tool_callcan never lose itstool_result. The structure guarantees this without a separate check. - 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 global. The extension registers
recall_foldedandunfoldas global tools. If another extension registers the same names, one will shadow the other (unfoldis the generic one;recall_foldedwas named to avoid this collision). - 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
A fold can be committed and correct locally yet still be discarded or deferred by another Pi extension. Context-fold watches provider usage for that outcome. Known interactions follow.
@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 fold records, 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_folded/unfoldtwice and fails loudly at load with a tool-name conflict. Choose either the installed copy or-e.
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-fold
From a clone, point Pi at the checkout instead: pi -e /path/to/context-fold.
Configuration
Run /context-fold and choose Settings to see every knob below (except the debug seams and
the kill switch), with its effective value and where it came from. Edits persist to
<agent dir>/context-fold.json (normally ~/.pi/agent/context-fold.json) and, where marked live,
apply to the running session immediately. Already-frozen folds keep their bytes, and new values
steer future folds only. Direct shortcuts still work: /context-fold status, /context-fold config
(or settings), and /context-fold discard-images. Headless use skips the menu and keeps the
status path. The config shortcut uses an effective-settings listing.
Precedence per knob: built-in default < saved settings file < environment variable. An env var keeps working exactly as before and shadows the saved value for that session; the menu flags the shadowing when it applies.
| 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_CACHE_IDLE_MINUTES |
30 |
Cache inactivity warning threshold; off or 0 disables prediction and send confirmation. |
CONTEXTFOLD_CONFIRM_COLD_PROMPT |
off |
on enables interactive pre-request confirmation. |
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. |
The Cache inactivity warning (minutes) menu row names the selected Pi provider. Edits to that row save a provider-specific override. Unconfigured providers use the fallback above. This setting controls warnings rather than claiming how long a provider retains its cache. Investigate the retention policy for your provider, model, and request settings, then tune the value accordingly.
The saved JSON supports a global fallback and provider overrides, for example:
{
"cacheIdleMinutes": 30,
"providerCacheIdleMinutes": {
"openai": 30,
"my-provider": 10
},
"confirmColdPrompt": "off"
}
Provider keys are Pi provider IDs. The environment timeout overrides every saved provider value.
Choosing default in the provider's menu row removes only that provider's override. Menu edits
apply immediately; direct JSON edits take effect when the extension reloads.
Verification
npm install && npm run typecheck && npm test # unit + integration suite
scripts/e2e-ladder.sh # live: fold event fires, index emitted, head byte-stable, buried value recalled
scripts/e2e-resume.sh # live: folds survive a session restart
scripts/e2e-compact-resume.sh # live: folds survive real hard compaction plus a restart, recalled from the session ledger
With Pi and tmux installed, bash scripts/check-cache-warning.sh checks cancellation, edited
resubmission, and image preservation through the provider boundary in a 48-column Pi terminal.
It uses a local throwing fixture provider, makes no network request, and prints the directory
containing its screen captures.
The 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
liveness checks assert that folding happens and survives. They do not benchmark savings.
Develop
npm install
npm run typecheck
npm test
The core (src/core/*) has zero harness dependencies, and the Pi adapter (src/adapters/pi/*)
owns all I/O and hook wiring. DESIGN.md describes the architecture,
docs/SEED_INDEX_SPEC.md defines the index format, and docs/pi-api-surface.md lists the Pi APIs
that the extension uses.
Pi loads the TypeScript source directly through jiti, so there is no build step. 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. Use the
copies Pi injects at runtime rather than bundling them.
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,
ledger-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.