pi-condense 2.0.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +22 -0
  3. package/PRUNING.md +1028 -0
  4. package/README.md +243 -0
  5. package/index.ts +858 -0
  6. package/package.json +56 -0
  7. package/src/batch-capture.ts +226 -0
  8. package/src/block-refs.test.ts +42 -0
  9. package/src/block-refs.ts +16 -0
  10. package/src/budget.test.ts +66 -0
  11. package/src/budget.ts +39 -0
  12. package/src/chain-compressor.test.ts +283 -0
  13. package/src/chain-compressor.ts +132 -0
  14. package/src/chain-detector.test.ts +302 -0
  15. package/src/chain-detector.ts +128 -0
  16. package/src/chain-range-prune.test.ts +522 -0
  17. package/src/chain-range-prune.ts +128 -0
  18. package/src/commands.test.ts +67 -0
  19. package/src/commands.ts +1207 -0
  20. package/src/config.ts +126 -0
  21. package/src/content-hash.ts +35 -0
  22. package/src/error-purge.test.ts +186 -0
  23. package/src/error-purge.ts +71 -0
  24. package/src/frontier.ts +62 -0
  25. package/src/indexer.ts +393 -0
  26. package/src/nested-placeholders.test.ts +82 -0
  27. package/src/nested-placeholders.ts +20 -0
  28. package/src/oversized-spill.integration.test.ts +73 -0
  29. package/src/protected.test.ts +62 -0
  30. package/src/protected.ts +51 -0
  31. package/src/pruner.test.ts +508 -0
  32. package/src/pruner.ts +156 -0
  33. package/src/query-tool.ts +78 -0
  34. package/src/range-compression.integration.test.ts +252 -0
  35. package/src/spill.test.ts +102 -0
  36. package/src/spill.ts +90 -0
  37. package/src/stats.test.ts +114 -0
  38. package/src/stats.ts +190 -0
  39. package/src/summarizer.test.ts +17 -0
  40. package/src/summarizer.ts +262 -0
  41. package/src/summary-refs.ts +61 -0
  42. package/src/thinking-strip.test.ts +175 -0
  43. package/src/thinking-strip.ts +42 -0
  44. package/src/tree-browser.ts +382 -0
  45. package/src/types.ts +764 -0
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # pi-condense
2
+
3
+ A [Pi coding-agent](https://github.com/badlogic/pi-mono) extension that summarizes completed tool-call batches, replaces raw tool outputs with short stubs in future context, and lets the LLM recover any original via the `context_tree_query` tool.
4
+
5
+ The session JSONL file is never modified β€” pruning only affects what each *next* request sees.
6
+
7
+ Fork of [`championswimmer/pi-context-prune`](https://github.com/championswimmer/pi-context-prune) with additional pre-flush safeguards, agent-message batching, chain compression, and an npm release flow.
8
+
9
+ πŸ“– For the algorithm, design rationale, prompt-cache interaction, and the research behind summarization-based context management, see **[PRUNING.md](PRUNING.md)**.
10
+
11
+ ## Install
12
+
13
+ Published to npm as [`pi-condense`](https://www.npmjs.com/package/pi-condense).
14
+
15
+ **User scope** (all repos under your pi profile):
16
+
17
+ ```bash
18
+ pi install npm:pi-condense
19
+ ```
20
+
21
+ **Project scope** (current repo only, committable via `.pi/settings.json`):
22
+
23
+ ```bash
24
+ pi install -l npm:pi-condense
25
+ ```
26
+
27
+ **Try without installing**:
28
+
29
+ ```bash
30
+ pi -e npm:pi-condense
31
+ ```
32
+
33
+ **From a local checkout** (for hacking on the extension itself):
34
+
35
+ ```bash
36
+ git clone git@github.com:jjuraszek/pi-condense.git ~/repos/pi-condense
37
+ cd ~/path/to/your/repo
38
+ pi install -l ~/repos/pi-condense
39
+ # or one-shot, no install:
40
+ pi -e ~/repos/pi-condense/index.ts
41
+ ```
42
+
43
+ Pin a specific version with `npm:pi-condense@X.Y.Z`. Upgrade by re-running `pi install`. Remove with `pi remove pi-condense`. Once installed, the extension auto-loads on every `pi` invocation; no flags needed.
44
+
45
+ > Diverges from upstream `championswimmer/pi-context-prune`; see [CHANGELOG.md](CHANGELOG.md) for what differs.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ /pruner on # enable pruning
51
+ /pruner status # see current mode + cumulative cost
52
+ /pruner model openai/gpt-4.1-mini # pick a cheap summarizer
53
+ /pruner now # flush pending batches immediately
54
+ ```
55
+
56
+ By default the extension is **off**. Enable it once and it stays enabled across sessions in the same pi agent directory.
57
+
58
+ ## How it decides when to prune
59
+
60
+ Two trigger modes. The mode controls *when* summarization fires; the algorithm is the same in each.
61
+
62
+ | Mode | Trigger | Cache impact | Use when |
63
+ |---|---|---|---|
64
+ | `agent-message` (default) | When the agent sends a final text-only reply | One cache rewrite per task batch | Normal coding-agent work β€” best balance |
65
+ | `on-demand` | Only when you run `/pruner now` | None until you ask | Long investigations; manual control |
66
+
67
+ Why `agent-message` is the default: provider prefix caches (Anthropic, OpenAI, Bedrock, vLLM) only hit when the prompt prefix matches exactly. Every prune rewrites that prefix. Batching tool turns and pruning once per agent reply means roughly one cache miss per task instead of one per turn. See [PRUNING.md Β§ The Sweet Spot](PRUNING.md#the-sweet-spot-batch-and-prune) for the full argument.
68
+
69
+ ## Configuration
70
+
71
+ Settings live under the `contextPrune` key in `<agent-dir>/settings.json` (i.e. pi's own settings file). `<agent-dir>` is `$PI_CODING_AGENT_DIR` if set, otherwise `~/.pi/agent`. Each pi preset gets its own settings, so you can run different summarizer models per preset.
72
+
73
+ ```json
74
+ {
75
+ "contextPrune": {
76
+ "enabled": false,
77
+ "showPruneStatusLine": true,
78
+ "summarizerModel": "default",
79
+ "summarizerThinking": "default",
80
+ "pruneOn": "agent-message",
81
+ "batchingMode": "turn",
82
+ "quietOversizedSkips": false,
83
+ "minBatchChars": 1000,
84
+ "protectedTools": [],
85
+ "protectedPaths": ["**/skills/**/*.md"],
86
+ "dedupByContentHash": true,
87
+ "autoBudgetThreshold": null,
88
+ "spillThreshold": 65536,
89
+ "spillPreviewBytes": 2048,
90
+ "budgetTurnDelta": null,
91
+ "chainCompression": {
92
+ "enabled": true,
93
+ "rollingWindow": 3,
94
+ "stripFinalAssistantThinking": true,
95
+ "fuseRangeSummary": true
96
+ },
97
+ "thinkingStrip": {
98
+ "enabled": true,
99
+ "keepLastTurns": 16
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ | Key | Values | Default | Notes |
106
+ |---|---|---|---|
107
+ | `enabled` | `true` / `false` | `false` | Master switch |
108
+ | `showPruneStatusLine` | `true` / `false` | `true` | Footer widget + queued-turn notifications |
109
+ | `summarizerModel` | `"default"` or `"provider/model-id"` | `"default"` | `default` = your active pi model. See [Choosing a summarizer model](#choosing-a-summarizer-model) |
110
+ | `summarizerThinking` | `default`/`off`/`minimal`/`low`/`medium`/`high`/`xhigh` | `default` | Provider-specific reasoning effort knob |
111
+ | `pruneOn` | see table above | `agent-message` | Trigger mode |
112
+ | `batchingMode` | `turn` / `agent-message` | `turn` | How coarse each summary is (independent of `pruneOn`) |
113
+ | `quietOversizedSkips` | `true` / `false` | `false` | Silences `skipped-oversized` / `skipped-trivial` info notifications |
114
+ | `minBatchChars` | non-negative integer, `0` disables | `1000` | Pre-flush guard β€” batches smaller than this skip the LLM entirely |
115
+ | `protectedTools` | `string[]` | `[]` | Never-pruned tool names (e.g. `["todowrite","todoread"]`). When a protected tool's chain is range-compressed, its output is preserved verbatim inside the `<compressed-chain>` block as `<protected-output>` β€” protected outputs are never lost. |
116
+ | `protectedPaths` | `string[]` | `["**/skills/**/*.md"]` | Globs matched against a tool call's `args.path`; matching outputs are never pruned (same semantics as `protectedTools`, including `<protected-output>` relocation in compressed chains). Already-summarized matching reads are repaired on the next turn; chain-compressed ones are not. Set `[]` to disable. |
117
+ | `dedupByContentHash` | `true` / `false` | `true` | Re-reads of identical (toolName, content) skip the LLM and alias the original |
118
+ | `autoBudgetThreshold` | fraction `0`–`1`, or `null` | `null` | Token-budget auto-flush: force a prune when context usage reaches this share of the window, regardless of `pruneOn`. `0.8` = 80%, not `80`. `null` = off. See [Token-budget auto-flush](#token-budget-auto-flush) |
119
+ | `spillThreshold` | positive integer | `65536` | Minimum chars (`resultText.length`) for a single tool result to be spilled eagerly to a sidecar file at capture time rather than waiting for normal summarization. Non-positive / invalid values fall back to the default; to effectively disable spilling, set it above any result you expect. See [Spilled outputs](#spilled-outputs) |
120
+ | `spillPreviewBytes` | non-negative integer | `2048` | Head preview (bytes) kept inline in the stub and index record for a spilled result. Full body is on disk. |
121
+ | `budgetTurnDelta` | fraction `0`–`1`, or `null` | `null` | Force a flush when a single turn's context-usage fraction jumps by at least this amount, ORed with `autoBudgetThreshold`. Catches sudden spikes a static threshold would miss until the next turn. `null` = off. |
122
+ | `chainCompression.enabled` | `true` / `false` | `true` | Master toggle for chain-level range compression |
123
+ | `chainCompression.rollingWindow` | positive integer | `3` | Keep this many most-recent closed chains raw; compress older ones |
124
+ | `chainCompression.stripFinalAssistantThinking` | `true` / `false` | `true` | Strip thinking blocks from the kept final text-only assistant when compressing |
125
+ | `chainCompression.fuseRangeSummary` | `true` / `false` | `true` | Fuse a compressed chain's per-batch summaries into one cohesive LLM summary (one extra summarizer call per multi-batch span); off keeps the per-batch concatenation |
126
+ | `purgeErrors.enabled` | `true` / `false` | `true` | Replace failed toolCall argument bodies with compact stubs after cooldown |
127
+ | `purgeErrors.cooldownTurns` | positive integer | `2` | Turns to wait after a tool error before purging its argument body |
128
+ | `purgeErrors.minArgChars` | non-negative integer | `500` | Only purge arg bodies at least this many characters long |
129
+ | `thinkingStrip.enabled` | `true` / `false` | `true` | Strip `thinking` blocks from assistant turns older than the last `keepLastTurns` |
130
+ | `thinkingStrip.keepLastTurns` | positive integer | `16` | Keep thinking on the last N assistant turns; strip older. Counts assistant turns, not chains. No-op under N turns |
131
+
132
+ See [PRUNING.md Β§ Chain Compression](PRUNING.md#chain-compression), [PRUNING.md Β§ Error Purge](PRUNING.md#error-purge), and [PRUNING.md Β§ Main-loop Thinking Strip](PRUNING.md#main-loop-thinking-strip) for the full algorithms.
133
+
134
+ The three pre-flush features (`minBatchChars`, `protectedTools`, `dedupByContentHash`) are explained in [PRUNING.md Β§ Pre-flush Pipeline & Safeguards](PRUNING.md#pre-flush-pipeline--safeguards). They run BEFORE any summarizer LLM call and can each drop a batch outright while still advancing the prune frontier.
135
+
136
+ ### Token-budget auto-flush
137
+
138
+ When `autoBudgetThreshold` is set to a value in `(0, 1]`, the extension checks context usage at the end of every tool-using turn. If `tokens / contextWindow` reaches the threshold, ALL pending batches are flushed immediately β€” regardless of `pruneOn` mode. This is an **additional** trigger layered on top of `pruneOn`, not a replacement.
139
+
140
+ - `0.8` means 80% of the context window β€” it is a **fraction**, not a percentage. `0.8 β‰  80`.
141
+ - The trigger is a no-op when `tokens` is `null` (right after a provider-side compaction); it resumes once usage is known again.
142
+ - Editable live via `/pruner settings` (row "Auto-flush at context %", presets Off / 60 / 70 / 80 / 90%).
143
+ - Default `null` = off.
144
+
145
+ Inspired by DCP's `maxContextLimit` nudging; simplified to a single threshold that forces a flush rather than separate nudge/force levels.
146
+
147
+ ### Spilled outputs
148
+
149
+ Single tool results larger than `spillThreshold` chars are written to `<session-dir>/<sessionId>-blobs/<toolCallId>.txt` at capture time and replaced in context with a short stub (tool name, byte size, head preview, file path). The full body is recoverable via the native `read` tool at the embedded path (offset/limit supported) or via `context_tree_query` by id, which falls back to the inline preview if the sidecar is missing. Moving a session `.jsonl` without its `-blobs/` directory loses only the giant-blob recovery path; bodies under `spillThreshold` stay inline in the index entry as usual.
150
+
151
+ ### Choosing a summarizer model
152
+
153
+ The `default` setting reuses whatever model you have active in pi β€” convenient but wasteful, since summary writing doesn't need a top-tier coding model. Picking the smallest/fastest model on your plan saves both latency and cost.
154
+
155
+ | Plan | Suggested summarizer |
156
+ |---|---|
157
+ | OpenAI / Codex / Copilot | `openai/gpt-4.1-mini`, `google/gemini-2.5-flash`, `xai/grok-3-fast` |
158
+ | OpenRouter | `openrouter/qwen/qwen3-30b-a3b` (cheap MoE) |
159
+ | Anthropic direct | `anthropic/claude-haiku-3-5` |
160
+ | Google AI direct | `google/gemini-2.5-flash` |
161
+
162
+ Set it from the slash command (saves immediately):
163
+
164
+ ```bash
165
+ /pruner model openai/gpt-4.1-mini
166
+ /pruner thinking low
167
+ # or both in one go:
168
+ /pruner model openai/gpt-4.1-mini:low
169
+ ```
170
+
171
+ ## Commands
172
+
173
+ | Command | Effect |
174
+ |---|---|
175
+ | `/pruner` | Interactive picker over all subcommands |
176
+ | `/pruner settings` | Settings overlay (toggle / cycle every option) |
177
+ | `/pruner on` / `off` | Enable / disable pruning |
178
+ | `/pruner status` | Show mode, model, trigger, cumulative stats |
179
+ | `/pruner stats` | Detailed cumulative summarizer token/cost stats |
180
+ | `/pruner model [id\[:thinking\]]` | Get / set summarizer model (and optionally thinking level) |
181
+ | `/pruner thinking [level]` | Get / set summarizer reasoning effort |
182
+ | `/pruner prune-on [mode]` | Get / set trigger mode |
183
+ | `/pruner batching [mode]` | Get / set batching granularity (`turn` / `agent-message`) |
184
+ | `/pruner protected-tools [names]` | Show or edit the never-pruned tool allowlist (comma- or space-separated; `none` clears) |
185
+ | `/pruner protected-paths [globs]` | Show or edit the never-pruned path globs (`none` clears) |
186
+ | `/pruner min-batch-chars [n]` | Show or set the pre-flush trivial-batch threshold (`0` disables) |
187
+ | `/pruner dedup [on\|off\|status]` | Toggle pre-flush content-hash dedup |
188
+ | `/pruner tree` | Foldable browser of pruned tool calls; `Ctrl-O` opens the full summary in an overlay |
189
+ | `/pruner compact` | Retroactively compress every eligible closed chain (bypasses `rollingWindow`) |
190
+ | `/pruner now` | Flush pending batches immediately with a multi-row progress widget above the input |
191
+ | `/pruner help` | Full help text |
192
+
193
+ ## Tools surfaced to the LLM
194
+
195
+ **`context_tree_query`** β€” always available when the extension is loaded. Pruned summaries end with short refs like `Summarized tool refs: \`t1\`, \`t2\`. Use \`context_tree_query\` with these refs to retrieve the original full outputs.` The model passes those refs (or full `toolCallId`s) and gets back the original tool result text from the session index. Content-hash-deduped duplicates resolve to the original's record automatically.
196
+
197
+ ## Footer status widget
198
+
199
+ A footer widget shows the current state, controlled by `showPruneStatusLine`:
200
+
201
+ Every rendered state is wrapped in `β”‚ … β”‚` so the segment stays visually isolated in the shared footer regardless of where other extensions' status segments land (load-order independent).
202
+
203
+ - `β”‚ prune: OFF β”‚` β€” disabled
204
+ - `β”‚ prune: ON β”‚` β€” enabled, no flushes yet
205
+ - `│ prune: ON · 92k->14k (-85%) │` — enabled; live reclaim ratio (estimated tokens before→after, percent reduction). Updates on every `pruneMessages` call.
206
+ - `β”‚ prune: 3 pending β”‚` β€” batches queued, waiting for the trigger
207
+ - `β”‚ prune: summarizing… β”‚` β€” flush in progress
208
+
209
+ Setting `showPruneStatusLine: false` hides the widget and silences the queued-turn notice; pruning still runs.
210
+
211
+ Cost no longer appears on the status line. Full token/cost detail is available via `/pruner stats`. The extension also emits cumulative session cost on the `cost:external` pi.events channel for external aggregators β€” see [External cost channel](#external-cost-channel).
212
+
213
+ ## External cost channel
214
+
215
+ Every time the summarizer cost updates, the extension emits on the shared `pi.events` channel identified by the constant `EXTERNAL_COST_CHANNEL = "cost:external"`. Payload shape:
216
+
217
+ ```ts
218
+ interface ExternalCostUpdate {
219
+ source: string; // EXTERNAL_COST_SOURCE = "pi-condense"
220
+ totalCost: number; // cumulative cost for the current session (USD)
221
+ inputTokens?: number;
222
+ outputTokens?: number;
223
+ }
224
+ ```
225
+
226
+ Semantics:
227
+ - **Cumulative per session**, not all-time. Re-emitted on every update; aggregators key by `source` and replace the previous value.
228
+ - **Live only.** Not persisted; not re-emitted on `session_start`. An aggregator that restarts mid-session sees cost from zero until the next summarizer call.
229
+ - Designed for aggregators like pi-cohort that show a unified Ξ£$ total across extensions.
230
+
231
+ ## Limitations
232
+
233
+ - Pruning only applies to batches captured *while enabled*. Enabling mid-session does not retroactively summarize earlier turns.
234
+ - Summarizer calls are synchronous inside `turn_end` (or `message_end` for `agent-message` mode), so they add latency between turns proportional to the summarizer model's response time. Pick a fast model.
235
+ - Content-hash dedup only matches against records already in the indexer (cross-flush). Two identical outputs within the *same* flush are not deduped β€” both go through the summarizer.
236
+ - The tree browser does not inline original tool outputs β€” use `context_tree_query` for that.
237
+
238
+ ## References
239
+
240
+ - Anthropic prompt caching: <https://docs.claude.com/en/docs/build-with-claude/prompt-caching>
241
+ - AWS Bedrock prompt caching: <https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html>
242
+ - OpenAI prompt caching: <https://platform.openai.com/docs/guides/prompt-caching>
243
+ - Research backing summarization-based context management: see [PRUNING.md Β§ Research Evidence](PRUNING.md#why-summarization-works-research-evidence)