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/README.md +111 -409
- package/docs/CACHE.md +5 -1
- package/docs/DESIGN.md +60 -24
- package/docs/REFERENCE.md +164 -0
- package/docs/RESULTS.md +146 -0
- package/docs/TESTING.md +29 -10
- package/package.json +2 -2
- package/src/checkpoint.ts +39 -16
- package/src/config.ts +8 -1
- package/src/images.ts +89 -0
- package/src/index.ts +87 -34
- package/src/items.ts +13 -14
- package/src/process.ts +45 -0
- package/src/recall.ts +44 -11
- package/src/render.ts +2 -0
- package/src/run.ts +71 -11
- package/src/size.ts +114 -0
- package/src/stats.ts +50 -18
- package/src/types.ts +2 -0
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` |
|
|
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
|
|
66
|
-
that
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
current run (`exchangeMemo`: exchange results in records after the
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
412
|
-
minus visible
|
|
413
|
-
|
|
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
|
-
- **
|
|
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
|
|
421
|
-
exchanges (the first part
|
|
422
|
-
|
|
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
|
|
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
|
|
431
|
-
|
|
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
|
|
451
|
-
from before it
|
|
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).
|
package/docs/RESULTS.md
ADDED
|
@@ -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.
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
210
|
-
| GPT-6 Luna / Claude Sonnet 5.5, topics, guard on / off | See
|
|
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
|
|
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
|
|
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
|
|
4
|
-
"description": "
|
|
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",
|