pi-dynamic-context-pruning
A pi extension that dynamically prunes stale context to keep sessions lean.
Package details
Install pi-dynamic-context-pruning from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-dynamic-context-pruning- Package
pi-dynamic-context-pruning- Version
0.1.1- Published
- Jul 21, 2026
- Downloads
- 149/mo · 13/wk
- Author
- diegopetrucci
- License
- MIT
- Types
- extension
- Size
- 125.8 KB
- Dependencies
- 0 dependencies · 1 peer
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
dynamic-context-pruning
A pi extension that automatically prunes stale, duplicate, and superseded tool-call
content from long-running sessions, gated by a cache-aware net-benefit calculation
so pruning only happens when it's actually worth the prompt-cache cost. Includes a
manual /prune picker and a /context-pruning status/control command.
This is v1: deterministic, non-agentic strategies only (no model-invoked compression, no summarization). See Roadmap for what a v2 would add and why it hasn't been built yet.
Design
- Non-destructive. Nothing is ever deleted from the session file. A prune replaces a tool result's content (or a tool call's input arguments) with a short placeholder string; the original message stays in the transcript and can always be restored.
- Branch-aware. Prune decisions and restores are persisted as custom session
entries (
dynamic-context-pruning:decision,dynamic-context-pruning:stats,dynamic-context-pruning:restore) appended viapi.appendEntry. Onsession_startandsession_tree, the extension folds the current branch's entries back into in-memory state, so forking/switching branches correctly recomputes which decisions are active for that branch. - Tombstone-based restore. Restoring a pruned result doesn't delete the original decision entry — it appends a restore entry for that decision's idempotency key. The most recent entry (by chronological order) for a given key wins when replaying history, so a decision can be pruned, restored, and re-pruned any number of times.
- Recomputed every call, not on a timer. All three automatic strategies run
fresh on every
contextevent, over the current message array. Nothing is pruned "once and forgotten"; if a tool result would no longer qualify (e.g. it's now within the protected recency window), it isn't proposed again, but an already-applied, persisted decision still applies until explicitly restored.
Strategies
Three deterministic strategies propose prunes; each can be toggled independently.
1. Deduplication (dedupe)
If the same tool is called more than once with canonically identical arguments, every occurrence except the newest is proposed for pruning (its tool result content is replaced with a placeholder pointing at the newest call).
- Canonicalization: JSON keys are recursively sorted before hashing/comparing
(
{"b":1,"a":2}and{"a":2,"b":1}are the same key). Tool name comparison is case-insensitive. - Only completed tool calls (an assistant
toolCallblock with a matchingtoolResultmessage) are considered; in-flight calls are never touched. - The newest occurrence in a duplicate group is always kept in full.
2. Error-input purge (error-purge)
Once an errored tool call's input arguments are older than a configurable
number of turns, they're proposed for pruning (the call's arguments are
replaced, not the error message itself).
- Default:
minTurnsOld = 4— a call must be strictly older than 4 turns to qualify (eligible at 5+ turns elapsed, not at exactly 4). - Only applies to tool calls whose result was
isError: true. - Configurable per session via
strategy error-purge on|offand thestrategies.errorPurge.minTurnsOldconfig field.
3. Superseded file operations (superseded-file-ops)
When the same file path is read and/or written more than once, older
read/write/edit tool outputs for that path can become stale. This
strategy is deliberately conservative — it only proposes a prune when it can
prove the older output is stale, never on a guess:
- A full-file read (no
offset/limit) is superseded by a later full-file read of the same normalized path. - A partial read (has
offset/limit) is not superseded by a later partial read unless the later read's range fully covers the earlier one. If range coverage can't be reliably determined from the arguments (e.g. the earlier read has nolimit— reads to EOF — but the later read is bounded), the strategy skips it rather than guessing. - Any read of a path (full or partial) is superseded once a later, successful (non-error) write/edit to that path occurs — the old read is now known-stale regardless of range comparisons. An errored later write/edit does not count, since the file may be unchanged.
- Older write/edit outputs for a path are superseded by a later, successful write/edit to that same path. An errored later write/edit never supersedes.
The newest operation for a given path (by message order) is never
superseded, regardless of kind. Path extraction accepts path, file_path,
and filePath argument names, matched only against pi's built-in
read/write/edit tools; bash file writes are out of scope (this
strategy does not parse shell commands).
Protections
Before any strategy is allowed to propose a prune, several protections apply:
- Protected tool names (case-insensitive, never pruned):
todo,task,subagent,agent,skill,oracle,librarian,contrarian,code_reviewer,gnosis,triage_comments. - Protected path globs (never pruned if any string argument matches):
**/.env,**/.env.*,**/*.pem,**/*.key,**/id_rsa*,**/id_ed25519*,**/secrets/**,**/*.p12,**/*.pfx. - Recency window: the most recent
recentTurnsconversational turns (default4) are never touched by any automatic strategy, regardless of what they contain.
All three lists/values are configurable (see Configuration).
Savings accounting and the net-benefit gate
Every applied decision's estimated token savings are folded into cumulative,
per-strategy stats (/context-pruning stats), persisted as
dynamic-context-pruning:stats session entries.
Before a fresh, automatic prune proposal is actually applied, it must pass a
cache-aware net-benefit gate. The intuition: prompt caches are
prefix-matched, so pruning at message position p invalidates ("busts") the
cached prefix from p onward, exactly once. That's a one-time cost, offset by
a smaller recurring saving on every subsequent call (fewer tokens sent). The
gate models this as a break-even calculation:
penalty ≈ (1 - r) * tailTokensAfterEarliestChange
recurringSaving ≈ r * tokensRemoved
breakEvenCalls = penalty / recurringSaving
Where r is gate.cachedPriceRatio — the fraction of full price still paid
for cached tokens (default 0.1, i.e. cached tokens cost ~10% of fresh
tokens). breakEvenCalls is how many subsequent LLM calls are needed to
recoup the one-time cache-bust cost via the recurring saving. The gate accepts
a batch of candidates only if breakEvenCalls <= gate.breakEvenThreshold
(default 22, calibrated from the representative-corpus benchmark — see the
calibration note under Configuration and the
Roadmap evidence).
All candidates proposed in the same context call share one cache bust (since
a prompt cache is a single linear prefix), so they're evaluated and
accepted/rejected jointly against the earliest position any of them touches.
Manual prunes via /prune bypass the gate entirely — you already decided the
result isn't worth keeping, so there's no threshold to check.
Gate modes (/context-pruning gate <mode>):
on(default): reject batches above the threshold.off: bypass the gate entirely; no cost modelling, everything applies.always-apply: cost is still modelled (for stats/observability) but nothing is ever rejected.
Commands
/prune
Interactive picker (TUI/RPC) over every prunable tool result in the current branch:
/prune
- Lists active, pruned, and previously-restored tool results.
- Selecting an active result shows a cost preview (predicted cache-bust penalty vs. recurring saving) before you confirm the prune.
- Selecting a pruned result offers to restore it.
- Outside an interactive UI (no
ctx.hasUI), prints a read-only report of prunable results instead.
/context-pruning
/context-pruning status
/context-pruning stats
/context-pruning on
/context-pruning off
/context-pruning toggle
/context-pruning strategy <dedupe|error-purge|superseded-file-ops> on|off
/context-pruning gate <on|off|always-apply>
status: enabled state, gate mode/threshold/ratio, per-strategy on/off state, protection counts, last call's raw/effective/saved token snapshot, and current context usage.stats: cumulative tokens saved, overall and per strategy.on/off/toggle: enable or disable the extension entirely.strategy <name> on|off: toggle one strategy independently.gate <mode>: change the net-benefit gate mode at runtime.
Config changes made via /context-pruning are persisted to disk (see
Configuration) and also applied in-memory immediately, even
if the write fails.
Configuration
Config is stored at:
<pi agent dir>/extensions/dynamic-context-pruning.json
It's read once on session_start and rewritten whenever /context-pruning
mutates it. A missing or unparseable file falls back to defaults; the file is
never required to exist. Full shape, with defaults:
{
"enabled": true,
"protections": {
"toolNames": ["todo", "task", "subagent", "agent", "skill", "oracle", "librarian", "contrarian", "code_reviewer", "gnosis", "triage_comments"],
"pathGlobs": ["**/.env", "**/.env.*", "**/*.pem", "**/*.key", "**/id_rsa*", "**/id_ed25519*", "**/secrets/**", "**/*.p12", "**/*.pfx"],
"recentTurns": 4
},
"thresholds": {
"minCharsSaved": 200
},
"strategies": {
"dedupe": { "enabled": true },
"errorPurge": { "enabled": true, "minTurnsOld": 4 },
"supersededFileOps": { "enabled": true }
},
"gate": {
"mode": "on",
"cachedPriceRatio": 0.1,
"breakEvenThreshold": 22,
"breakEvenThresholdByState": { "idle": 22, "mid_loop": 22 }
}
}
Notes:
thresholds.minCharsSavedis reserved for a future minimum-size filter and is not currently enforced by the pipeline; the net-benefit gate's break-even math is what actually decides whether a prune applies.gate.breakEvenThresholdByStateletsidlevsmid_loopagent states use different thresholds; both default togate.breakEvenThreshold. Real mid-loop/idle detection isn't wired through yet, so this always evaluates asidlein v1 — the field exists so the seam is ready once that lands. The representative-corpus benchmark (see Roadmap) showsmid_loopcandidates carry ~all realized net benefit andidlecandidates carry ~none at r=0.1, which argues for a stricteridledefault — but since every live evaluation runs asidletoday, defaulting it stricter would silently disable most automatic pruning. Both states are intentionally kept at parity until real agent-state detection lands; that's a follow-up ticket.gate.breakEvenThreshold's default of22is calibrated from the representative-corpus benchmark atcachedPriceRatior=0.1 (aggressive prompt caching, the common case) — see Roadmap for the full evidence and its ratio-sensitivity caveats. It is provider/ratio- dependent: the optimal threshold rises as caching gets weaker (T=29 at r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same corpus).
Prompt-cache trade-off
Pruning trades a smaller stream of tokens sent on every future call against a one-time cache-bust cost, because prompt caches match on a strict prefix: any change to the message stream busts the cached prefix from that point onward for the next call. This is exactly why the net-benefit gate exists rather than pruning unconditionally — a prune that happens too late in a session (few calls left to amortize the one-time cost over) or removes too few tokens relative to the tail it invalidates can cost more than it saves. The gate's break-even math (above) is the mechanism that keeps pruning net-positive on average rather than accepting the cache trade-off unconditionally.
gate.cachedPriceRatio (r) should track your provider's real prompt-cache
pricing:
- Anthropic prompt caching: cached reads ~0.1x base input price, plus a
~1.25x one-time cache-write premium →
r ≈ 0.1. - Google Gemini context caching: ~0.25x base input price, plus a
separate storage/time fee →
r ≈ 0.25. - OpenAI automatic prompt caching: ~0.5x base input price →
r ≈ 0.5. - No caching available or enabled →
r → 1: the cache-bust penalty vanishes, so pruning is a pure win regardless of threshold.
Provider pricing drifts — check current price sheets before trusting these ratios. The penalty math also assumes the cache would still be warm at the next call; if the provider's cache TTL (e.g. ~5 min default on Anthropic) elapses between calls, the bust costs nothing and pruning is again a pure win.
Benchmark harness
A standalone, read-only offline benchmark replays real (or fixture) pi
session .jsonl files through the extension's exported strategy/pipeline/cost
model helpers — it never writes back to a session file or changes runtime
behavior.
node extensions/dynamic-context-pruning/scripts/benchmark.mjs [paths...] [options]
Options:
--limit N: only process the first N session files found.--ratio R: cached-price ratio(s) to model; repeatable and/or comma-separated (e.g.--ratio 0.1,0.2). Default:0.1.--json: emit a full machine-readable JSON dump instead of aligned text.--help: print usage.
With no positional paths, it defaults to every *.jsonl file found
recursively under ~/.pi/agent/sessions.
Corpus choice matters.
~/.pi/agent/sessions(the default) tends to be dominated by short orchestrator/delegation sessions, which structurally under-report candidates and skew the remaining-calls-after-position distribution low. The representative-corpus numbers in Roadmap instead come from~/.the-last-harness/agent/sessions— the harness's own longer, tool-heavy agent sessions — which is the corpus to point the harness at when re-deriving or sanity-checking the gate default.
For every point in a session where an LLM call would have happened (approximated as immediately before each assistant message), the harness reports two kinds of numbers:
- Predicted: what the strategies would propose right now, and what the cache-aware cost model (penalty / recurring saving / break-even calls) estimates, without knowing what happens next in the session.
- Realized: since replay knows the session's actual future, the harness
also reports how many subsequent LLM calls actually followed a candidate
prune point, and the actual net benefit that prune would have produced —
realizedNetBenefit = actualRemainingCalls * recurringSaving - penalty.
It also sweeps the break-even threshold T from 1 to 30 and reports the T
that maximizes total realized net benefit across all gate-eligible
candidates, both overall and split by idle/mid-loop agent state.
Comparison
- opencode-dynamic-context-pruning ("pi-dcp" upstream):
the OpenCode plugin this extension takes its name and general problem space
from. It ships a model-invoked
compresstool (agentic, nested-summary compression), autonomous nudges, manual mode, and a full/dcpcommand surface. This extension's v1 implements none of that agentic layer — only the deterministic automatic strategies and cache-aware gate, which is the subset OpenCode DCP calls its own automatic dedup/error-purge strategies. See Roadmap for the full parity gap and what would be needed to close it. - context-cap / context-inspector:
siblings in this repo that address adjacent but different problems.
context-capshrinks the effective model context window so pi's own auto-compaction fires earlier; it doesn't remove or alter any content.context-inspectoris a read-only dashboard that shows where context is going; it doesn't modify anything either.dynamic-context-pruningis the only one of the three that actually removes/replaces content from what gets sent to the model.
Roadmap
docs/v2-design.md is the authoritative v2 design
spec: full OpenCode DCP parity, meaning a model-invoked compress tool,
nested block summaries, autonomous nudges, manual mode, and the full /dcp-
equivalent command surface (decompress/recompress, prompt overrides,
notifications).
v2 proceeds only if benchmark evidence supports it. The design doc's
benchmark decision gate requires a meaningfully positive aggregate realized
net benefit, at sufficient candidate volume, with enough remaining-calls
runway per session to plausibly amortize a compression summary's own token
cost — see docs/v2-design.md §4 for the exact go/no-go bar.
The representative-corpus run of the benchmark harness (pe-c5n9, over
~/.the-last-harness/agent/sessions: 1,390+ session files, 556 gate-eligible
candidates — see the corpus-choice note above) produced:
Candidates and tokens removed by strategy:
dedupe111 candidates / 93,330 tokens removed;error-purge187 / 28,191;superseded-file-ops258 / 247,474.Remaining-calls-after-position distribution: p50 = 47, p90 = 156 — far more amortization runway than the earlier small-corpus run showed, because this corpus is dominated by long, tool-heavy agent sessions rather than short orchestrator sessions.
Hindsight-optimal break-even threshold
T(maximizes total realized net benefit), by cached-price ratior, split overall / mid_loop / idle:ratio roverall T(benefit)mid_loop T(benefit)idle T(benefit)0.1 T=22 (~20.6k) T=22 (~20.6k) T=1 (0 — zero benefit) 0.25 T=29 (~1.54M) T=29 (~1.54M) T=12 (~4.0k) 0.5 T=54 (~8.06M) T=54 (~8.06M) T=27 (~37.2k) 0.9 T=58 (~24.7M) T=58 (~24.2M) T=21 (~459k)
Reading: mid_loop candidates carry essentially all of the realized
benefit; idle candidates carry essentially none (literally zero at r=0.1).
At r=0.1 (aggressive prompt caching, the common Anthropic case) the total
realized benefit across the whole 1,390-session corpus is only ~20.6k
token-units — economically marginal, on the order of pennies. Savings only
become material at weaker caching, r>=0.25.
This reframes, rather than weakens, the case for v2. Small deterministic
removals (dedupe/error-purge/superseded-file-ops) structurally cannot beat
the cache-bust penalty at r=0.1 — there just isn't enough single-message
token volume in them. If pruning is going to matter economically under
aggressive caching, it has to come from large removals: agentic range
compression and nested summaries (the v2 compress tool), which remove
orders of magnitude more tokens per operation than these deterministic
strategies do. So this evidence argues for prototyping v2 (contingent on
summary quality/hardening work), not against it — v1's mechanical strategies
remain useful as a free, always-on baseline, but they were never going to be
where the real savings live.
Install
Standalone npm package
pi install npm:pi-dynamic-context-pruning
Collection package
pi install npm:@diegopetrucci/pi-extensions
GitHub package
pi install git:github.com/diegopetrucci/pi-extensions
Then reload pi:
/reload
Notes
- This extension's package name is intentionally unscoped (
pi-dynamic-context-pruning, not@diegopetrucci/pi-dynamic-context-pruning), a deliberate deviation from this repo's usual package-naming convention. - Token counts throughout (
status,stats, the benchmark harness) are estimates from a simple chars/4-style heuristic, not exact provider token counts.