pi-context-guard-jev 0.1.0 → 0.3.1
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 +98 -253
- package/docs/CACHE.md +139 -3
- package/docs/DESIGN.md +263 -39
- package/docs/JEV.md +95 -27
- package/docs/REFERENCE.md +164 -0
- package/docs/RESULTS.md +146 -0
- package/docs/TESTING.md +116 -27
- package/package.json +2 -2
- package/src/cache-pin.ts +170 -0
- package/src/checkpoint.ts +459 -0
- package/src/config.ts +63 -2
- package/src/decide.ts +74 -0
- package/src/distill.ts +9 -2
- package/src/images.ts +89 -0
- package/src/index.ts +299 -58
- package/src/items.ts +446 -0
- package/src/process.ts +213 -0
- package/src/recall.ts +98 -6
- package/src/render.ts +2 -0
- package/src/run.ts +143 -32
- package/src/size.ts +114 -0
- package/src/stats.ts +77 -15
- package/src/types.ts +9 -0
package/docs/DESIGN.md
CHANGED
|
@@ -9,26 +9,35 @@ evidence behind the thresholds are in [JEV.md](JEV.md). Cache behaviour is in [C
|
|
|
9
9
|
|---|---|
|
|
10
10
|
| `src/index.ts` | The only module that uses the Pi API: hooks, `recall` tool, `/guard` command, status bar, classifier lookup |
|
|
11
11
|
| `src/config.ts` | `DEFAULT_CONFIG`, validation (`normalizeConfig`), load/save of `context-guard.json`, `provider/id` parsing |
|
|
12
|
-
| `src/run.ts` | `collectRun`: finds the run that just finished, its question, answer, notes and candidate tool results |
|
|
12
|
+
| `src/run.ts` | `collectRun`: finds the run that just finished, its question, answer, notes and candidate tool results; the shared span and candidate rules |
|
|
13
|
+
| `src/checkpoint.ts` | Mid-run checkpoints: eligible outputs (`collectCheckpoint`), superseded detection, the memo and pending edits from the branch, the read point, the break-even rule |
|
|
13
14
|
| `src/chunk.ts` | `chunkOutput` (grep-aware chunking) and `segmentChunks` (grouping chunks into Jev requests) |
|
|
14
|
-
| `src/decide.ts` | `buildRequest` (the Jev request) and `interpret` (answers → keep set); `citedFiles` |
|
|
15
|
+
| `src/decide.ts` | `buildRequest` (the run-end Jev request), `buildCheckpointRequest` (the mid-run one) and `interpret` (answers → keep set); `citedFiles` |
|
|
15
16
|
| `src/distill.ts` | `distillRun`: chunk, run Jev requests in parallel within a time budget, decide per result, build `context_edit` drafts |
|
|
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; `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) |
|
|
16
21
|
| `src/render.ts` | The replacement text: header, verbatim chunks, omission lines; the `[context-guard]` marker |
|
|
17
|
-
| `src/cache-pin.ts` | Anthropic
|
|
22
|
+
| `src/cache-pin.ts` | Anthropic breakpoints: the previous user question, and the read point after edits (`placeGuardBreakpoints`) |
|
|
18
23
|
| `src/stats.ts` | Savings and Jev usage computed from the session; status-bar text |
|
|
19
|
-
| `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) |
|
|
20
25
|
| `src/types.ts` | Loose structural types for the Pi objects used, so the pure modules can be tested without Pi |
|
|
21
26
|
|
|
22
27
|
Everything except `index.ts` is pure. `distillRun` gets the classifier as an injected function.
|
|
23
28
|
|
|
24
|
-
## The
|
|
29
|
+
## The boundaries: `agent_before_settle` and `turn_end`
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
Run-end distillation happens in `agent_before_settle`, and only when `event.outcome === "completed"` and
|
|
32
|
+
the guard is on. Long runs also get mid-run checkpoints at `turn_end` (see
|
|
33
|
+
[Mid-run checkpoints](#mid-run-checkpoints)); everything below about drafts applies to both.
|
|
27
34
|
|
|
28
35
|
Why there:
|
|
29
36
|
|
|
30
37
|
- The final answer exists, so relevance can be judged against it. Pruning during a run would take raw
|
|
31
38
|
data away from the model while it still needs it, and every edit would break the prompt cache again.
|
|
39
|
+
Mid-run checkpoints accept those costs only for outputs that are several turns old, in rare batches,
|
|
40
|
+
and only when the break-even rule says they pay off.
|
|
32
41
|
- A boundary handler can return session entry drafts. A `context_edit` draft is append-only: it changes
|
|
33
42
|
only what the model sees from then on. The raw entry stays in the session file, the UI and exports, and
|
|
34
43
|
`ctx.sessionManager.getEntry(id)` still returns it. That is what `recall` reads.
|
|
@@ -45,18 +54,24 @@ Hooks used:
|
|
|
45
54
|
| Hook | Use |
|
|
46
55
|
|---|---|
|
|
47
56
|
| `session_start` | Load settings, check the classifier model and credential, draw the status |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
57
|
+
| `turn_end` | Mid-run checkpoint, when a batch is due |
|
|
58
|
+
| `agent_before_settle` | Distill the finished run (skipping outputs judged mid-run) |
|
|
59
|
+
| `before_provider_request` | Anthropic breakpoints (only for `anthropic-messages` models) |
|
|
60
|
+
| `session_tree`, `session_compact` | Redraw the status (checkpoint state is read from the branch anyway) |
|
|
61
|
+
| `agent_settled` | Redraw the status |
|
|
51
62
|
|
|
52
63
|
## Algorithm
|
|
53
64
|
|
|
54
65
|
### 1. Run span
|
|
55
66
|
|
|
56
|
-
`collectRun` walks back through `event.context.contextEntries` (Pi's projected entries) to the
|
|
57
|
-
that
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
75
|
text in the run gives the **answer** (the last text) and the **notes** (all earlier text). Tool calls are
|
|
61
76
|
indexed by id so each result can be matched with its tool name and arguments.
|
|
62
77
|
|
|
@@ -66,7 +81,7 @@ default 3; `0` turns all of it off):
|
|
|
66
81
|
- `summary`: the text (`summary` field) of the latest `compactionSummary` or `branchSummary` message, at
|
|
67
82
|
most 2,000 characters.
|
|
68
83
|
- `exchanges`: the last N earlier exchanges, oldest first. Every user message starts one (steering messages
|
|
69
|
-
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
|
|
70
85
|
800 characters) and the **last** assistant text before the next user message (at most 1,200). Tool calls
|
|
71
86
|
and tool results are left out.
|
|
72
87
|
- `firstRequest`: the first user prompt in the projection (at most 1,000 characters), only when it is not
|
|
@@ -186,6 +201,184 @@ In this order:
|
|
|
186
201
|
usually already cleared at this boundary, so Esc does not cancel it; the time budget is the limit.
|
|
187
202
|
- An exception from the whole pass shows a warning and leaves the run untouched.
|
|
188
203
|
|
|
204
|
+
## Items: what one pass judges
|
|
205
|
+
|
|
206
|
+
Every pass (run end, or a mid-run checkpoint) collects three kinds of item and judges them in parallel
|
|
207
|
+
(`processItems`); their edits and results go into one record.
|
|
208
|
+
|
|
209
|
+
| Kind | What | How it is judged | Dropped item becomes |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `large` | Tool results of the current run of at least `minResultChars` (4,000), under the candidate rules above | Chunked, one request per segment (above) | The kept chunks verbatim with omission lines |
|
|
212
|
+
| `small` | Tool results of the current run from `smallResultMinChars` (400) up to `minResultChars`, under the same rules | Whole: one bool per item, many items per request | A one-line stub: `[context-guard] Omitted the output of bash \`…\` (1.2k chars): judged no longer needed. Full output: recall({"entryId":"…"}).` |
|
|
213
|
+
| `exchange` | Old exchanges (below) | Whole: one bool per exchange, several per request | A stub on the prompt, every other entry omitted |
|
|
214
|
+
|
|
215
|
+
**Small outputs** (`collectSmall`). An output whose stub would not be shorter is skipped. Each item carries
|
|
216
|
+
the tool label, `status: failed` for error results, `superseded` when a later call made it out of date
|
|
217
|
+
(`supersededBy`, as for large outputs mid-run), and the output (at most 4,000 characters). The question is
|
|
218
|
+
"will the agent still need the output in item_N?" mid-run, or "does item_N contain anything that
|
|
219
|
+
final_answer relies on, or that a likely follow-up would need?" at run end; both say that an output marked
|
|
220
|
+
superseded is out of date. An item is kept when P ≥ `smallKeepThreshold` (0.45; probe in
|
|
221
|
+
[JEV.md](JEV.md#small-outputs)). Mid-run, small outputs follow the same age rule and memo as large ones; at
|
|
222
|
+
run end every small output without the marker is judged, including those kept at a checkpoint.
|
|
223
|
+
|
|
224
|
+
**Old exchanges** (`collectExchanges`). An exchange is a user prompt and every entry up to the next one:
|
|
225
|
+
assistant messages, tool calls and results, custom messages such as subagent notices. Eligible are
|
|
226
|
+
exchanges before the current run's user message, without the last `keepRecentExchanges` (2) before it,
|
|
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,
|
|
232
|
+
or an omission by someone else, which Pi keeps in the projection as an entry with no messages), at least
|
|
233
|
+
2,000 characters of editable content, and a stub shorter than that content. A kept exchange is
|
|
234
|
+
judged again in a later run, against that run's work. Compaction and branch summaries are never part of an
|
|
235
|
+
exchange; entries Pi cannot edit (system messages, `!` shell executions) stay in place.
|
|
236
|
+
|
|
237
|
+
Jev sees the current work (`user_question`, `earlier_conversation`, and `agent_progress` mid-run or
|
|
238
|
+
`final_answer` at run end) and, per exchange, the prompt (at most 600 characters), the last assistant text
|
|
239
|
+
(800), the tool calls as `toolLabel()`s (at most 20) and the size. The question: "Is exchange_N still
|
|
240
|
+
relevant to the current work? Relevant means the agent may need its details: the same files, task,
|
|
241
|
+
decisions or facts." An exchange is omitted only when P < `exchangeOmitThreshold` (0.2; probe in
|
|
242
|
+
[JEV.md](JEV.md#old-exchanges)). Then the prompt entry gets the stub
|
|
243
|
+
`[context-guard] Omitted an earlier exchange judged unrelated to the current work: "<first 120 characters of the prompt>" (N messages, ~Mk tokens). Full exchange: recall({"entryId":"<prompt entry id>"}).`
|
|
244
|
+
and every other editable entry gets `replacement: null`, which omits it from model context. The record's
|
|
245
|
+
result for the exchange lists exactly those entry ids (`omitted`), and `recall` on the stub shows exactly
|
|
246
|
+
the prompt and those entries. Tool calls and
|
|
247
|
+
their results are omitted together. The resulting shapes (a stub user message right before the next user
|
|
248
|
+
message; spans without assistant and tool-result messages) are accepted by Anthropic (with
|
|
249
|
+
`pi-claude-auth`) and OpenAI Codex, checked live with `test/e2e/omit-probe.ts` and the `topics` scenario.
|
|
250
|
+
|
|
251
|
+
**Run-end break-even for exchanges** (`exchangeBreakEven`, on). After the pass is judged, its exchange
|
|
252
|
+
omissions are checked: their marginal cache cost is the rewrite with them minus the rewrite the pass's
|
|
253
|
+
other edits cause anyway (`rewriteCharsFor`, with the trusted read point and floor rules of
|
|
254
|
+
[CACHE.md](CACHE.md); on OpenAI Codex any edit costs the whole context, so exchanges ride along for free
|
|
255
|
+
when the pass edits anything else). If that cost is above zero, the omissions must save at least
|
|
256
|
+
`exchangeMinSavingChars` (8,000) and pass `paysOff` with as many later requests as the session has had so
|
|
257
|
+
far (model-visible assistant messages). Otherwise they are deferred: their edits are dropped, their results
|
|
258
|
+
say `kept` / `deferred`, and they are judged again at later run ends against the work of that run, so
|
|
259
|
+
deferred exchanges accumulate until their total pays off. `exchangeBreakEven: false` always omits
|
|
260
|
+
unrelated exchanges.
|
|
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
|
+
|
|
278
|
+
**Batching.** Whole items are grouped into requests of at most `maxSegmentChars` characters of item text and
|
|
279
|
+
`maxChunksPerSegment` (40) items, under the same `timeoutMs` budget; the three kinds run at the same time,
|
|
280
|
+
and one shared limiter keeps at most `concurrency` (6) Jev requests in flight across all of them. An item without an
|
|
281
|
+
answer (error, timeout) is kept and asked again next time.
|
|
282
|
+
|
|
283
|
+
## Mid-run checkpoints
|
|
284
|
+
|
|
285
|
+
A 30–120 minute autonomous run would otherwise carry every tool output until it ends; Pi compacts only
|
|
286
|
+
near the window minus 16k tokens. Checkpoints judge older outputs while the run is still going.
|
|
287
|
+
|
|
288
|
+
**Hook (verified on Pi 1.0.3).** `turn_end` is an actionable boundary (`TurnEndEvent extends
|
|
289
|
+
BoundaryState`). `agent-session.js` dispatches it from `agent.finishTurn`, commits the returned drafts
|
|
290
|
+
(`_commitBoundaryDrafts` → `_refreshFinalizedContext`), and only then runs `prepareNextTurn`, which rebuilds
|
|
291
|
+
the messages from the session projection and runs the turn-end compaction check
|
|
292
|
+
(`_compactBeforeNextAssistantResponse`). So a `context_edit` returned at `turn_end` reaches the very next
|
|
293
|
+
provider request, before any compaction decision; the e2e runs confirm it (the next request's context
|
|
294
|
+
drops by the removed amount). The runner replaces the draft list as for `agent_before_settle`, so the
|
|
295
|
+
handler returns `[...event.entries, ...edits, record]`, and it never asks for `continue`.
|
|
296
|
+
|
|
297
|
+
**When.** At the end of a turn whose assistant message has tool calls (the final turn of a run is left to
|
|
298
|
+
run-end distillation), with `outcome: "completed"` and `midRun` on:
|
|
299
|
+
|
|
300
|
+
1. **Eligible items:** large outputs (`collectCheckpoint`) and small outputs (`collectSmall`) of the current
|
|
301
|
+
run whose call was made at least `midRunMinAgeTurns` (4) turns before the current turn (turn = assistant
|
|
302
|
+
message index in the run) and that are not in the memo, and old exchanges not judged during this run.
|
|
303
|
+
2. **Batch:** their total must reach `midRunBatchChars` (60,000; `midRunBatchCharsOpenAI` for
|
|
304
|
+
`openai-codex` models), so edits come in rare batches.
|
|
305
|
+
3. **Break-even** (`midRunBreakEven`): the checkpoint must be likely to pay for its one-time cache
|
|
306
|
+
rewrite: `pending chars × turns so far ≥ factor × rewrite chars`, with factor 13 and the whole context
|
|
307
|
+
for OpenAI Codex; factor 16 for Anthropic, where the rewrite is the context after the read point the
|
|
308
|
+
next request would get (the latest trusted entry before the first eligible output, `trustedReadPoint`)
|
|
309
|
+
or, without one, after the question (the floor; with `pinAnthropicCache` off, the whole context); and
|
|
310
|
+
factor 16 and the context from the first eligible output on for other providers
|
|
311
|
+
(derivation in `checkpoint.ts` and [CACHE.md](CACHE.md#mid-run-checkpoints)). "Turns so far" stands in
|
|
312
|
+
for the turns still to come. The rewrite starts at the earliest edited entry: an old exchange comes
|
|
313
|
+
before the question, so with exchanges in the batch the Anthropic rewrite is everything after the latest
|
|
314
|
+
trusted entry before that exchange, or the whole context. If the whole batch does not pay off, the rule
|
|
315
|
+
is tried for the tool outputs alone, and the exchanges wait for run end.
|
|
316
|
+
|
|
317
|
+
Then all eligible outputs go to `distillRun` with the checkpoint request builder and
|
|
318
|
+
`chunkKeepThreshold = midRunChunkKeepThreshold` (0.6), under the same `timeoutMs` budget, abort signal and
|
|
319
|
+
keep-on-error rules, with the busy status and working message shown.
|
|
320
|
+
|
|
321
|
+
**The checkpoint request** (`buildCheckpointRequest`; separate from the run-end request): there is no final
|
|
322
|
+
answer yet. The state holds `situation` (the agent is still working; this is an earlier call's output),
|
|
323
|
+
`earlier_conversation`, `user_question`, `agent_progress` (`latest_note`: the latest assistant text, at
|
|
324
|
+
most 2,000 characters; `earlier_notes`: earlier text of the run, clipped from the start to 2,000),
|
|
325
|
+
`later_tool_calls` (labels of the calls made after this output, oldest first; above 30 the first and last
|
|
326
|
+
15 with a gap note), `superseded` when it applies, and then tool, status, arguments, size and chunks as at
|
|
327
|
+
run end. The questions ask whether the agent will still need the output (or chunk) to finish
|
|
328
|
+
`user_question`; out-of-date lines and lines it has already acted on and moved past are not needed. The
|
|
329
|
+
probe behind the wording and the threshold is in [JEV.md](JEV.md#checkpoint-wording).
|
|
330
|
+
|
|
331
|
+
**Superseded** (`supersededBy`), judged from the later tool calls: a `read` whose file was later changed by
|
|
332
|
+
`edit`/`write` ("this file was changed after this read (edit src/a.ts, 3 turns later)") or read again with
|
|
333
|
+
the same offset and limit; a command that ran again (whitespace and a trailing `2>&1` normalized); any other
|
|
334
|
+
tool called again with identical arguments.
|
|
335
|
+
|
|
336
|
+
**State from the branch.** The memo and the pending edits are derived, at that moment, from the entries
|
|
337
|
+
persisted on the active branch (`ctx.sessionManager.getBranch()`). A boundary's drafts are not final: a
|
|
338
|
+
later handler can replace the list, and one invalid draft makes Pi discard all of them; state taken from
|
|
339
|
+
the returned drafts would then skip those outputs forever. Reading the branch also makes `/tree`,
|
|
340
|
+
compaction and reloads correct without any rebuild step. The only in-memory state is the log of cache
|
|
341
|
+
entries this process wrote (below), which only ever adds a read point.
|
|
342
|
+
|
|
343
|
+
**Memo.** Each output is judged mid-run at most once. The memo is the set of output results in the mid-run
|
|
344
|
+
records on the branch, except those Jev never answered for (`timeout`, `error`, `aborted`), which the next
|
|
345
|
+
checkpoint asks again. A "keep" is therefore not re-asked at every checkpoint. Old exchanges have their own
|
|
346
|
+
memo per run (see Items).
|
|
347
|
+
|
|
348
|
+
**Run end judges kept outputs again.** An output kept at a checkpoint (whole, or `not-worth`) is a
|
|
349
|
+
candidate again at run end, this time with the final answer: files the agent was still editing are usually not
|
|
350
|
+
needed once the task is done. Distilled outputs carry the marker and are skipped by the candidate rules.
|
|
351
|
+
|
|
352
|
+
**Anthropic breakpoints** (`placeGuardBreakpoints`). Every Anthropic request gets the previous-question
|
|
353
|
+
pin (the floor; with `pi-claude-auth` that block also carries the relocated system prompt). On the first
|
|
354
|
+
request after **pending edits** (`context_edit` entries on the branch after the last answered assistant
|
|
355
|
+
message, whether from a checkpoint, from run end on the next prompt, or from another extension; a failed or
|
|
356
|
+
aborted response does not count, since its retry repeats the request), `cacheAnchors` adds up to two more:
|
|
357
|
+
|
|
358
|
+
- **Read point**, only at a cache entry this process saw itself write. After placing its breakpoints, the
|
|
359
|
+
extension records every `tool_result` block that carries a breakpoint in the payload (Pi's rolling one
|
|
360
|
+
and its own): entry id, model, time, and the last branch entry at that moment. An entry is trusted only
|
|
361
|
+
if the same model wrote it, or read through it, within the TTL (5 minutes), the entry and that last
|
|
362
|
+
branch entry are still on the branch, and since then no `context_edit` touched anything at or before it
|
|
363
|
+
and no compaction or branch summary was appended. The latest trusted entry before the first edited entry
|
|
364
|
+
is the read point. The log is cleared on `session_start` (and so on reload); anything uncertain falls
|
|
365
|
+
back to the question pin.
|
|
366
|
+
- **Refresh on read-through** (`message_end`, `refreshOnReadThrough`): reading a longer prefix keeps the
|
|
367
|
+
entries on its path alive (CACHE.md), so a response moves a logged entry's time to its request's time
|
|
368
|
+
when the request went to the same model, the entry's prefix is unchanged, and `cacheRead` reaches past
|
|
369
|
+
the entry with a margin: at least the run's baseline (the full input of its first request: system prompt,
|
|
370
|
+
tools, question) plus the projected characters from the question to the entry divided by 1.5. Code and
|
|
371
|
+
logs run at about 2.3 characters per token and prose at about 4, so this overestimates the tokens up to
|
|
372
|
+
the entry. Without a baseline, or with any doubt, nothing is refreshed.
|
|
373
|
+
- **Write anchor:** the end of the tool-result batch holding the first edited entry, so the edited prefix
|
|
374
|
+
gets an entry that a later checkpoint can read. It counts as written only once it was seen in a request
|
|
375
|
+
the extension sent (the same log).
|
|
376
|
+
|
|
377
|
+
Priority under the 4-breakpoint budget: after dropping Pi's redundant identity and tools breakpoints, the
|
|
378
|
+
question comes first, then the read point, then the write anchor. Cache-warming replays add no assistant
|
|
379
|
+
message, so they get the same breakpoints. Measured effect and the TTL findings:
|
|
380
|
+
[CACHE.md](CACHE.md#mid-run-checkpoints).
|
|
381
|
+
|
|
189
382
|
## Replacement format (`render.ts`)
|
|
190
383
|
|
|
191
384
|
Kept chunks are copied verbatim, in order. Each run of removed chunks becomes one omission line. For
|
|
@@ -208,21 +401,27 @@ The draft is `{ type: "context_edit", targetId, replacement: { content: [{ type:
|
|
|
208
401
|
|
|
209
402
|
## The `context-guard` record
|
|
210
403
|
|
|
211
|
-
Each run that made at least one Jev request also appends a custom entry
|
|
404
|
+
Each run (and each mid-run checkpoint) that made at least one Jev request also appends a custom entry
|
|
405
|
+
(`customType: "context-guard"`):
|
|
212
406
|
|
|
213
407
|
| Field | Meaning |
|
|
214
408
|
|---|---|
|
|
215
409
|
| `v` | Format version, `1` |
|
|
410
|
+
| `phase` | `"mid-run"` for checkpoints, `"run-end"` otherwise (a record without it counts as run end) |
|
|
216
411
|
| `model` | Classifier used |
|
|
217
412
|
| `savedChars` | Characters removed by this run's edits |
|
|
218
413
|
| `requests`, `inputTokens`, `costUsd`, `ms` | Jev usage for the run |
|
|
219
414
|
| `timedOut` | The time budget ran out |
|
|
220
|
-
| `results[]` | One per
|
|
415
|
+
| `results[]` | One per item: `kind` (`large`, `small`, `exchange`; absent means large), `entryId` (for an exchange, its prompt entry), `tool`, `label`, `outcome` (`distilled`, `removed`, `kept`), `reason`, `beforeChars`, `afterChars`, `jev` |
|
|
221
416
|
|
|
222
417
|
`reason` is one of `chunks`, `none-needed`, `not-worth`, `all-chunks`, `whole-needed`, `whole-chosen`,
|
|
223
|
-
`no-answer`, `error` or `timeout` (several are joined with commas)
|
|
418
|
+
`no-answer`, `error` or `timeout` (several are joined with commas) for large outputs, `needed` or
|
|
419
|
+
`not-needed` for small outputs, and `relevant`, `unrelated` or `deferred` for exchanges (an omitted exchange's
|
|
420
|
+
result also has `omitted`, the entry ids that got `replacement: null`). Whole items have the trace
|
|
421
|
+
`p.06` (P of the bool). `jev` holds one trace per segment, such as
|
|
224
422
|
`kw.15 focus=chunk_4:.52 keep 9/29` or `kw.82 focus=whole:.71 keep all (whole-needed)`. That is
|
|
225
|
-
P(`keep_whole`), the focus choice and its probability, and the result. `/guard` prints these for the last
|
|
423
|
+
P(`keep_whole`), the focus choice and its probability, and the result. `/guard` prints these for the last
|
|
424
|
+
record (run or checkpoint) and counts the checkpoints.
|
|
226
425
|
|
|
227
426
|
The extension does not write Jev's usage into Pi's own usage entries. It is recorded only here.
|
|
228
427
|
|
|
@@ -231,48 +430,73 @@ The extension does not write Jev's usage into Pi's own usage entries. It is reco
|
|
|
231
430
|
Nothing is kept in memory. Every redraw recomputes from the session:
|
|
232
431
|
|
|
233
432
|
- **Savings:** Pi's session projection (`buildSessionProjection()`) is checked for tool results whose
|
|
234
|
-
model-visible text starts with `[context-guard]` while the raw entry does not
|
|
235
|
-
minus visible length.
|
|
236
|
-
|
|
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
|
|
437
|
+
entry in the projection with no messages) minus the stub.
|
|
438
|
+
Tokens are estimated as characters ÷ 4.
|
|
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.
|
|
237
442
|
|
|
238
443
|
So the numbers follow branches, `/tree`, reloads and compaction.
|
|
239
444
|
|
|
240
|
-
The ready-state footer is `🛡 −4.2k · 1`: ≈ tokens kept out of context · distilled
|
|
241
|
-
in the theme's success colour, the rest dimmed). Before
|
|
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).
|
|
242
449
|
|
|
243
450
|
## Recall
|
|
244
451
|
|
|
245
452
|
`recall` reads the raw entry with `ctx.sessionManager.getEntry(entryId)`. `context_edit` never changes that
|
|
246
|
-
entry.
|
|
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
|
|
455
|
+
returns a transcript of the raw exchange from the branch: `## user`, `## assistant` with the text and
|
|
456
|
+
`[tool call] name {arguments}` lines, `## tool result (tool)` and `## custom message (type)` sections, for
|
|
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
|
|
247
460
|
`offset`/`limit` window. Output is capped at 2,000 lines and 50 KB, with a continuation note; a single
|
|
248
461
|
line longer than 50 KB is cut with a `[line N cut at 50KB]` note. `pattern` is a JavaScript regex of at most
|
|
249
462
|
500 characters, matched against the first 4,000 characters of each line inside a `node:vm` sandbox with a
|
|
250
463
|
1-second timeout, so a catastrophic pattern returns an error instead of freezing Pi. An id that is
|
|
251
|
-
|
|
464
|
+
neither a tool result nor a user prompt on the branch gives an error that tells the model to use the id
|
|
465
|
+
from a `[context-guard]` note.
|
|
252
466
|
|
|
253
467
|
## Pitfalls
|
|
254
468
|
|
|
255
|
-
- **Never
|
|
256
|
-
|
|
257
|
-
- **Never
|
|
258
|
-
|
|
259
|
-
-
|
|
260
|
-
|
|
469
|
+
- **Never advance state on returned drafts.** A later handler may replace them, or Pi may reject the list;
|
|
470
|
+
read state back from the branch.
|
|
471
|
+
- **Never omit half of a tool call.** `replacement: null` on a tool result alone would leave its `tool_use`
|
|
472
|
+
without a result, and providers reject the request. A single output that is not needed is replaced with
|
|
473
|
+
a one-line stub; `null` is only used for whole exchanges, which omit the assistant message with the call
|
|
474
|
+
and its results together.
|
|
475
|
+
- **Never replace an assistant message's content.** A replacement turns the content into one text block,
|
|
476
|
+
which drops thinking, signatures and tool calls. Assistant messages are only omitted, as part of a whole
|
|
477
|
+
exchange.
|
|
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.
|
|
261
488
|
- **`keepCitedFiles` is off by default.** Answers also name files to rule them out. In a live run, Claude's
|
|
262
489
|
answer said `test/fixtures/entities.json` was *not* a usage, and the option kept 40 lines of noise from
|
|
263
490
|
that file. See [JEV.md](JEV.md).
|
|
264
|
-
- **
|
|
265
|
-
point on
|
|
491
|
+
- **Earlier runs are only edited as whole exchanges.** Editing single outputs of older runs would break the
|
|
492
|
+
prompt cache from that point on for little gain; an old exchange is omitted as a whole, once, when it is
|
|
493
|
+
unrelated to the current work, and only its first later request pays the rewrite.
|
|
266
494
|
- **Don't distill from a `context` hook.** Output that varies between requests makes every request miss the
|
|
267
495
|
cache.
|
|
268
496
|
|
|
269
497
|
## Open questions
|
|
270
498
|
|
|
271
|
-
1.
|
|
272
|
-
If not, warm requests do not carry the question breakpoint. Not checked.
|
|
273
|
-
2. Should the pin go on the last unedited tool result instead of the question? That would keep more of the
|
|
274
|
-
run cached on the next prompt. Not tried.
|
|
275
|
-
3. Other classifier entries (e.g. `typesafe/jev-latest` with a 64k context) and non-code outputs have
|
|
499
|
+
1. Other classifier entries (e.g. `typesafe/jev-latest` with a 64k context) and non-code outputs have
|
|
276
500
|
not been tested.
|
|
277
|
-
|
|
501
|
+
2. The OpenAI Codex WebSocket transport loses its delta continuation after an edit
|
|
278
502
|
([CACHE.md](CACHE.md#openai)). It is not clear whether anything can be done about that from an extension.
|
package/docs/JEV.md
CHANGED
|
@@ -53,7 +53,7 @@ question per chunk separated them clearly: cited files 0.90–0.93, noise 0.14
|
|
|
53
53
|
A 30k-character grep with 28 chunks and 30 questions: **13,155 input tokens, 745 ms, $0.00055**. Code is
|
|
54
54
|
about 2.3 characters per token.
|
|
55
55
|
|
|
56
|
-
**Worst-case budget probe** (
|
|
56
|
+
**Worst-case budget probe** (with earlier conversation in the state): every history part at its
|
|
57
57
|
clip limit (2,000-character summary, 1,000-character first request, 800 + 1,200 characters per exchange),
|
|
58
58
|
a 4,000-character question, a 6,000-character answer, 2,000 characters of notes, and one 40-chunk segment of
|
|
59
59
|
31,848 characters of dense `rg` output over Pi's `dist/core`:
|
|
@@ -64,8 +64,8 @@ a 4,000-character question, a 6,000-character answer, 2,000 characters of notes,
|
|
|
64
64
|
| 10 (maximum) | 83,214 | **23,619** | 799 ms | $0.00099 |
|
|
65
65
|
|
|
66
66
|
Both fit Jev's 32k-token context with room to spare (the target was ≤ 28k), so `maxSegmentChars` is
|
|
67
|
-
**32,000
|
|
68
|
-
|
|
67
|
+
**32,000**, which keeps the worst case well below 32k with history included, and `maxChunksPerSegment`
|
|
68
|
+
is 40. Nested objects in the state (`earlier_conversation`) are accepted.
|
|
69
69
|
|
|
70
70
|
In the live end-to-end runs, one qualifying run took 2 Jev requests: 550 ms and $0.00083 (GPT-6 Luna run), and
|
|
71
71
|
1.18 s and $0.00084 (Claude run).
|
|
@@ -98,14 +98,14 @@ noise. It is off by default.
|
|
|
98
98
|
### Small reads are not worth it
|
|
99
99
|
|
|
100
100
|
`read` results of about 2.3k characters always ended as `not-worth`: Jev kept 2 of 3 chunks. So
|
|
101
|
-
`minResultChars`
|
|
101
|
+
`minResultChars` is 4,000.
|
|
102
102
|
|
|
103
103
|
### Earlier conversation (`historyExchanges`)
|
|
104
104
|
|
|
105
105
|
Without history, Jev only sees the run's own question and answer. A broad search in a session whose goal was
|
|
106
106
|
stated earlier ("I'm about to migrate every API client to a shared retry helper") then loses exactly the
|
|
107
107
|
lines the ongoing work needs, because this run's question ("where is StatusBadge used?") does not mention
|
|
108
|
-
them. So the state
|
|
108
|
+
them. So the state carries `earlier_conversation`: the latest compaction/branch summary, the first
|
|
109
109
|
prompt and the last 3 exchanges (prompt + last assistant text), clipped (docs/DESIGN.md).
|
|
110
110
|
|
|
111
111
|
**Wording.** Offline A/B on the real turn-2 run of the e2e `history` scenario (Claude's `rg -n -i status`,
|
|
@@ -126,28 +126,93 @@ still borderline (the relevant chunks sit around the threshold), so history make
|
|
|
126
126
|
what the ongoing work needs, not reliably all of it; `recall` remains the safety net.
|
|
127
127
|
|
|
128
128
|
**Live A/B** (`node test/e2e/run-e2e.mjs --scenario history`, with and without `--config
|
|
129
|
-
'{"historyExchanges":0}'`). Turn 1 states the retry-migration goal,
|
|
130
|
-
turn 3 asks which clients retry on 429 without a new search (9 of
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| Model, history |
|
|
134
|
-
|
|
135
|
-
| GPT-6 Luna, on |
|
|
136
|
-
| GPT-6 Luna, off |
|
|
137
|
-
| Claude Sonnet 5.5, on | 1 |
|
|
138
|
-
| Claude Sonnet 5.5, off |
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
'{"historyExchanges":0}'`), with the wording `src/decide.ts` sends. Turn 1 states the retry-migration goal,
|
|
130
|
+
turn 2 is the broad StatusBadge search, turn 3 asks which clients retry on 429 without a new search (9 of
|
|
131
|
+
the 10 clients do); one run per cell:
|
|
132
|
+
|
|
133
|
+
| Model, history | src/api retry lines kept in turn 2 (of 80; with 429) | Turn-2 output, chars before → after | Jev | Turn 3 used `recall` | Turn 3 answer |
|
|
134
|
+
|---|---|---|---|---|---|
|
|
135
|
+
| GPT-6 Luna, on | **72 (21)** | 36,113 → 12,402 | 568 ms | yes | correct |
|
|
136
|
+
| GPT-6 Luna, off | 0 | 36,113 → 1,753 | 564 ms | yes | correct |
|
|
137
|
+
| Claude Sonnet 5.5, on | 5 (1) | 22,641 → 2,103 (Claude ran `rg … \| head -200`) | 555 ms | yes | missed `ticketStatus` (cut by Claude's own `head -200`) |
|
|
138
|
+
| Claude Sonnet 5.5, off | 0 | 36,113 → 2,638 | 547 ms | yes | correct list; headline says "eight" |
|
|
139
|
+
|
|
140
|
+
With history, GPT kept 72 of the 80 retry lines (9 of the 10 clients), at the cost of keeping 12.4k instead
|
|
141
|
+
of ~1.8k characters; Claude ran a truncated search and kept 5. A round with the second wording of the
|
|
142
|
+
table above kept no retry lines with history. History did not change turn-3 behaviour: the models used
|
|
143
|
+
`recall` either way, and the answers were about as good. Where it pays off is the context the model already
|
|
144
|
+
has after turn 2. Single runs per cell; the variance between runs is large.
|
|
145
|
+
|
|
146
|
+
### Checkpoint wording
|
|
147
|
+
|
|
148
|
+
Mid-run checkpoints ask a different question: there is no final answer, so Jev judges whether the agent
|
|
149
|
+
will still need an output to finish the task (`buildCheckpointRequest`; state and wording in
|
|
150
|
+
[DESIGN.md](DESIGN.md#mid-run-checkpoints)). Probed offline on real mid-run states from two no-guard runs
|
|
151
|
+
of the e2e `long` scenario (`fix all failing tests`), two repetitions per variant, reporting the share of
|
|
152
|
+
the output's characters that would be kept at chunk thresholds 0.5 / 0.55 / 0.6:
|
|
153
|
+
|
|
154
|
+
| Output (state) | What should happen | base wording | "acted" wording |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| GPT, 6 old `npm test` logs, each rerun later (`superseded`) | mostly go | 0.5: 15–69%, 0.6: **8–36%** | 0.5: 15–45%, 0.6: 15–30% |
|
|
157
|
+
| Claude, first `npm test` log (still the only full failure list; later runs were filtered) | keep the unresolved failures | 0.5: 73–80%, 0.6: 35–41% | 0.5: 80–87%, 0.6: 33–46% |
|
|
158
|
+
| Claude, `cat` of 5 source files, **all fixed since** | go | 0.5: 49–62%, 0.6: **11%** | 11% at every threshold |
|
|
159
|
+
| Claude, the same `cat` one turn after it ran, **files not fixed yet** | stay | 0.5: 100%, 0.6: **78%** | 0.5: 90%, 0.6: **10%** |
|
|
160
|
+
|
|
161
|
+
- *base*: "To finish user_question, will the agent still need the lines in chunk_N? Lines that are out of
|
|
162
|
+
date or that the agent has already acted on and moved past are not needed."
|
|
163
|
+
- *acted*: "Will the agent need to look at chunk_N again before it finishes user_question? Say no if
|
|
164
|
+
later_tool_calls show the agent already acted on it, or if superseded says a newer version exists."
|
|
165
|
+
It dropped the in-use files (10%) and was rejected.
|
|
166
|
+
- A stronger `superseded` sentence ("the agent still sees that newer output…"), with the acted wording, did not help (15–65% at 0.5).
|
|
167
|
+
|
|
168
|
+
Mid-run probabilities are compressed (per-chunk 0.14–0.76, against 0.14–0.93 at run end). At 0.5,
|
|
169
|
+
superseded logs and finished files kept 50–70%, which `maxKeepRatio` (0.6) would then turn into "not
|
|
170
|
+
worth it", so most checkpoints would edit nothing. **Base wording at 0.6** separated the cases: in-use
|
|
171
|
+
files 78%, finished files 11%, superseded logs 8–36%. So `midRunChunkKeepThreshold` defaults to 0.6, the
|
|
172
|
+
same as run end, not lower: the brief proposed starting at 0.5 to keep more, and the probe showed that
|
|
173
|
+
0.5 keeps nearly everything.
|
|
174
|
+
|
|
175
|
+
### Old exchanges
|
|
176
|
+
|
|
177
|
+
`buildExchangeRequest` asks one bool per exchange: "Is exchange_N still relevant to the current work
|
|
178
|
+
(user_question and what the agent is doing for it)? Relevant means the agent may need its details: the
|
|
179
|
+
same files, task, decisions or facts." Probed with `node test/e2e/probe-items.mjs` (2 repetitions):
|
|
180
|
+
|
|
181
|
+
- three hand-written sessions (`test/e2e/probe-data/exchanges.json`) modeled on the shape of real long
|
|
182
|
+
main-thread sessions (a prompt, a final answer, tool-call labels, a size; 5 old exchanges each, mixed
|
|
183
|
+
relevant and unrelated to the current task);
|
|
184
|
+
- the eligible exchanges at prompts 4 and 5 of two no-guard runs of the `topics` scenario (GPT and Claude,
|
|
185
|
+
public test repo): the first exchange (API retries) is unrelated to prompt 4 (`StatusBadge`) and relevant
|
|
186
|
+
to prompt 5 (its follow-up).
|
|
187
|
+
|
|
188
|
+
| Label | P(relevant), 42 answers |
|
|
189
|
+
|---|---|
|
|
190
|
+
| relevant (16) | 0.47–0.96 (the weakest: "add a disk-usage alert for the backup volume" for a failing-backup task, 0.47–0.49; "how is contextTokens estimated with edits" for a compaction-settings task, 0.68) |
|
|
191
|
+
| unrelated (26) | 0.03–0.10 |
|
|
192
|
+
|
|
193
|
+
| Omit when P < | Accuracy | Relevant exchanges omitted | Unrelated exchanges omitted |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| 0.1 – 0.4 | 42/42 | 0/16 | 26/26 |
|
|
196
|
+
| 0.5 | 41/42 | 1/16 | 26/26 |
|
|
197
|
+
|
|
198
|
+
`exchangeOmitThreshold` is 0.2: omit only when Jev is clearly sure, with a wide margin to the weakest
|
|
199
|
+
relevant exchange (0.47). The sessions are synthetic or generated, so real sessions may be less clear-cut;
|
|
200
|
+
the margin is meant to absorb that. Repetitions agreed within 0.03.
|
|
201
|
+
|
|
202
|
+
### Small outputs
|
|
203
|
+
|
|
204
|
+
`buildSmallRequest` asks one bool per output ("will the agent still need the output in item_N?" mid-run;
|
|
205
|
+
"does item_N contain anything that final_answer relies on, or that a likely follow-up would need?" at run
|
|
206
|
+
end), with `superseded` shown when a later call made the output out of date.
|
|
207
|
+
|
|
208
|
+
| Probe | Needed | Not needed |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| Two hand-written mid-run states on the `long` fixture's code (`test/e2e/probe-data/small.json`), 24 answers | 0.59–0.74 | 0.06–0.32 |
|
|
211
|
+
| A `long` run of GPT-6 Luna (public fixture), mid-run at request 12 and at run end: reads of source files | not superseded: 0.48–0.68 | superseded (the file was edited afterwards): 0.19–0.41 |
|
|
212
|
+
|
|
213
|
+
At 0.6 (the chunk threshold) 2 of 10 needed outputs would be dropped; at 0.45 none, and every superseded or
|
|
214
|
+
unneeded output goes. `smallKeepThreshold` is 0.45. Without the `superseded` hint, Jev kept reads of files
|
|
215
|
+
the agent had already edited (0.49–0.86), so the hint is part of the request.
|
|
151
216
|
|
|
152
217
|
## The final question set
|
|
153
218
|
|
|
@@ -175,5 +240,8 @@ answer did not quote but that a natural next question would need.
|
|
|
175
240
|
| `noneThreshold` | 0.5 | Remove a whole output only if `none` wins and no chunk passes its own question |
|
|
176
241
|
| `maxKeepRatio` | 0.6 | If most of the output is kept anyway, the saving does not justify the cache miss and the lost context |
|
|
177
242
|
| `minResultChars` | 4,000 | Reads around 2.3k characters were never worth an edit |
|
|
243
|
+
| `smallKeepThreshold` | 0.45 | Small-output probe above: needed outputs ≥ 0.48, superseded or unneeded ≤ 0.41 |
|
|
244
|
+
| `exchangeOmitThreshold` | 0.2 | Exchange probe above: unrelated ≤ 0.10, relevant ≥ 0.47; omit only when Jev is clearly sure |
|
|
245
|
+
| `midRunChunkKeepThreshold` | 0.6 | Checkpoint probe above: 0.5 kept 50–100% of everything; 0.6 kept in-use files and dropped finished ones |
|
|
178
246
|
|
|
179
247
|
When an answer is missing, the request fails or the budget runs out, the extension keeps everything.
|