pi-dynamic-context-pruning 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi-fleet-tested-version +1 -0
- package/README.md +435 -0
- package/index.ts +2546 -0
- package/package.json +34 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.80.3
|
package/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# dynamic-context-pruning
|
|
2
|
+
|
|
3
|
+
A pi extension that automatically prunes stale, duplicate, and superseded tool-call
|
|
4
|
+
content from long-running sessions, gated by a cache-aware net-benefit calculation
|
|
5
|
+
so pruning only happens when it's actually worth the prompt-cache cost. Includes a
|
|
6
|
+
manual `/prune` picker and a `/context-pruning` status/control command.
|
|
7
|
+
|
|
8
|
+
This is v1: deterministic, non-agentic strategies only (no model-invoked
|
|
9
|
+
compression, no summarization). See [Roadmap](#roadmap) for what a v2 would add
|
|
10
|
+
and why it hasn't been built yet.
|
|
11
|
+
|
|
12
|
+
## Design
|
|
13
|
+
|
|
14
|
+
- **Non-destructive.** Nothing is ever deleted from the session file. A prune
|
|
15
|
+
replaces a tool result's content (or a tool call's input arguments) with a
|
|
16
|
+
short placeholder string; the original message stays in the transcript and can
|
|
17
|
+
always be restored.
|
|
18
|
+
- **Branch-aware.** Prune decisions and restores are persisted as custom session
|
|
19
|
+
entries (`dynamic-context-pruning:decision`, `dynamic-context-pruning:stats`,
|
|
20
|
+
`dynamic-context-pruning:restore`) appended via `pi.appendEntry`. On
|
|
21
|
+
`session_start` and `session_tree`, the extension folds the current branch's
|
|
22
|
+
entries back into in-memory state, so forking/switching branches correctly
|
|
23
|
+
recomputes which decisions are active for that branch.
|
|
24
|
+
- **Tombstone-based restore.** Restoring a pruned result doesn't delete the
|
|
25
|
+
original decision entry — it appends a restore entry for that decision's
|
|
26
|
+
idempotency key. The most recent entry (by chronological order) for a given
|
|
27
|
+
key wins when replaying history, so a decision can be pruned, restored, and
|
|
28
|
+
re-pruned any number of times.
|
|
29
|
+
- **Recomputed every call, not on a timer.** All three automatic strategies run
|
|
30
|
+
fresh on every `context` event, over the current message array. Nothing is
|
|
31
|
+
pruned "once and forgotten"; if a tool result would no longer qualify (e.g.
|
|
32
|
+
it's now within the protected recency window), it isn't proposed again, but
|
|
33
|
+
an already-applied, persisted decision still applies until explicitly
|
|
34
|
+
restored.
|
|
35
|
+
|
|
36
|
+
## Strategies
|
|
37
|
+
|
|
38
|
+
Three deterministic strategies propose prunes; each can be toggled independently.
|
|
39
|
+
|
|
40
|
+
### 1. Deduplication (`dedupe`)
|
|
41
|
+
|
|
42
|
+
If the same tool is called more than once with **canonically identical
|
|
43
|
+
arguments**, every occurrence except the newest is proposed for pruning (its
|
|
44
|
+
tool result content is replaced with a placeholder pointing at the newest
|
|
45
|
+
call).
|
|
46
|
+
|
|
47
|
+
- Canonicalization: JSON keys are recursively sorted before hashing/comparing
|
|
48
|
+
(`{"b":1,"a":2}` and `{"a":2,"b":1}` are the same key). Tool name comparison
|
|
49
|
+
is case-insensitive.
|
|
50
|
+
- Only *completed* tool calls (an assistant `toolCall` block with a matching
|
|
51
|
+
`toolResult` message) are considered; in-flight calls are never touched.
|
|
52
|
+
- The **newest** occurrence in a duplicate group is always kept in full.
|
|
53
|
+
|
|
54
|
+
### 2. Error-input purge (`error-purge`)
|
|
55
|
+
|
|
56
|
+
Once an errored tool call's *input arguments* are older than a configurable
|
|
57
|
+
number of turns, they're proposed for pruning (the call's `arguments` are
|
|
58
|
+
replaced, not the error message itself).
|
|
59
|
+
|
|
60
|
+
- Default: `minTurnsOld = 4` — a call must be **strictly older** than 4 turns
|
|
61
|
+
to qualify (eligible at 5+ turns elapsed, not at exactly 4).
|
|
62
|
+
- Only applies to tool calls whose result was `isError: true`.
|
|
63
|
+
- Configurable per session via `strategy error-purge on|off` and the
|
|
64
|
+
`strategies.errorPurge.minTurnsOld` config field.
|
|
65
|
+
|
|
66
|
+
### 3. Superseded file operations (`superseded-file-ops`)
|
|
67
|
+
|
|
68
|
+
When the same file path is read and/or written more than once, older
|
|
69
|
+
`read`/`write`/`edit` tool *outputs* for that path can become stale. This
|
|
70
|
+
strategy is deliberately conservative — it only proposes a prune when it can
|
|
71
|
+
prove the older output is stale, never on a guess:
|
|
72
|
+
|
|
73
|
+
1. A **full-file read** (no `offset`/`limit`) is superseded by a **later**
|
|
74
|
+
full-file read of the same normalized path.
|
|
75
|
+
2. A **partial read** (has `offset`/`limit`) is *not* superseded by a later
|
|
76
|
+
partial read unless the later read's range fully covers the earlier one.
|
|
77
|
+
If range coverage can't be reliably determined from the arguments (e.g. the
|
|
78
|
+
earlier read has no `limit` — reads to EOF — but the later read is
|
|
79
|
+
bounded), the strategy skips it rather than guessing.
|
|
80
|
+
3. **Any** read of a path (full or partial) is superseded once a **later,
|
|
81
|
+
successful** (non-error) write/edit to that path occurs — the old read is
|
|
82
|
+
now known-stale regardless of range comparisons. An errored later
|
|
83
|
+
write/edit does **not** count, since the file may be unchanged.
|
|
84
|
+
4. Older write/edit outputs for a path are superseded by a **later,
|
|
85
|
+
successful** write/edit to that same path. An errored later write/edit
|
|
86
|
+
never supersedes.
|
|
87
|
+
|
|
88
|
+
The newest operation for a given path (by message order) is never
|
|
89
|
+
superseded, regardless of kind. Path extraction accepts `path`, `file_path`,
|
|
90
|
+
and `filePath` argument names, matched only against pi's built-in
|
|
91
|
+
`read`/`write`/`edit` tools; `bash` file writes are out of scope (this
|
|
92
|
+
strategy does not parse shell commands).
|
|
93
|
+
|
|
94
|
+
## Protections
|
|
95
|
+
|
|
96
|
+
Before any strategy is allowed to propose a prune, several protections apply:
|
|
97
|
+
|
|
98
|
+
- **Protected tool names** (case-insensitive, never pruned): `todo`, `task`,
|
|
99
|
+
`subagent`, `agent`, `skill`, `oracle`, `librarian`, `contrarian`,
|
|
100
|
+
`code_reviewer`, `gnosis`, `triage_comments`.
|
|
101
|
+
- **Protected path globs** (never pruned if any string argument matches):
|
|
102
|
+
`**/.env`, `**/.env.*`, `**/*.pem`, `**/*.key`, `**/id_rsa*`,
|
|
103
|
+
`**/id_ed25519*`, `**/secrets/**`, `**/*.p12`, `**/*.pfx`.
|
|
104
|
+
- **Recency window**: the most recent `recentTurns` conversational turns
|
|
105
|
+
(default `4`) are never touched by any automatic strategy, regardless of
|
|
106
|
+
what they contain.
|
|
107
|
+
|
|
108
|
+
All three lists/values are configurable (see [Configuration](#configuration)).
|
|
109
|
+
|
|
110
|
+
## Savings accounting and the net-benefit gate
|
|
111
|
+
|
|
112
|
+
Every applied decision's estimated token savings are folded into cumulative,
|
|
113
|
+
per-strategy stats (`/context-pruning stats`), persisted as
|
|
114
|
+
`dynamic-context-pruning:stats` session entries.
|
|
115
|
+
|
|
116
|
+
Before a *fresh, automatic* prune proposal is actually applied, it must pass a
|
|
117
|
+
**cache-aware net-benefit gate**. The intuition: prompt caches are
|
|
118
|
+
prefix-matched, so pruning at message position `p` invalidates ("busts") the
|
|
119
|
+
cached prefix from `p` onward, exactly once. That's a one-time cost, offset by
|
|
120
|
+
a smaller recurring saving on every subsequent call (fewer tokens sent). The
|
|
121
|
+
gate models this as a break-even calculation:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
penalty ≈ (1 - r) * tailTokensAfterEarliestChange
|
|
125
|
+
recurringSaving ≈ r * tokensRemoved
|
|
126
|
+
breakEvenCalls = penalty / recurringSaving
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Where `r` is `gate.cachedPriceRatio` — the fraction of full price still paid
|
|
130
|
+
for cached tokens (default `0.1`, i.e. cached tokens cost ~10% of fresh
|
|
131
|
+
tokens). `breakEvenCalls` is how many subsequent LLM calls are needed to
|
|
132
|
+
recoup the one-time cache-bust cost via the recurring saving. The gate accepts
|
|
133
|
+
a batch of candidates only if `breakEvenCalls <= gate.breakEvenThreshold`
|
|
134
|
+
(default `22`, calibrated from the representative-corpus benchmark — see the
|
|
135
|
+
calibration note under [Configuration](#configuration) and the
|
|
136
|
+
[Roadmap](#roadmap) evidence).
|
|
137
|
+
|
|
138
|
+
All candidates proposed in the same `context` call share one cache bust (since
|
|
139
|
+
a prompt cache is a single linear prefix), so they're evaluated and
|
|
140
|
+
accepted/rejected jointly against the earliest position any of them touches.
|
|
141
|
+
|
|
142
|
+
Manual prunes via `/prune` bypass the gate entirely — you already decided the
|
|
143
|
+
result isn't worth keeping, so there's no threshold to check.
|
|
144
|
+
|
|
145
|
+
Gate modes (`/context-pruning gate <mode>`):
|
|
146
|
+
|
|
147
|
+
- `on` (default): reject batches above the threshold.
|
|
148
|
+
- `off`: bypass the gate entirely; no cost modelling, everything applies.
|
|
149
|
+
- `always-apply`: cost is still modelled (for stats/observability) but nothing
|
|
150
|
+
is ever rejected.
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
### `/prune`
|
|
155
|
+
|
|
156
|
+
Interactive picker (TUI/RPC) over every prunable tool result in the current
|
|
157
|
+
branch:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
/prune
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Lists active, pruned, and previously-restored tool results.
|
|
164
|
+
- Selecting an active result shows a cost preview (predicted cache-bust
|
|
165
|
+
penalty vs. recurring saving) before you confirm the prune.
|
|
166
|
+
- Selecting a pruned result offers to restore it.
|
|
167
|
+
- Outside an interactive UI (no `ctx.hasUI`), prints a read-only report of
|
|
168
|
+
prunable results instead.
|
|
169
|
+
|
|
170
|
+
### `/context-pruning`
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
/context-pruning status
|
|
174
|
+
/context-pruning stats
|
|
175
|
+
/context-pruning on
|
|
176
|
+
/context-pruning off
|
|
177
|
+
/context-pruning toggle
|
|
178
|
+
/context-pruning strategy <dedupe|error-purge|superseded-file-ops> on|off
|
|
179
|
+
/context-pruning gate <on|off|always-apply>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- `status`: enabled state, gate mode/threshold/ratio, per-strategy on/off
|
|
183
|
+
state, protection counts, last call's raw/effective/saved token snapshot,
|
|
184
|
+
and current context usage.
|
|
185
|
+
- `stats`: cumulative tokens saved, overall and per strategy.
|
|
186
|
+
- `on` / `off` / `toggle`: enable or disable the extension entirely.
|
|
187
|
+
- `strategy <name> on|off`: toggle one strategy independently.
|
|
188
|
+
- `gate <mode>`: change the net-benefit gate mode at runtime.
|
|
189
|
+
|
|
190
|
+
Config changes made via `/context-pruning` are persisted to disk (see
|
|
191
|
+
[Configuration](#configuration)) and also applied in-memory immediately, even
|
|
192
|
+
if the write fails.
|
|
193
|
+
|
|
194
|
+
## Configuration
|
|
195
|
+
|
|
196
|
+
Config is stored at:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
<pi agent dir>/extensions/dynamic-context-pruning.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
It's read once on `session_start` and rewritten whenever `/context-pruning`
|
|
203
|
+
mutates it. A missing or unparseable file falls back to defaults; the file is
|
|
204
|
+
never required to exist. Full shape, with defaults:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"enabled": true,
|
|
209
|
+
"protections": {
|
|
210
|
+
"toolNames": ["todo", "task", "subagent", "agent", "skill", "oracle", "librarian", "contrarian", "code_reviewer", "gnosis", "triage_comments"],
|
|
211
|
+
"pathGlobs": ["**/.env", "**/.env.*", "**/*.pem", "**/*.key", "**/id_rsa*", "**/id_ed25519*", "**/secrets/**", "**/*.p12", "**/*.pfx"],
|
|
212
|
+
"recentTurns": 4
|
|
213
|
+
},
|
|
214
|
+
"thresholds": {
|
|
215
|
+
"minCharsSaved": 200
|
|
216
|
+
},
|
|
217
|
+
"strategies": {
|
|
218
|
+
"dedupe": { "enabled": true },
|
|
219
|
+
"errorPurge": { "enabled": true, "minTurnsOld": 4 },
|
|
220
|
+
"supersededFileOps": { "enabled": true }
|
|
221
|
+
},
|
|
222
|
+
"gate": {
|
|
223
|
+
"mode": "on",
|
|
224
|
+
"cachedPriceRatio": 0.1,
|
|
225
|
+
"breakEvenThreshold": 22,
|
|
226
|
+
"breakEvenThresholdByState": { "idle": 22, "mid_loop": 22 }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Notes:
|
|
232
|
+
|
|
233
|
+
- `thresholds.minCharsSaved` is reserved for a future minimum-size filter and
|
|
234
|
+
is **not** currently enforced by the pipeline; the net-benefit gate's
|
|
235
|
+
break-even math is what actually decides whether a prune applies.
|
|
236
|
+
- `gate.breakEvenThresholdByState` lets `idle` vs `mid_loop` agent states use
|
|
237
|
+
different thresholds; both default to `gate.breakEvenThreshold`. Real
|
|
238
|
+
mid-loop/idle detection isn't wired through yet, so this always evaluates as
|
|
239
|
+
`idle` in v1 — the field exists so the seam is ready once that lands. The
|
|
240
|
+
representative-corpus benchmark (see [Roadmap](#roadmap)) shows `mid_loop`
|
|
241
|
+
candidates carry ~all realized net benefit and `idle` candidates carry ~none
|
|
242
|
+
at r=0.1, which argues for a stricter `idle` default — but since every live
|
|
243
|
+
evaluation runs as `idle` today, defaulting it stricter would silently
|
|
244
|
+
disable most automatic pruning. Both states are intentionally kept at
|
|
245
|
+
parity until real agent-state detection lands; that's a follow-up ticket.
|
|
246
|
+
- `gate.breakEvenThreshold`'s default of `22` is calibrated from the
|
|
247
|
+
representative-corpus benchmark at `cachedPriceRatio` r=0.1 (aggressive
|
|
248
|
+
prompt caching, the common case) — see [Roadmap](#roadmap) for the full
|
|
249
|
+
evidence and its ratio-sensitivity caveats. It is **provider/ratio-
|
|
250
|
+
dependent**: the optimal threshold rises as caching gets weaker (T=29 at
|
|
251
|
+
r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same corpus).
|
|
252
|
+
|
|
253
|
+
## Prompt-cache trade-off
|
|
254
|
+
|
|
255
|
+
Pruning trades a smaller stream of tokens sent on every future call against a
|
|
256
|
+
one-time cache-bust cost, because prompt caches match on a strict prefix: any
|
|
257
|
+
change to the message stream busts the cached prefix from that point onward
|
|
258
|
+
for the next call. This is exactly why the net-benefit gate exists rather than
|
|
259
|
+
pruning unconditionally — a prune that happens too late in a session (few
|
|
260
|
+
calls left to amortize the one-time cost over) or removes too few tokens
|
|
261
|
+
relative to the tail it invalidates can cost *more* than it saves. The gate's
|
|
262
|
+
break-even math (above) is the mechanism that keeps pruning net-positive on
|
|
263
|
+
average rather than accepting the cache trade-off unconditionally.
|
|
264
|
+
|
|
265
|
+
`gate.cachedPriceRatio` (`r`) should track your provider's real prompt-cache
|
|
266
|
+
pricing:
|
|
267
|
+
|
|
268
|
+
- **Anthropic** prompt caching: cached reads ~0.1x base input price, plus a
|
|
269
|
+
~1.25x one-time cache-write premium → `r ≈ 0.1`.
|
|
270
|
+
- **Google Gemini** context caching: ~0.25x base input price, plus a
|
|
271
|
+
separate storage/time fee → `r ≈ 0.25`.
|
|
272
|
+
- **OpenAI** automatic prompt caching: ~0.5x base input price → `r ≈ 0.5`.
|
|
273
|
+
- **No caching** available or enabled → `r → 1`: the cache-bust penalty
|
|
274
|
+
vanishes, so pruning is a pure win regardless of threshold.
|
|
275
|
+
|
|
276
|
+
Provider pricing drifts — check current price sheets before trusting these
|
|
277
|
+
ratios. The penalty math also assumes the cache would still be warm at the
|
|
278
|
+
next call; if the provider's cache TTL (e.g. ~5 min default on Anthropic)
|
|
279
|
+
elapses between calls, the bust costs nothing and pruning is again a pure
|
|
280
|
+
win.
|
|
281
|
+
|
|
282
|
+
## Benchmark harness
|
|
283
|
+
|
|
284
|
+
A standalone, read-only offline benchmark replays real (or fixture) pi
|
|
285
|
+
session `.jsonl` files through the extension's exported strategy/pipeline/cost
|
|
286
|
+
model helpers — it never writes back to a session file or changes runtime
|
|
287
|
+
behavior.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
node extensions/dynamic-context-pruning/scripts/benchmark.mjs [paths...] [options]
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Options:
|
|
294
|
+
|
|
295
|
+
- `--limit N`: only process the first N session files found.
|
|
296
|
+
- `--ratio R`: cached-price ratio(s) to model; repeatable and/or
|
|
297
|
+
comma-separated (e.g. `--ratio 0.1,0.2`). Default: `0.1`.
|
|
298
|
+
- `--json`: emit a full machine-readable JSON dump instead of aligned text.
|
|
299
|
+
- `--help`: print usage.
|
|
300
|
+
|
|
301
|
+
With no positional paths, it defaults to every `*.jsonl` file found
|
|
302
|
+
recursively under `~/.pi/agent/sessions`.
|
|
303
|
+
|
|
304
|
+
> **Corpus choice matters.** `~/.pi/agent/sessions` (the default) tends to be
|
|
305
|
+
> dominated by short orchestrator/delegation sessions, which structurally
|
|
306
|
+
> under-report candidates and skew the remaining-calls-after-position
|
|
307
|
+
> distribution low. The representative-corpus numbers in [Roadmap](#roadmap)
|
|
308
|
+
> instead come from `~/.the-last-harness/agent/sessions` — the harness's own
|
|
309
|
+
> longer, tool-heavy agent sessions — which is the corpus to point the
|
|
310
|
+
> harness at when re-deriving or sanity-checking the gate default.
|
|
311
|
+
|
|
312
|
+
For every point in a session where an LLM call would have happened
|
|
313
|
+
(approximated as immediately before each assistant message), the harness
|
|
314
|
+
reports two kinds of numbers:
|
|
315
|
+
|
|
316
|
+
- **Predicted**: what the strategies would propose right now, and what the
|
|
317
|
+
cache-aware cost model (penalty / recurring saving / break-even calls)
|
|
318
|
+
estimates, without knowing what happens next in the session.
|
|
319
|
+
- **Realized**: since replay knows the session's actual future, the harness
|
|
320
|
+
also reports how many subsequent LLM calls actually followed a candidate
|
|
321
|
+
prune point, and the *actual* net benefit that prune would have produced —
|
|
322
|
+
`realizedNetBenefit = actualRemainingCalls * recurringSaving - penalty`.
|
|
323
|
+
|
|
324
|
+
It also sweeps the break-even threshold `T` from 1 to 30 and reports the `T`
|
|
325
|
+
that maximizes total realized net benefit across all gate-eligible
|
|
326
|
+
candidates, both overall and split by idle/mid-loop agent state.
|
|
327
|
+
|
|
328
|
+
## Comparison
|
|
329
|
+
|
|
330
|
+
- **[opencode-dynamic-context-pruning](https://github.com/sculptdotfun/opencode-dynamic-context-pruning) ("pi-dcp" upstream)**:
|
|
331
|
+
the OpenCode plugin this extension takes its name and general problem space
|
|
332
|
+
from. It ships a model-invoked `compress` tool (agentic, nested-summary
|
|
333
|
+
compression), autonomous nudges, manual mode, and a full `/dcp` command
|
|
334
|
+
surface. This extension's v1 implements none of that agentic layer — only
|
|
335
|
+
the deterministic automatic strategies and cache-aware gate, which is the
|
|
336
|
+
subset OpenCode DCP calls its own automatic dedup/error-purge strategies.
|
|
337
|
+
See [Roadmap](#roadmap) for the full parity gap and what would be needed to
|
|
338
|
+
close it.
|
|
339
|
+
- **[context-cap](../context-cap)** / **[context-inspector](../context-inspector)**:
|
|
340
|
+
siblings in this repo that address adjacent but different problems.
|
|
341
|
+
`context-cap` shrinks the *effective* model context window so pi's own
|
|
342
|
+
auto-compaction fires earlier; it doesn't remove or alter any content.
|
|
343
|
+
`context-inspector` is a read-only dashboard that shows where context is
|
|
344
|
+
going; it doesn't modify anything either. `dynamic-context-pruning` is the
|
|
345
|
+
only one of the three that actually removes/replaces content from what gets
|
|
346
|
+
sent to the model.
|
|
347
|
+
|
|
348
|
+
## Roadmap
|
|
349
|
+
|
|
350
|
+
[`docs/v2-design.md`](./docs/v2-design.md) is the authoritative v2 design
|
|
351
|
+
spec: full OpenCode DCP parity, meaning a model-invoked `compress` tool,
|
|
352
|
+
nested block summaries, autonomous nudges, manual mode, and the full `/dcp`-
|
|
353
|
+
equivalent command surface (decompress/recompress, prompt overrides,
|
|
354
|
+
notifications).
|
|
355
|
+
|
|
356
|
+
**v2 proceeds only if benchmark evidence supports it.** The design doc's
|
|
357
|
+
benchmark decision gate requires a meaningfully positive aggregate realized
|
|
358
|
+
net benefit, at sufficient candidate volume, with enough remaining-calls
|
|
359
|
+
runway per session to plausibly amortize a compression summary's own token
|
|
360
|
+
cost — see `docs/v2-design.md` §4 for the exact go/no-go bar.
|
|
361
|
+
|
|
362
|
+
The representative-corpus run of the benchmark harness (`pe-c5n9`, over
|
|
363
|
+
`~/.the-last-harness/agent/sessions`: 1,390+ session files, 556 gate-eligible
|
|
364
|
+
candidates — see the corpus-choice note above) produced:
|
|
365
|
+
|
|
366
|
+
- **Candidates and tokens removed by strategy**: `dedupe` 111 candidates /
|
|
367
|
+
93,330 tokens removed; `error-purge` 187 / 28,191; `superseded-file-ops`
|
|
368
|
+
258 / 247,474.
|
|
369
|
+
- **Remaining-calls-after-position distribution: p50 = 47, p90 = 156** — far
|
|
370
|
+
more amortization runway than the earlier small-corpus run showed, because
|
|
371
|
+
this corpus is dominated by long, tool-heavy agent sessions rather than
|
|
372
|
+
short orchestrator sessions.
|
|
373
|
+
- **Hindsight-optimal break-even threshold `T`** (maximizes total *realized*
|
|
374
|
+
net benefit), by cached-price ratio `r`, split overall / mid_loop / idle:
|
|
375
|
+
|
|
376
|
+
| ratio `r` | overall `T` (benefit) | mid_loop `T` (benefit) | idle `T` (benefit) |
|
|
377
|
+
| --- | --- | --- | --- |
|
|
378
|
+
| 0.1 | T=22 (~20.6k) | T=22 (~20.6k) | T=1 (0 — zero benefit) |
|
|
379
|
+
| 0.25 | T=29 (~1.54M) | T=29 (~1.54M) | T=12 (~4.0k) |
|
|
380
|
+
| 0.5 | T=54 (~8.06M) | T=54 (~8.06M) | T=27 (~37.2k) |
|
|
381
|
+
| 0.9 | T=58 (~24.7M) | T=58 (~24.2M) | T=21 (~459k) |
|
|
382
|
+
|
|
383
|
+
**Reading**: `mid_loop` candidates carry essentially all of the realized
|
|
384
|
+
benefit; `idle` candidates carry essentially none (literally zero at r=0.1).
|
|
385
|
+
At r=0.1 (aggressive prompt caching, the common Anthropic case) the total
|
|
386
|
+
realized benefit across the whole 1,390-session corpus is only ~20.6k
|
|
387
|
+
token-units — economically marginal, on the order of pennies. Savings only
|
|
388
|
+
become material at weaker caching, r>=0.25.
|
|
389
|
+
|
|
390
|
+
**This reframes, rather than weakens, the case for v2.** Small deterministic
|
|
391
|
+
removals (dedupe/error-purge/superseded-file-ops) structurally cannot beat
|
|
392
|
+
the cache-bust penalty at r=0.1 — there just isn't enough single-message
|
|
393
|
+
token volume in them. If pruning is going to matter economically under
|
|
394
|
+
aggressive caching, it has to come from *large* removals: agentic range
|
|
395
|
+
compression and nested summaries (the v2 `compress` tool), which remove
|
|
396
|
+
orders of magnitude more tokens per operation than these deterministic
|
|
397
|
+
strategies do. So this evidence argues *for* prototyping v2 (contingent on
|
|
398
|
+
summary quality/hardening work), not against it — v1's mechanical strategies
|
|
399
|
+
remain useful as a free, always-on baseline, but they were never going to be
|
|
400
|
+
where the real savings live.
|
|
401
|
+
|
|
402
|
+
## Install
|
|
403
|
+
|
|
404
|
+
### Standalone npm package
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pi install npm:pi-dynamic-context-pruning
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Collection package
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### GitHub package
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Then reload pi:
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
/reload
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Notes
|
|
429
|
+
|
|
430
|
+
- This extension's package name is intentionally unscoped (`pi-dynamic-context-pruning`,
|
|
431
|
+
not `@diegopetrucci/pi-dynamic-context-pruning`), a deliberate deviation from
|
|
432
|
+
this repo's usual package-naming convention.
|
|
433
|
+
- Token counts throughout (`status`, `stats`, the benchmark harness) are
|
|
434
|
+
estimates from a simple chars/4-style heuristic, not exact provider token
|
|
435
|
+
counts.
|