pi-context-guard-jev 0.2.0 → 0.3.2

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/docs/DESIGN.md CHANGED
@@ -15,11 +15,13 @@ evidence behind the thresholds are in [JEV.md](JEV.md). Cache behaviour is in [C
15
15
  | `src/decide.ts` | `buildRequest` (the run-end Jev request), `buildCheckpointRequest` (the mid-run one) and `interpret` (answers → keep set); `citedFiles` |
16
16
  | `src/distill.ts` | `distillRun`: chunk, run Jev requests in parallel within a time budget, decide per result, build `context_edit` drafts |
17
17
  | `src/items.ts` | Whole items: small outputs (`collectSmall`) and old exchanges (`collectExchanges`), their Jev requests, stubs and edits, and `judgeItems` (batched, parallel, time budget) |
18
- | `src/process.ts` | `processItems`: large outputs, small outputs and old exchanges of one pass, judged in parallel and merged into one list of edits and records |
18
+ | `src/process.ts` | `processItems`: large outputs, small outputs and old exchanges of one pass, judged in parallel and merged into one list of edits and records; `addImages` adds the image removals |
19
+ | `src/images.ts` | Old images: `collectImages` (tool results with images at least `imageKeepTurns` old), their stub and replacement |
20
+ | `src/size.ts` | Context size in text characters: images by pixels (width × height ÷ 750 tokens × 4), thinking signatures at half, image headers (PNG/JPEG/GIF/WebP) |
19
21
  | `src/render.ts` | The replacement text: header, verbatim chunks, omission lines; the `[context-guard]` marker |
20
22
  | `src/cache-pin.ts` | Anthropic breakpoints: the previous user question, and the read point after edits (`placeGuardBreakpoints`) |
21
23
  | `src/stats.ts` | Savings and Jev usage computed from the session; status-bar text |
22
- | `src/recall.ts` | The text returned by `recall` (a tool output, or the transcript of an omitted exchange) |
24
+ | `src/recall.ts` | What `recall` returns (a tool output with its images, or the transcript of an omitted exchange with its user images) |
23
25
  | `src/types.ts` | Loose structural types for the Pi objects used, so the pure modules can be tested without Pi |
24
26
 
25
27
  Everything except `index.ts` is pure. `distillRun` gets the classifier as an injected function.
@@ -62,10 +64,14 @@ Hooks used:
62
64
 
63
65
  ### 1. Run span
64
66
 
65
- `collectRun` walks back through `event.context.contextEntries` (Pi's projected entries) to the last entry
66
- that has a user message, with or without text. Its text is the **question**; a prompt with only an image
67
- gets the placeholder `[the user sent only an image]`, so an older question is never reused. Everything
68
- after it is the run. Assistant
67
+ `collectRun` walks back through `event.context.contextEntries` (Pi's projected entries) to the user
68
+ message that started the run (`findRunStart`), with or without text. **Steering messages** (user messages
69
+ sent while the agent was working: the message before them, custom notices aside, is a tool result or an
70
+ assistant message with tool calls) do not start a run; the walk goes on to the prompt before them. The
71
+ **question** is that prompt's text followed by every steering message of the run, each prefixed with
72
+ `[The user added during the run]`; a message with only an image gets the placeholder
73
+ `[the user sent only an image]`, so an older question is never reused. Everything after the prompt is the
74
+ run. Assistant
69
75
  text in the run gives the **answer** (the last text) and the **notes** (all earlier text). Tool calls are
70
76
  indexed by id so each result can be matched with its tool name and arguments.
71
77
 
@@ -75,7 +81,7 @@ default 3; `0` turns all of it off):
75
81
  - `summary`: the text (`summary` field) of the latest `compactionSummary` or `branchSummary` message, at
76
82
  most 2,000 characters.
77
83
  - `exchanges`: the last N earlier exchanges, oldest first. Every user message starts one (steering messages
78
- included; an image-only prompt shows as `[the user sent only an image]`). Each holds the prompt (at most
84
+ of earlier runs included; an image-only prompt shows as `[the user sent only an image]`). Each holds the prompt (at most
79
85
  800 characters) and the **last** assistant text before the next user message (at most 1,200). Tool calls
80
86
  and tool results are left out.
81
87
  - `firstRequest`: the first user prompt in the projection (at most 1,000 characters), only when it is not
@@ -218,11 +224,11 @@ run end every small output without the marker is judged, including those kept at
218
224
  **Old exchanges** (`collectExchanges`). An exchange is a user prompt and every entry up to the next one:
219
225
  assistant messages, tool calls and results, custom messages such as subagent notices. Eligible are
220
226
  exchanges before the current run's user message, without the last `keepRecentExchanges` (2) before it,
221
- that completed (their last assistant message has no tool calls, so a span cut by a steering message is
222
- never one), that are not already omitted (their prompt shows the stub), that were not judged during the
223
- current run (`exchangeMemo`: exchange results in records after the last user message, except deferred
224
- ones), and that can be recovered and are worth it: no message with an image, no entry whose model-visible
225
- content differs from the raw entry for any reason other than our own marker (another extension's edit,
227
+ that completed (their last assistant message has no tool calls; a steering message belongs to the exchange
228
+ it was sent in), that are not already omitted (their prompt shows the stub), that were not judged during the
229
+ current run (`exchangeMemo`: exchange results in records after the run's prompt, except deferred ones), and that can be recovered and are worth it: no entry whose model-visible
230
+ content differs from the raw entry for any reason other than our own marker (images are fine: `recall` returns
231
+ them) (another extension's edit,
226
232
  or an omission by someone else, which Pi keeps in the projection as an entry with no messages), at least
227
233
  2,000 characters of editable content, and a stub shorter than that content. A kept exchange is
228
234
  judged again in a later run, against that run's work. Compaction and branch summaries are never part of an
@@ -253,6 +259,22 @@ say `kept` / `deferred`, and they are judged again at later run ends against the
253
259
  deferred exchanges accumulate until their total pays off. `exchangeBreakEven: false` always omits
254
260
  unrelated exchanges.
255
261
 
262
+ **Old images** (`images.ts → collectImages`, `imageKeepTurns`, 3). Every tool result in the projection,
263
+ earlier runs included, whose model-visible content has images and that at least `imageKeepTurns` assistant
264
+ messages follow. Skipped: user messages (the images you send are usually the spec), results another extension
265
+ changed (text or image count differs from the raw entry), and results whose images are already gone. No Jev
266
+ request: Jev reads text only, and a model reacts to an image in its next message, whose text stays. The
267
+ replacement is a text-only content: the stub
268
+ `[context-guard] Removed 1 image from this output (1280×960; ~1.6k tokens), 4 turns old. recall({"entryId":"…"}) shows it again.`
269
+ followed by the result's own text blocks. Because the stub leads, the result counts as ours (`editedByOthers`
270
+ is false, stats count it), and as already distilled (its text is not judged again). Its size before and after
271
+ uses `size.ts` (an image is width × height ÷ 750 tokens, 4 characters per token; header unreadable: 1,600
272
+ tokens; at most 6,700). Records have `kind: "image"`, `requests: 0`, reason `N turns old`. When Jev is
273
+ unavailable, a pass still removes images. At run end, images from before the run's prompt go through the
274
+ same marginal-cost gate as exchanges (`exchangeBreakEven`, with no minimum saving); deferred ones are just
275
+ collected again next time. Mid-run, images count towards the batch; the tiers are: everything; without
276
+ exchanges; without exchanges and images from before the last user message.
277
+
256
278
  **Batching.** Whole items are grouped into requests of at most `maxSegmentChars` characters of item text and
257
279
  `maxChunksPerSegment` (40) items, under the same `timeoutMs` budget; the three kinds run at the same time,
258
280
  and one shared limiter keeps at most `concurrency` (6) Jev requests in flight across all of them. An item without an
@@ -408,27 +430,33 @@ The extension does not write Jev's usage into Pi's own usage entries. It is reco
408
430
  Nothing is kept in memory. Every redraw recomputes from the session:
409
431
 
410
432
  - **Savings:** Pi's session projection (`buildSessionProjection()`) is checked for tool results whose
411
- model-visible text starts with `[context-guard]` while the raw entry does not; each one adds raw length
412
- minus visible length. A prompt that shows the exchange stub adds the raw length of its exchange (the
413
- prompt and the entries up to the next user message that the model no longer sees: Pi keeps an omitted
433
+ model-visible text starts with `[context-guard]` while the raw entry does not; each one adds raw size
434
+ minus visible size (`size.ts`: text length, images by pixels). Results that lost only their images count
435
+ as image results. A prompt that shows the exchange stub adds the raw size of its exchange (the
436
+ prompt and the entries up to the next prompt that the model no longer sees: Pi keeps an omitted
414
437
  entry in the projection with no messages) minus the stub.
415
438
  Tokens are estimated as characters ÷ 4.
416
- - **Jev usage:** the `context-guard` records on the active branch (`getBranch()`) are summed.
439
+ - **Lifetime:** the `savedChars` of every `context-guard` record on the active branch, and whether a
440
+ compaction came after a record that saved something (`compacted`).
441
+ - **Jev usage:** the same records are summed.
417
442
 
418
443
  So the numbers follow branches, `/tree`, reloads and compaction.
419
444
 
420
- The ready-state footer is `🛡 −4.2k · 1`: ≈ tokens kept out of context · distilled outputs plus omitted
421
- exchanges (the first part
422
- in the theme's success colour, the rest dimmed). Before anything is distilled it shows `🛡 0 saved`.
445
+ The ready-state footer is `🛡 −4.2k · 1`: ≈ tokens kept out of context · distilled outputs, outputs without
446
+ their images and omitted exchanges (the first part in the theme's success colour, the rest dimmed). Before
447
+ anything is distilled it shows `🛡 0 saved`. After a compaction dropped earlier edits it adds the lifetime
448
+ total: `🛡 0 · Σ−79k`, or `🛡 −4.2k · 3 · Σ−83k` once new edits exist (only when Σ is larger).
423
449
 
424
450
  ## Recall
425
451
 
426
452
  `recall` reads the raw entry with `ctx.sessionManager.getEntry(entryId)`. `context_edit` never changes that
427
- entry. For a tool result it returns the output; for a user prompt (the stub of an omitted exchange) it
453
+ entry. For a tool result it returns the output and its images (image blocks after the text, at most 6, only
454
+ for a plain recall: with `pattern` or `offset` a note says how many there are); for a user prompt (the stub of an omitted exchange) it
428
455
  returns a transcript of the raw exchange from the branch: `## user`, `## assistant` with the text and
429
456
  `[tool call] name {arguments}` lines, `## tool result (tool)` and `## custom message (type)` sections, for
430
- exactly the entries the latest record lists as `omitted` (without such a record, up to the next user
431
- message). The tool returns the whole text, or only lines that match `pattern` (with line numbers), or an
457
+ exactly the entries the latest record lists as `omitted` (without such a record, up to the next prompt).
458
+ Images of the exchange's user messages are attached (at most 6); a tool result's images are named with a
459
+ `recall({"entryId":…})` line for that result. The tool returns the whole text, or only lines that match `pattern` (with line numbers), or an
432
460
  `offset`/`limit` window. Output is capped at 2,000 lines and 50 KB, with a continuation note; a single
433
461
  line longer than 50 KB is cut with a `[line N cut at 50KB]` note. `pattern` is a JavaScript regex of at most
434
462
  500 characters, matched against the first 4,000 characters of each line inside a `node:vm` sandbox with a
@@ -447,8 +475,16 @@ from a `[context-guard]` note.
447
475
  - **Never replace an assistant message's content.** A replacement turns the content into one text block,
448
476
  which drops thinking, signatures and tool calls. Assistant messages are only omitted, as part of a whole
449
477
  exchange.
450
- - **Steering messages move the span.** A user message sent during a run starts a new span. Tool results
451
- from before it are never candidates, now or later. That is the safe direction.
478
+ - **Steering messages continue the run.** Until 0.2.0 a user message sent during a run started a new span,
479
+ and tool results from before it were never candidates again. In real sessions that left 0.1–0.4M
480
+ characters per steered run in context for good (they also blocked omitting the exchange, which contains
481
+ images). Now the span goes back to the prompt and the steering messages join the question. The
482
+ Anthropic question pin sits on the last user message with text (the steering message), so an edit before
483
+ it has no readable pin: `anthropicRewriteChars` counts the whole context unless a trusted read point
484
+ applies.
485
+ - **Recall outputs are not judged mid-run.** The model just asked for them. Judging them at the next
486
+ checkpoint made models recall the same entry again (seen in real sessions). At run end they are judged
487
+ like other outputs.
452
488
  - **`keepCitedFiles` is off by default.** Answers also name files to rule them out. In a live run, Claude's
453
489
  answer said `test/fixtures/entities.json` was *not* a usage, and the option kept 40 lines of noise from
454
490
  that file. See [JEV.md](JEV.md).
@@ -0,0 +1,164 @@
1
+ # Reference
2
+
3
+ Everything you can see, call and configure. How it works inside: [DESIGN.md](DESIGN.md).
4
+
5
+ ## Requirements
6
+
7
+ - Pi ≥ 1.0.3
8
+ - An OpenRouter credential (`OPENROUTER_API_KEY` or `/login`) for the default classifier,
9
+ `openrouter/~typesafe/jev-latest`. Any classifier model in Pi's catalog can be set with the `model`
10
+ setting, but only the default has been tested.
11
+
12
+ If the model or credential is missing, the extension does nothing and the status bar says so.
13
+
14
+ ## Status bar
15
+
16
+ | Text | Meaning |
17
+ |---|---|
18
+ | `🛡 0 saved` | On and ready; nothing is distilled on this branch yet |
19
+ | `🛡 0 · Σ−79k` | A compaction dropped the earlier edits from context: nothing is kept out right now, about 79k tokens were saved on this branch in total |
20
+ | `🛡 distilling…` | Jev is judging the outputs of the run that just finished, or of a mid-run checkpoint |
21
+ | `🛡 −4.2k · 1` | ≈ tokens kept out of context · items: about 4.2k tokens are currently kept out of context, by 1 distilled or omitted tool output, output without its images, or old exchange |
22
+ | `🛡 −4.2k · 1 · Σ−83k` | The same, after a compaction: Σ is the total saved on this branch, including edits the compaction dropped |
23
+ | `🛡 guard off` | Turned off with `/guard off` or `"enabled": false` |
24
+ | `🛡 no openrouter key` | No credential for the classifier's provider (the provider name is filled in) |
25
+ | `🛡 Jev model not found` | The configured `model` is not in Pi's classifier catalog |
26
+
27
+ The token figure is Pi's usual estimate, characters ÷ 4. It counts what is kept out of the model's context
28
+ **on the current branch right now**: original size minus replacement size, for every distilled output,
29
+ removed image and omitted exchange that is still in the context (an image counts as about width × height ÷ 750
30
+ tokens). It goes down after `/tree` to a point before an edit, and after compaction removes distilled
31
+ results; then Σ shows the total of every pass on the branch so far. It is not a measured token count.
32
+
33
+ ## `/guard` command
34
+
35
+ | Command | Effect |
36
+ |---|---|
37
+ | `/guard` or `/guard status` | Shows whether the guard is on, the model, the savings on this branch (distilled outputs and omitted exchanges), Jev runs and mid-run checkpoints, requests and cost, whether checkpoints and exchange pruning are on, and the last run's or checkpoint's result for each item (large, small, exchange), including Jev traces such as `kw.15 focus=chunk_4:.52 keep 9/29` or `p.06` |
38
+ | `/guard on` / `/guard off` | Turns the guard on or off and saves `enabled` to the settings file |
39
+ | `/guard reload` | Re-reads the settings file and shows any warnings again |
40
+
41
+ Settings are read at session start and on `/guard reload`.
42
+
43
+ ## `recall` tool
44
+
45
+ Every distilled output and omitted exchange starts with `[context-guard]` and ends with
46
+ `recall({"entryId":"…"})`. For an omitted exchange, recall returns the whole exchange as a transcript
47
+ (prompt, replies, tool calls and results, custom messages). The model can call:
48
+
49
+ | Parameter | Meaning |
50
+ |---|---|
51
+ | `entryId` (required) | The entry id from the header |
52
+ | `pattern` | JavaScript regex (≤ 500 chars, 1 s limit); returns only matching lines, prefixed with their line numbers |
53
+ | `offset` | First line to return, 1-based. When `pattern` is set, this counts matching lines. |
54
+ | `limit` | Most lines to return |
55
+
56
+ Like Pi's `read` tool, the output is capped at 2,000 lines and 50 KB, and a note says how to continue.
57
+
58
+ ## Configuration
59
+
60
+ The settings file is `~/.pi/agent/context-guard.json` (inside Pi's agent directory), or the path in
61
+ `PI_CONTEXT_GUARD_CONFIG`. Every key is optional. Values that are missing, the wrong type or out of range
62
+ fall back to the default.
63
+
64
+ | Key | Default | Meaning |
65
+ |---|---|---|
66
+ | `enabled` | `true` | Master switch (`/guard on` and `/guard off` write it) |
67
+ | `model` | `"openrouter/~typesafe/jev-latest"` | Classifier as `provider/id`; the first `/` separates the provider |
68
+ | `minResultChars` | `4000` | Tool results from this size are chunked; shorter ones are judged whole (see `smallResultMinChars`) |
69
+ | `minRunChars` | `8000` | Distill a run's large outputs only if they add up to at least this (small outputs and exchanges are judged regardless) |
70
+ | `keepWholeThreshold` | `0.7` | Keep a segment untouched if P(`keep_whole`) is at least this |
71
+ | `focusWholeThreshold` | `0.6` | Keep a segment untouched if `focus` picks "whole" with at least this probability |
72
+ | `chunkKeepThreshold` | `0.6` | Keep a chunk if P(chunk needed) is at least this |
73
+ | `noneThreshold` | `0.5` | Remove a segment entirely if `focus` picks "none" with at least this probability and no chunk passes |
74
+ | `maxKeepRatio` | `0.6` | Skip the edit if the kept chunks are more than this fraction of the original |
75
+ | `keepCitedFiles` | `false` | Also keep grep chunks for files that the final answer names (see DESIGN.md for why it is off) |
76
+ | `timeoutMs` | `8000` | Total time budget for all Jev requests of one run; unfinished parts are kept |
77
+ | `concurrency` | `6` | Jev requests in parallel |
78
+ | `maxSegmentChars` | `32000` | Most characters of one output per Jev request (keeps the worst-case request under Jev's 32k-token context, see [JEV.md](JEV.md)) |
79
+ | `maxChunksPerSegment` | `40` | Most chunks per Jev request |
80
+ | `excludeTools` | `["edit", "write"]` | Tools whose results are never distilled |
81
+ | `distillErrors` | `true` | Also distill error results, such as the log of a failing test or build; Jev is told the call failed, and that its details are usually no longer needed once the agent got past it |
82
+ | `historyExchanges` | `3` | Earlier exchanges (prompt + last assistant text) sent to Jev so it judges relevance against the ongoing work; `0` sends no earlier conversation at all (integer 0–10) |
83
+ | `pinAnthropicCache` | `true` | Anthropic cache breakpoints: at the previous user question on every request, and on the first request after context edits a read point (at a cache entry this process wrote) and a write anchor ([CACHE.md](CACHE.md)) |
84
+ | `midRun` | `true` | Mid-run checkpoints during long runs ([RESULTS.md](RESULTS.md#long-autonomous-runs)) |
85
+ | `midRunMinAgeTurns` | `4` | Judge an output mid-run only once it is at least this many turns old (integer 1–100) |
86
+ | `midRunBatchChars` | `60000` | Run a checkpoint only when the not-yet-judged eligible outputs add up to this many characters |
87
+ | `midRunBatchCharsOpenAI` | `60000` | The same, for `openai-codex` models |
88
+ | `midRunChunkKeepThreshold` | `0.6` | Keep a chunk at a checkpoint if P(still needed) is at least this (probe in [JEV.md](JEV.md)) |
89
+ | `midRunBreakEven` | `true` | Skip a checkpoint unless its one-time prompt-cache rewrite is likely to pay off; `false` checkpoints purely for context room |
90
+ | `smallResultMinChars` | `400` | Tool results from this size up to `minResultChars` are judged as whole items; `0` turns this off |
91
+ | `smallKeepThreshold` | `0.45` | Keep a small output if P(still needed) is at least this (probe in [JEV.md](JEV.md)) |
92
+ | `pruneExchanges` | `true` | Judge old exchanges and omit the unrelated ones |
93
+ | `keepRecentExchanges` | `2` | The last this many exchanges before the current prompt are never omitted |
94
+ | `exchangeOmitThreshold` | `0.2` | Omit an exchange only if P(still relevant) is below this (probe in [JEV.md](JEV.md)) |
95
+ | `exchangeBreakEven` | `true` | At run end, omit unrelated exchanges only when the cache rewrite they cause pays off; deferred ones are judged again at later run ends and omitted once their total pays off. `false` always omits them |
96
+ | `exchangeMinSavingChars` | `8000` | A run end omits exchanges only if they save at least this many characters together (when they cost a cache rewrite) |
97
+ | `imageKeepTurns` | `3` | Remove images from tool results once this many assistant messages have followed them (`recall` shows them again); images in user messages are kept; `0` turns this off |
98
+
99
+ Example:
100
+
101
+ ```json
102
+ { "minResultChars": 6000, "excludeTools": ["edit", "write", "bash"] }
103
+ ```
104
+
105
+ ## Data sent to Jev
106
+
107
+ **This extension sends data to a third party, and it is on by default.** After every run that qualifies,
108
+ each Jev request sends the following to OpenRouter, which forwards it to TypeSafe:
109
+
110
+ - the user's question (up to 4,000 characters),
111
+ - the final answer (up to 6,000 characters),
112
+ - the assistant's notes written during the run (up to 2,000 characters),
113
+ - excerpts of the earlier conversation in this session: the latest compaction or branch summary (up to
114
+ 2,000 characters), the first prompt (up to 1,000), and the last 3 earlier exchanges, each one prompt
115
+ (up to 800) and the last assistant text after it (up to 1,200). Tool calls and tool outputs of earlier runs
116
+ are not included in this part. Set `historyExchanges` to `0` to send none of it,
117
+ - the tool name and arguments,
118
+ - the full text of the large tool output being judged, and of small outputs (from 400 characters; up to
119
+ 4,000 characters each). This can include source code, logs or anything else the tools returned,
120
+ - excerpts of old exchanges being judged: each prompt (up to 600 characters), the last assistant text (up to
121
+ 800), the tool calls as short labels (tool name and the command or path, up to 80 characters each) and the
122
+ size. Set `pruneExchanges` to `false` to send none of it.
123
+
124
+ Never sent: tool outputs below `smallResultMinChars` (400 characters), results of excluded tools (`edit`,
125
+ `write`), images (never sent: they are removed by age), the text of outputs with images, old exchanges with entries another extension changed,
126
+ exchanges below 2,000 characters, and the last `keepRecentExchanges` (2) exchanges before the current
127
+ prompt. Large outputs are sent only when the run's large outputs add up to `minRunChars` (8,000); small
128
+ outputs and old exchanges are sent at every run end and every mid-run checkpoint that has any.
129
+
130
+ `/guard off` turns the extension off and saves that choice in the settings file, so it stays off in later
131
+ sessions. See OpenRouter's data policy:
132
+ <https://openrouter.ai/docs/guides/privacy/data-collection>.
133
+
134
+ ## What it never touches
135
+
136
+ - messages of the current run other than tool results, and the last `keepRecentExchanges` exchanges before
137
+ the current prompt
138
+ - compaction and branch summaries, exchanges that did not finish (the last reply has tool calls), and
139
+ messages Pi cannot edit (system messages, `!` shell executions), which stay in place inside an omitted
140
+ exchange
141
+ - images in your own messages; images in tool results younger than `imageKeepTurns` (3) turns
142
+ - the text of tool results that contain images (only the images are removed, by age)
143
+ - error results, if `distillErrors` is `false`
144
+ - results of excluded tools (`edit`, `write` by default)
145
+ - results shorter than `smallResultMinChars` (400), and large results of runs below `minRunChars`
146
+ - aborted or failed runs
147
+ - results that are already distilled, or that another extension already edited
148
+ - single lines too long for one Jev request (kept as they are)
149
+ - tool outputs of earlier runs one by one: earlier runs are only touched as whole exchanges (and their old
150
+ images)
151
+
152
+ ## Limitations
153
+
154
+ - **Lossy by design.** Relevance is judged against the run's question and answer and the earlier
155
+ conversation (the ongoing task), so a follow-up that goes somewhere new may need something that was
156
+ removed. The header and omission lines say what is missing, and `recall` returns it. An earlier task
157
+ that comes back after several unrelated ones may have been omitted as a whole exchange.
158
+ - **Latency.** Settling waits for Jev. This adds about 0.5–1.2 s after each qualifying run (measured), and
159
+ never more than `timeoutMs` (8 s). A mid-run checkpoint pauses the run the same way.
160
+ - **Cost.** Each qualifying run costs a few Jev requests, measured at under $0.001 per run. The first prompt
161
+ after an edit also has a one-time partial prompt-cache miss ([CACHE.md](CACHE.md)).
162
+ - **Steering.** A message sent during a run joins the run's question. Outputs from before it can be distilled,
163
+ but the first edit before it rewrites the whole prompt cache once (the question pin moves to the steering
164
+ message).
@@ -0,0 +1,146 @@
1
+ # Measured results
2
+
3
+ Single runs on Pi 1.0.3, not a benchmark: the main model takes a different path each time. Costs are what Pi
4
+ reports at list price. How to reproduce them: [TESTING.md](TESTING.md).
5
+
6
+ ## Examples
7
+
8
+ Real runs on public code: each example ran once with the guard on and once with it off (`--no-guard`), from
9
+ the same prompts. Reproduce with `node test/e2e/examples.mjs`.
10
+ Examples A–C judge large outputs only (their numbers match `smallResultMinChars: 0`); example D shows the
11
+ default configuration and `exchangeBreakEven: false`.
12
+
13
+ **A. Searching a real codebase.** In a copy of the published Pi package (`dist/` without source maps and
14
+ the minified bundle, plus `docs/`), the agent was asked where Pi decides to auto-compact, starting with
15
+ `rg -n -i compact dist docs`. Pi's bash tool returned the last 50 KB of that search. Jev kept the matches
16
+ in the compaction module, the settings and the decision points in `agent-session.js`, and dropped matches
17
+ in the grep tool, HTML export templates, the SDK and so on. Then the agent was asked "which of those
18
+ settings can a user change, and what are their defaults?".
19
+ - Claude Sonnet 5.5: 50,413 → 16,210 chars (141 of 431 lines kept). The first turn-2 request carried
20
+ **15.5k tokens instead of 28.2k**. The answer was correct (`enabled` true, `reserveTokens` 16384,
21
+ `keepRecentTokens` 20000), without `recall`.
22
+ - GPT-6 Luna: 51,216 → 13,023 chars (107 of 385 lines), and a 5.4k read of `compaction.js` → 1.8k; two
23
+ smaller outputs were left whole. Turn 2 carried **11.5k tokens instead of 34.3k** and was correct, without
24
+ `recall`.
25
+
26
+ **C. Reading a 164 KB source file.** The agent read `dist/core/agent-session.js` (3,535 lines) in
27
+ seven 500-line pieces and explained how a prompt reaches the model. Jev kept 45–164 lines of each of the first
28
+ four pieces and removed the last three completely (each became a one-line stub). The follow-up asked for a
29
+ detail the explanation never mentioned: the exact error thrown when a prompt arrives during compaction.
30
+ That line was among the kept ones, so the agent quoted it exactly without `recall`. The turn-2 request
31
+ carried **9.2k tokens instead of 35.3k**.
32
+
33
+ **B. A failing test run.** In a copy of this repository with one injected off-by-one bug, the agent ran
34
+ the unit tests (a 15.8k-character log, 3 failures among 116 tests) and reported the cause. Jev kept the
35
+ 3 failures with their assertion messages and the failing-test summary, and dropped the passing tests:
36
+ 15.8k → 8.6k chars. Turn 2 ("now fix it") carried **5.7k tokens instead of 7.5k**, and both runs fixed
37
+ the bug. Failing commands are distilled because `distillErrors` is on by default.
38
+
39
+ What the model sees after distilling (example A, Claude; the first lines of the real replacement):
40
+
41
+ ```text
42
+ [context-guard] Distilled the output of bash `rg -n -i compact dist docs`: kept 141 of 431 lines (15.1k of 50.4k chars); the rest was judged not needed for the answer. Full output: recall({"entryId":"1704787f"}).
43
+ [… 79 lines omitted: matches in dist/core/tools/grep.js, dist/core/session-manager.js, dist/core/export-html/template.js, dist/core/export-html/template.css, dist/core/sdk.js and 5 more …]
44
+ dist/core/compaction/compaction.d.ts:25: /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
45
+ dist/core/compaction/compaction.d.ts:28:export interface CompactionSettings {
46
+ dist/core/compaction/compaction.d.ts:33:export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
47
+ dist/core/compaction/compaction.d.ts:54:/** Estimate projected context without trusting usage captured before a later edit or compaction. */
48
+ dist/core/compaction/compaction.d.ts:57: * Check if compaction should trigger based on context usage.
49
+ dist/core/compaction/compaction.d.ts:59:export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
50
+ [… 44 lines omitted: matches in dist/core/compaction/compaction.d.ts, dist/core/compaction/branch-summarization.d.ts, dist/core/compaction/index.d.ts, dist/core/session-manager.d.ts, dist/core/cache-stats.js and 2 more …]
51
+ ```
52
+
53
+ | Example | Model | Tool output before → after | Jev | Next request's context, on / off | Main-model cost of turn 2, on / off | Follow-up |
54
+ |---|---|---|---|---|---|---|
55
+ | A. code search | Claude Sonnet 5.5 | 50.4k → 16.2k chars | 3 requests, 0.8 s, $0.0012 | 15.5k / 28.2k tokens | $0.033 / $0.016 | correct, no `recall` |
56
+ | A. code search | GPT-6 Luna | 56.6k → 14.8k chars (2 outputs; 2 more, 15k, left whole) | 5 requests, 0.6 s, $0.0015 | 11.5k / 34.3k tokens | $0.0012 / $0.0005 | correct, no `recall` |
57
+ | C. 164 KB file | GPT-6 Luna | 163k → 26k chars (7 reads) | 7 requests, 0.9 s, $0.0027 | 9.2k / 35.3k tokens | $0.0007 / $0.0005 | correct, no `recall` |
58
+ | B. failing test log | GPT-6 Luna | 15.8k → 8.6k chars | 1 request, 0.8 s, $0.0004 | 5.7k / 7.5k tokens | $0.0022 / $0.0016 | fixed in both runs |
59
+ | D. five topics | Claude Sonnet 5.5, `exchangeBreakEven: false` | 1 old exchange omitted (8.8k chars) | 2 requests, $0.0003 | 8.0k / 12.8k tokens (prompt 5) | $0.017 / $0.006 (prompt 5's first request) | correct, searched again |
60
+
61
+ **D. Several topics in one session.** In the generated test repo, five prompts in a row: which API
62
+ clients retry on 503; a README paragraph for `StatusBar`; a count of HTTP 500 lines in a log; where
63
+ `StatusBadge` is used; and then "back to the clients that retry on 503: which of them also retry on 502?"
64
+ (`node test/e2e/run-e2e.mjs --scenario topics --turns 5`). The README exchange is under 2,000 characters
65
+ and is never judged; the last 2 exchanges before each prompt are never judged either.
66
+
67
+ | Model, configuration | First exchange at the end of prompt 4 | At the end of prompt 5 | Prompt 5 | Context of prompt 5's first request | Main-model cost | Jev |
68
+ |---|---|---|---|---|---|---|
69
+ | GPT-6 Luna, default | judged unrelated (P = 0.07), **deferred**: too small to pay for the cache rewrite in a 4-prompt session | relevant (P = 0.95), kept | correct, no `recall` | 7.5k, 6,656 read from cache | $0.0018 | $0.0005 |
70
+ | Claude Sonnet 5.5, default | unrelated (P = 0.07), deferred | relevant (P = 0.97), kept | correct, no `recall` | 11.5k, 11,049 read from cache | $0.073 | $0.0003 |
71
+ | Claude Sonnet 5.5, `exchangeBreakEven: false` | unrelated (P = 0.08), **omitted** (8.8k characters, 5 entries) | – | correct, searched again | 8.0k: 1,760 read, 6,280 written | $0.087 | $0.0003 |
72
+ | Claude Sonnet 5.5, guard off | – | – | – | 12.8k | $0.083 | – |
73
+
74
+ In this short session the gate kept the first exchange, which the follow-up then needed. With the gate off,
75
+ the omission made the next request rewrite the cache from the omitted exchange on (it comes before the
76
+ question pin) and the model searched again; the smaller context pays off only in longer sessions.
77
+
78
+ **When it doesn't help.**
79
+
80
+ - **The first request after an edit costs more, not less.** The prompt cache only matches up to the
81
+ first changed message, so the next request writes the shorter history again instead of reading the long
82
+ one from cache. In A that made Claude's turn-2 request cost $0.033 instead of $0.016, and GPT's $0.0012
83
+ instead of $0.0005; OpenAI read nothing from cache on that request (in the fixture runs it read 2,560
84
+ tokens instead of 13,824: a partial miss).
85
+ At the per-token prices in these runs, the smaller context earns that back after about 7 more requests
86
+ (Claude, A), about 3 (GPT, A) or about 1 (GPT, C). Before that, the benefit is a smaller context window,
87
+ not a lower bill. Anthropic requests also get a cache breakpoint at the previous question; in a run with
88
+ 11 sequential tool calls it raised the first request's cache read from 1,693 to 5,810 tokens compared
89
+ with `pinAnthropicCache: false` ([CACHE.md](CACHE.md)).
90
+ - **Small or fully needed outputs are left whole.** In A, GPT's 9k-character read of `agent-session.js` was
91
+ `not-worth` (too much of it was needed) and a 6k narrowed search was judged needed whole.
92
+
93
+ Numbers on the generated StatusBadge test repo (96% removed, `recall` of a removed line) are in
94
+ [TESTING.md](TESTING.md#results-pi-103).
95
+
96
+ ## Long autonomous runs
97
+
98
+ **Measured** (Pi 1.0.3, e2e scenario `long`: "fix all failing tests" in a generated project with 10 bugs,
99
+ the full suite after every fix; single runs, not a benchmark; costs are what Pi reports at list price):
100
+
101
+ | Main model | Configuration | Requests | Peak context | Main-model cost | Checkpoints (Jev) | Suite green |
102
+ |---|---|---|---|---|---|---|
103
+ | GPT-6 Luna | guard off | 32 / 37 | 68.4k / 87.7k | $0.020 / $0.030 | – | yes / yes |
104
+ | GPT-6 Luna | `midRun: false` (run end only) | 26 | 86.3k | $0.022 | – (run end $0.006) | yes |
105
+ | GPT-6 Luna | default | 29 | **45.0k** | $0.021 | 2 at requests 16 and 24, 0.8 s each, removed 62k and 60k chars ($0.006 with run end) | yes |
106
+ | GPT-6 Luna | `smallResultMinChars: 0` (large outputs only) | 31 | 72.4k | $0.024 | 1 at request 19, 1.0 s, removed 154k chars ($0.007 with run end) | yes |
107
+ | Claude Sonnet 5.5 | guard off | 14 | 89.8k | $0.41 | – | yes |
108
+ | Claude Sonnet 5.5 | `smallResultMinChars: 0` | 8 | 65.7k | $0.24 | none: too short to pay off (run end $0.003) | yes |
109
+ | Claude Sonnet 5.5 | forced: `midRunBreakEven: false`, `midRunMinAgeTurns: 2`, `midRunBatchChars: 20000`, `smallResultMinChars: 0` | 12 | 43.3k | $0.34 | 3 ($0.0032 with run end) | yes |
110
+
111
+ Context per request (every 5th request), GPT-6 Luna:
112
+
113
+ | Request | 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 |
114
+ |---|---|---|---|---|---|---|---|---|
115
+ | guard off (37 requests) | 3.0k | 14.6k | 29.3k | 41.4k | 52.1k | 62.5k | 73.3k | 83.4k |
116
+ | default (29 requests) | 3.2k | 17.0k | 35.3k | 41.0k | 39.3k | 28.9k | – | – |
117
+
118
+ At the default run's first checkpoint, Jev distilled four superseded test logs (for example 30,164 → 8,163)
119
+ and replaced 4 of 7 small reads of files the agent had already edited with one-line notes (for example
120
+ 3,386 → 145); the next request carried 20.1k instead of 41.0k tokens. No run needed `recall`.
121
+
122
+ **Trade-offs:**
123
+ - **OpenAI Codex reads nothing from cache after any edit** (2,560 or 0 tokens on the next request, after
124
+ every checkpoint measured, wherever the edit was). Each checkpoint re-sends the context uncached once and
125
+ pays back after about 8–10 later requests. In these 26–37-request runs the main-model cost came out
126
+ about the same with and without checkpoints, plus about $0.005 of Jev per run; the gain is a smaller
127
+ context.
128
+ - **On Anthropic a checkpoint rewrites the cache after the latest entry the next request can read** (at worst after the question; writing costs 12.5× the read price).
129
+ In the forced run, the requests after the three checkpoints read 5.8k / 16.7k / 16.7k tokens and wrote
130
+ 10.9k / 22.8k / 20.0k. After the first checkpoint of a run only the system prompt and question can be
131
+ read, because the run's first output is usually the first one edited. Claude solved the task in 8–14
132
+ requests, too few for a checkpoint to pay off, so with the default break-even rule it never
133
+ checkpointed; forced checkpoints in such short runs cost more than they saved. Checkpoints are meant
134
+ for runs that go on for dozens of requests.
135
+ - **Latency mid-run:** a checkpoint pauses the run for the Jev requests (0.5–2.2 s measured, at most
136
+ `timeoutMs`).
137
+ - Details: [CACHE.md](CACHE.md#mid-run-checkpoints), [DESIGN.md](DESIGN.md#mid-run-checkpoints),
138
+ wording probe in [JEV.md](JEV.md#checkpoint-wording).
139
+
140
+ ## Old images
141
+
142
+ Claude Sonnet 5.5 with `pi-claude-auth`, 2026-10-08: six 1200×800 PNGs, read one per message, then a
143
+ follow-up about the second one. The run end removed the four images that were 3–6 turns old, with no Jev
144
+ request (19.9k chars ≈ 5k tokens). The next request carried 2 images instead of 6: 9.4k input tokens
145
+ instead of about 14.1k. Told not to read the file again, the model called `recall`, got the image back and
146
+ read the code number in it correctly.
package/docs/TESTING.md CHANGED
@@ -15,13 +15,15 @@ These need Node ≥ 22.18, which runs the `.ts` files directly. There is no netw
15
15
  |---|---|
16
16
  | `checkpoint.test.ts` | Mid-run eligibility and age, the memo and candidate rules, later calls, notes, superseded detection (edits, re-reads, reruns), cache anchors (read point only from logged entries of the same model within the TTL, invalidated by later edits and compaction, parallel-call batches never split, early return without pending edits), refresh on read-through (same model, cacheRead clearly past the entry, unchanged prefix; trusted after 5 minutes once refreshed), the run baseline, the Anthropic rewrite estimate (from the trusted read point, else from the question), pending edits, the memo from records, the break-even rule, context size with summaries |
17
17
  | `items.test.ts` | Small outputs (size window, candidate rules, age, memo, stub length, superseded), exchange spans (custom messages, completed exchanges only, the current run and the recent exchanges never touched, already omitted, memo; images, foreign edits and foreign omissions skipped, our own markers allowed; system, `!` and custom messages; a compaction that cut an exchange; minimum size), stubs and edits, the small and exchange requests in both phases, batching, the shared limiter, `judgeItems` (parallel limit, failures keep, time budget), `processItems` (stubs, omissions, records with kinds, one `concurrency` across all kinds) |
18
- | `run.test.ts` | Run span (including image-only prompts), question/answer/notes, candidate rules (errors, images, excluded tools, short, already-distilled and other-extension-edited results), steering messages; history (last N exchanges, first request, compaction/branch summaries, image-only and steering prompts, clipping, `historyExchanges` 0) |
18
+ | `run.test.ts` | Run span (including image-only prompts), question/answer/notes, candidate rules (errors, images, excluded tools, short, already-distilled and other-extension-edited results), steering messages (continue the run, join the question); history (last N exchanges, first request, compaction/branch summaries, image-only and steering prompts, clipping, `historyExchanges` 0) |
19
19
  | `chunk.test.ts` | grep detection, grouping by file, context lines and `--`, `-digits-` file names, split/merge rules, plain-text windows, segment limits |
20
20
  | `decide.test.ts` | Checkpoint request shape and wording; run-end request shape, with and without `earlier_conversation` (state order, omitted parts, history wording vs. the unchanged single-run wording); the decision order (error, missing answers, `keep_whole`, strong/weak `whole`, chunks, `none`); `citedFiles` |
21
21
  | `distill.test.ts` | Skips, edits, `removed`, `not-worth`, failures, time budget (including a classifier that never answers and late answers), parent abort, concurrency limit, multi-segment outputs, oversize segments never sent, `keepCitedFiles`, usage sums |
22
22
  | `render.test.ts` | Labels, verbatim chunks with omission lines, `read` offsets, file lists, full removal |
23
23
  | `cache-pin.test.ts` | Question pin: API-key and OAuth payloads, trailing system messages, TTL selection and `ttl-conflict`, string content, tool-result-only messages, over-budget restore. Anchors: question > read > write priority, read anchor inside a batch of tool_result blocks, breakpoint budget (identity and tools breakpoints dropped, then write, then read; the question survives a foreign breakpoint), not found / last message / already marked, normalized tool ids, TTL order, `markedToolResults` |
24
- | `stats.test.ts` | Savings from the projection, run records (checkpoints counted), status texts and colours, cost format |
24
+ | `stats.test.ts` | Savings from the projection (images by pixels), run records (checkpoints counted), lifetime total and `compacted`, status texts (`🛡 0 · Σ−79k`) and colours, cost format |
25
+ | `images.test.ts` | Old images: age rule, keep-turns 0/1/3, stub text and sizes, user images / already removed / foreign-edited results skipped, results the same pass already omits or distills keep that edit (no orphaned tool result) |
26
+ | `size.test.ts` | Image sizes from PNG/JPEG/GIF/WebP headers, image tokens by pixels (not base64 length), thinking and tool-call sizes, `contextChars` with screenshots |
25
27
  | `recall.test.ts` | Original output, pattern (length cap, 4,000-char match window, catastrophic-regex timeout), offset/limit, line and byte caps, oversized lines |
26
28
  | `config.test.ts` | Defaults, validation and ranges (incl. `historyExchanges` 0–10 integer and the `midRun*` settings), `provider/id` parsing, load/save round trip |
27
29
  | `extension.test.ts` | The real entry point (see below) |
@@ -49,8 +51,13 @@ whole exchange, that `pruneExchanges: false` and `smallResultMinChars: 0` turn t
49
51
  break-even rule drops exchanges from a checkpoint batch that would not pay off with them. The run-end
50
52
  exchange gate: a small omission is deferred and the accumulated batch is omitted at a later run end; a big
51
53
  early exchange in a short session is deferred, and omitted with `exchangeBreakEven: false`; on OpenAI Codex
52
- an exchange rides along when the pass edits something else and is deferred when it does not. The fake
53
- session uses Pi's projection shape (an omitted entry stays with no messages).
54
+ an exchange rides along when the pass edits something else and is deferred when it does not. Old images: run
55
+ end removes images 3+ turns old with no Jev request (also with the model missing), `recall` returns them as
56
+ image blocks (a pattern search only names them), nothing is removed twice; a checkpoint fills its batch with
57
+ old images alone and still obeys break-even, `imageKeepTurns: 0` turns it off; images of an earlier run are
58
+ deferred by the run-end gate and removed with `exchangeBreakEven: false`; `recall` of an omitted exchange
59
+ attaches user images and names tool-result images. The fake session uses Pi's projection shape (an omitted
60
+ entry stays with no messages).
54
61
 
55
62
  Test through Pi's loader (or `pi -e`), not a plain Node import of the extension. Plain Node imports of
56
63
  `@earendil-works/pi-coding-agent` can fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, even though Pi loads the
@@ -132,7 +139,7 @@ The summary gets a `long` block: per-request contexts (input + cacheRead + cache
132
139
  totals of input/cacheRead/cacheWrite/output and the main-model cost Pi reports, every `context-guard`
133
140
  record with its phase, saved characters, Jev ms and cost, and the request before and after it (context,
134
141
  cacheRead, cacheWrite), whether the suite is green, `recall` calls, and repeated non-suite tool calls (lost
135
- outputs searched again). Results are in the README ("Long autonomous runs") and in
142
+ outputs searched again). Results are in [RESULTS.md](RESULTS.md#long-autonomous-runs) and in
136
143
  [CACHE.md](CACHE.md#mid-run-checkpoints).
137
144
 
138
145
  **`topics` scenario.** Five prompts in the generated StatusBadge repo: which API clients retry on 503; a
@@ -141,7 +148,7 @@ the API clients that retry on 503: which of them also retry on 502?". The summar
141
148
  the context of each prompt's first request and its usage, the exchange results (phase, prompt, outcome,
142
149
  P), the small-output results, the last prompt's tool calls and whether it used `recall`, and whether its
143
150
  answer names exactly the clients whose file has both `=== 503` and `=== 502` (a crude check: it fails on
144
- answers that also list clients to say they do not qualify). Results are in the README (example D) and in
151
+ answers that also list clients to say they do not qualify). Results are in [RESULTS.md](RESULTS.md#examples) (example D) and in
145
152
  [CACHE.md](CACHE.md#old-exchanges).
146
153
 
147
154
  **API-shape probe.** `CG_OMIT_AT_TURN=2 node test/e2e/run-e2e.mjs --model … --no-guard --ext
@@ -149,6 +156,18 @@ test/e2e/omit-probe.ts` omits the session's first exchange after turn 2 the way
149
156
  sends a stub user message right before the next user message, without the omitted assistant and
150
157
  tool-result messages. Both GPT-6 Luna and Claude Sonnet 5.5 (`pi-claude-auth`) accepted it and answered.
151
158
 
159
+ **Old images, live** (2026-10-08, Claude Sonnet 5.5 with `pi-claude-auth`, ad-hoc): six 1200×800 PNGs read
160
+ one per message, then a follow-up about the second one. Run end removed the four images 3–6 turns old with
161
+ no Jev request (record `requests: 0`, 19.9k chars ≈ 5k tokens); the next request carried 2 images instead
162
+ of 6 (input 14.1k → 9.5k tokens). Told not to read the file again, the model called
163
+ `recall({"entryId":…})`, got the image back and read the code in it correctly.
164
+
165
+ **Session replay.** `node test/e2e/replay-session.ts <session.jsonl> [--from N] [--all] [--summary]` rebuilds Pi's
166
+ projection at every turn of a real session file and prints what the mid-run gates decide (run start, eligible
167
+ large and small outputs, old exchanges, batch size, break-even) and the run-end candidates. No model calls,
168
+ nothing leaves the machine. Without the live cache log it uses the question pin, so its break-even is the
169
+ conservative case.
170
+
152
171
  **Whole-item probe.** `node test/e2e/probe-items.mjs [--reps 2] [--sessions <topics no-guard run dir>]…`
153
172
  sends the hand-written exchanges and small outputs in `test/e2e/probe-data/` (and the `topics` exchanges)
154
173
  to Jev and prints P per item and the accuracy per threshold (results in [JEV.md](JEV.md#old-exchanges)).
@@ -206,14 +225,14 @@ node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/
206
225
  | GPT-6 Luna, badge, with / without guard | turn 2 cacheRead 2,560 / 13,824 |
207
226
  | GPT-6 Luna, badge, with history (default) | 36,113 → ~2.3k chars; 9/9 checks pass (a run in parallel with 4 others had one OpenAI cache miss on turn 2: 8/9) |
208
227
  | GPT-6 Luna / Claude Sonnet 5.5, history on / off | See [JEV.md](JEV.md#earlier-conversation-historyexchanges): 2 rounds × 4 runs |
209
- | GPT-6 Luna / Claude Sonnet 5.5, long (mid-run checkpoints) | See the README, "Long autonomous runs"; all suites green, no `recall` |
210
- | GPT-6 Luna / Claude Sonnet 5.5, topics, guard on / off | See the README, example D |
228
+ | GPT-6 Luna / Claude Sonnet 5.5, long (mid-run checkpoints) | See [RESULTS.md](RESULTS.md#long-autonomous-runs); all suites green, no `recall` |
229
+ | GPT-6 Luna / Claude Sonnet 5.5, topics, guard on / off | See [RESULTS.md](RESULTS.md#examples), example D |
211
230
 
212
231
  See [CACHE.md](CACHE.md) for what the cache numbers mean.
213
232
 
214
233
  ## Real-world examples (`examples.mjs`)
215
234
 
216
- The README's "Examples (measured)" section comes from `test/e2e/examples.mjs`. It runs the same prompts on
235
+ The examples in [RESULTS.md](RESULTS.md#examples) come from `test/e2e/examples.mjs`. It runs the same prompts on
217
236
  real public code twice, with context-guard on and with it off (`--no-guard`), and compares the first
218
237
  request of the follow-up turn.
219
238
 
@@ -243,7 +262,7 @@ model saw.
243
262
  Pi from list prices (most of it Claude in example A; GPT runs used a subscription). About 5 minutes with 4
244
263
  parallel jobs.
245
264
 
246
- **Findings** (details in the README):
265
+ **Findings** (details in [RESULTS.md](RESULTS.md#examples)):
247
266
 
248
267
  - Next-request context, on / off: A Claude 15.5k / 28.2k tokens, A GPT 11.5k / 34.3k, C GPT 9.2k / 35.3k,
249
268
  B GPT 5.7k / 7.5k; all follow-ups correct, none needed `recall`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-context-guard-jev",
3
- "version": "0.2.0",
4
- "description": "Keep your Pi context lean: the Jev classifier trims big tool outputs to the lines your work still needs, drops stale small outputs and unrelated old exchanges, at run end and during long autonomous runs. Verbatim, recallable, cache-aware, savings in the status bar.",
3
+ "version": "0.3.2",
4
+ "description": "Pi extension that removes tool output the agent no longer needs from its context. The Jev classifier keeps the needed lines verbatim; stale small outputs, unrelated old exchanges and old screenshots become one-line notes. Everything can be recalled.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "alandarev",