@yeyudeh/pi-compaction-files
Caps Pi's compaction file lists (<read-files>/<modified-files>): drops dead, duplicate and temp paths, hides sensitive ones, strips blocks the summarizer re-seeds into the narrative, and keeps the cost per turn constant.
Package details
Install @yeyudeh/pi-compaction-files from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@yeyudeh/pi-compaction-files- Package
@yeyudeh/pi-compaction-files- Version
0.3.0- Published
- Oct 20, 2026
- Downloads
- 568/mo · 568/wk
- Author
- yeyudeh
- License
- MIT
- Types
- extension
- Size
- 101.9 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-compaction-files
Pi's compaction appends two unbounded file lists to every summary and re-inherits them forever. This extension caps them, and the cost per turn stops growing.
Measured on one long-running real session (30k+ entries, 112 compactions):
| file lists in context | cost per model call | |
|---|---|---|
| Pi default | 110,924 B / 1,930 paths (57% of the summary) | ≈27,700 tokens |
| with this extension | 1,594 B / 38 paths (4.8%) | ≈399 tokens, constant |
Pi's own docs describe the growth as intended behaviour
(docs/compaction.md → "Cumulative File Tracking"):
Default compaction and branch summarization track files cumulatively. […] Compaction also carries file lists from the previous Pi-generated compaction. […] File tracking therefore accumulates across default compactions and nested default branch summaries. Pi does not automatically carry file lists from extension-generated summaries whose
fromHookfield istrue; extensions manage their owndetailsformat.
That last sentence is the documented contract this extension uses. It is not a monkey patch.
中文文档见 README.zh.md。
Install
pi install npm:@yeyudeh/pi-compaction-files
# or straight from git — this one tracks `main`
pi install git:github.com/yeyudeH/pi-compaction-files
Try it for a single run without installing:
pi -e npm:@yeyudeh/pi-compaction-files
Both forms were verified end to end on Pi 0.37: the extension loads and logs
self-check ok on stderr. Pin a git release with
pi install git:github.com/yeyudeH/pi-compaction-files#v0.2.0.
Then check it is loaded:
/filelist
You should see something like:
enabled=true style=native keepModified=60 keepRead=40 dropMissing=true
archiveSensitive=false tool=false indexRecent=12 failStreak=0/2
exclude=[/tmp/, /dev/shm/, /node_modules/, /.cache/, /proc/, /sys/]
cache=0 streamMode=[]
pi-self-check=ok dist=/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/dist
ok: compaction.js: fromHook inheritance guard present
ok: session-manager.js: fromHook persisted on compaction entries
ok: compaction/utils.js + compaction.js: still emits read-files/modified-files blocks
ok: compaction.js: details field still carried
当前清单: read 12 / modified 31
The extension takes effect on the next Pi start, because extensions load at process startup. A compaction summary that is already in your session is rewritten in memory on every turn (nothing is written to the session file), so an existing bloated session shrinks immediately.
What it does
1. Shortens the lists at the source
session_before_compact calls Pi's own compact() — so the summary text is exactly what
Pi would have produced — and then replaces the file blocks with shorter ones. Because the
result comes from a hook, Pi records fromHook: true and stops inheriting the previous
lists. The snowball breaks at the source instead of being re-trimmed every turn.
2. Strips every complete block, not just the trailing one
Pi appends the real block at the end. But the summarizer can also reproduce an earlier block inside the narrative. Observed in a real session:
offset 29181 1594 B 38 paths ← previous compaction's block, copied into the prose
offset 33349 75 B 1 path ← the real block Pi appended
A "take the last match" implementation leaves the first one in context on every turn, and it grows again. This extension removes all complete blocks and merges their paths into one capped list. Prose that merely mentions the tags is untouched: the regex requires the tag alone on its own line plus a matching close tag. Measured across 112 real compactions: 141 narrative mentions, 0 swallowed.
3. Filters the paths
- drop paths that no longer exist on disk (
dropMissing) - drop temp noise:
/tmp/,/dev/shm/,/node_modules/,/.cache/,/proc/,/sys/ - dedupe the same path written relative and absolute (Pi stores both)
- count and hide paths matching
sensitivePatterns - keep the
keepModified/keepReadmost recently touched, newest first
4. Keeps sensitive paths out of the session file too
Pi writes the full lists into the compaction entry's details, which it never renders
into context. Hiding a path from the prompt does not remove it from the session file on
disk. In one real session, 1,643 of 140,048 archived paths (1.17%, 21 distinct) matched
the sensitive patterns — credential-ish file names, agent config files, screenshots in a
private folder.
Since 0.2.0 the archive is filtered as well (archiveSensitive: false, the default). Set
it to true if you want the full inventory kept on disk so the session_files tool can
search it.
Existing entries are never rewritten — the filter applies to new compactions.
Configuration
Optional file: ~/.pi/pi-compaction-files.json
{
"enabled": true,
"style": "native",
"tool": false,
"keepModified": 60,
"keepRead": 40,
"dropMissing": true,
"indexRecent": 12,
"archiveSensitive": false,
"compactThinking": "medium",
"outputBudgetRatio": 0.8,
"outputHardCapRatio": 0.9,
"retryOnOverflow": true,
"summaryMaxTokens": 0,
"summaryHardTrim": false,
"excludePatterns": ["/tmp/", "/dev/shm/", "/node_modules/", "/.cache/", "/proc/", "/sys/"],
"sensitivePatterns": ["/credential", "/secret", "/token", ".key", "/password", "apikey", "/.pi/agent/settings.json", "/.pi/agent/models.json"],
"piDistDir": null
}
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Master switch. PI_FILELIST_DISABLE=1 also disables at runtime. |
style |
native |
native re-emits Pi's own <read-files> / <modified-files> blocks, shorter. index replaces them with a <session_files> index. |
tool |
false |
Register the session_files lookup tool. Only useful with style: "index". |
keepModified |
60 |
How many modified paths stay in context (newest first). |
keepRead |
40 |
How many read paths stay in context. |
indexRecent |
12 |
index style only: how many paths the index shows. Without this cap an index is longer than native (measured +506 chars at kept=38); with it, 854 chars shorter. If the index still would not be shorter, the extension falls back to native for that summary. |
dropMissing |
true |
Drop paths that no longer exist. |
archiveSensitive |
false |
false also strips sensitive-shaped paths from the on-disk details archive. |
excludePatterns |
see above | Substrings whose paths are dropped entirely. |
compactThinking |
medium |
Thinking level for the summarisation request itself instead of the session's. |
outputBudgetRatio |
0.8 |
Share of Pi's own output ceiling (0.8 * reserveTokens) that the whole summarisation reply — reasoning plus summary — should fit inside. 0 disables. |
outputHardCapRatio |
0.9 |
Lower the ceiling the inference server enforces for this request to this share of Pi's cap, so reasoning+summary cannot reach it. 0 leaves Pi's ceiling untouched. |
retryOnOverflow |
true |
If the reply used more than that budget, redo it once with tighter instructions and one step lower thinking; keep whichever was cheaper. |
summaryMaxTokens |
0 |
Optional second ceiling on the summary text alone (chars/4). 0 = off; outputBudgetRatio is the primary control. |
summaryHardTrim |
false |
When the summary is still over the ceiling after the lists are tightened, cut the prose too (default: tighten lists and warn). |
sensitivePatterns |
see above | Substrings whose paths are counted but never listed. Add your own private directories here. |
piDistDir |
auto | Only needed if the self-check cannot find Pi's dist/. PI_CODING_AGENT_DIST works too. |
Results
Against Pi's default (measured on 101 real native compaction entries)
Growth fitted on the real data: +1,108 B per compaction, unbounded.
| file-list bytes | tokens per call | |
|---|---|---|
| Pi default, at the point this extension was installed | 110,924 B | 27,731 |
| Pi default, projected 40 compactions later | 155,234 B | 38,809 |
| this extension | 1,594 B | 399 |
The list is capped by keepModified + keepRead, so it does not depend on how many
compactions you have had.
0.1.0 vs 0.2.0 (same real data, side by side)
| compaction | input | 0.1.0 output | 0.2.0 output | blocks left: 0.1.0 → 0.2.0 |
|---|---|---|---|---|
| 5 of the last 6 | — | identical | identical | 1→1, 2→2, 1→1, 1→1, 1→1 |
| the one with a re-seeded block | 33,424 | 33,424 | 33,281 | 3 → 1 |
Five of six inputs were already clean, so there is no regression. The sixth is the case 0.2.0 exists for.
Projection with the observed parameters (native +1,108 B/step; re-seeded block 1,594 B, observed in 1 of 11 hook-produced compactions):
| +N compactions | Pi default | 0.1.0 at observed rate | 0.1.0 worst case | 0.2.0 |
|---|---|---|---|---|
| 5 | 116 KB | 2,319 B | 9,564 B | 1,594 B |
| 20 | 133 KB | 4,492 B | 33,474 B | 1,594 B |
| 40 | 155 KB | 7,390 B | 65,354 B | 1,594 B |
Cost of the extension itself
Measured:
- cold path (no cache, includes
statSyncper path): 0.43 ms - warm path (cache hit, signature only): 0.0002 ms
The warm path is what runs on a normal turn. Negligible.
Full method and caveats: docs/benchmark.md.
Coexistence with other context extensions
There are several excellent context-management packages for Pi — pi-zip,
billion-context, context-mode, pi-cache-optimizer, magic-context,
pi-smart-compact, pi-hot-compact, pi-context-prune. None of them touches the
compaction file lists, and this extension touches nothing else, so they are orthogonal.
pi-zip documents the same conclusion from its side:
An extension that only writes Pi's compaction summaries (a
session_before_compacthandler) does not pause pi-zip.
If you run two extensions that both rewrite compaction summaries, expect unpredictable results — pick one.
Safety
- It never cancels compaction. Every handler returns
{}on any error, so Pi's default path runs untouched. - It never writes to your session file. The
contexthandler edits the summary in memory per turn; thesession_before_compacthandler returns a value that Pi persists like any hook result. - No network access. No telemetry, no fetch, no analytics.
- Uninstall = full rollback. Old summaries stay in the session file byte for byte.
- Failure budget. After 2 consecutive failures it stops for the rest of the process and lets Pi do compaction.
Known limits (read these)
- It depends on Pi internals. Three behaviours matter: the
fromHookinheritance guard,detailsnot being rendered into context, and in-place edits from thecontexthook. The first is documented; the other two are not contractual. A Pi upgrade can change them. The extension then does not crash — it silently stops helping — so it self-checks on load and logs a warning. Watch stderr after upgrading, or run/filelist. - Pi's compaction trigger is unaffected. Pi decides when to compact from its own
entry projection (
agent-session.js:422), not from the rewritten prompt. Shrinking the summary makes the real prompt smaller but does not move Pi's trigger point. Hooking the compaction itself mitigates this, because the new summary is small to begin with. - Information is deliberately dropped. Dead paths disappear from the list, and the
list is capped. The narrative summary still mentions what mattered. If you need the full
inventory, set
tool: trueandarchiveSensitive: true, and usesession_files. style: "index"+tool: truehas not been run in production by the author; theindexRecentcap was measured offline.- The archive filter is not retroactive. Old compaction entries keep their old lists.
Development
npm test # pure logic, no session data needed
npm run test:plugin # full hook chain against a mocked Pi (no model call)
npm run privacy # refuse to publish personal paths / session ids
npm run check # all three
test/plugin-load.mjs loads the extension against a mocked compact() and a throwaway
HOME, so it never reads the developer's own config and never calls a model. Point it at
another copy of the package with PLUGIN_PKG=/path/to/pkg.
To measure against your own session:
PI_TEST_SESSION=~/.pi/agent/sessions/<your-session-dir>/<your-session>.jsonl npm run test:plugin
That path is read-only.
License
MIT — see LICENSE.