Proposal: Jev questions and thresholds
September 19, 2026 · View on GitHub
AGENTS.md asks for the Jev questions and thresholds to be proposed before they
are wired in. This file records that proposal for fast-jev. The question
wording is the vendored library's (vendor/fast-jev-compaction/compact.ts), not
this plugin's.
Questions asked per tool call
For every candidate tool call tN the plugin asks two noul judgments in one
Jev request:
| Key | Instructions sent |
|---|---|
call_tN | "Tool call tN (<tool>) should stay in the history: knowing this call was made, with its input, still matters for what the assistant does next" |
result_tN | "The full output of tool call tN (<tool>, N chars) should stay in the history verbatim: the assistant still needs its contents and re-running the tool would not do" |
Each answer is a probability in [0, 1] (the noul field), read with
noulAnswer, and compared against keepThreshold:
keepCall < thresholdandkeepResult < threshold→drop_call: the call and its result leave the outgoing request together.- only
keepResult < threshold, and the result is long enough to shorten →drop_result: the call stays, the result becomes its head plus a note. - otherwise →
keep.
Because the pair is always decided and applied together, the request can never carry an orphan call or an orphan result.
Thresholds
| Threshold | Value | Why |
|---|---|---|
keepThreshold | 0.5 | Matches upstream. Above 0.5 Jev leans "keep", and a wrongly dropped result is paid for by re-running a tool. Must be in [0,1]; an invalid value falls back to 0.5 instead of clamping, so a bad config.json cannot make every eligible call look droppable. |
preserveRecentMessages | 6 | Upstream default. Together with the pinned first message and the preserveCurrentTurn boundary, this is the floor. |
preserveCurrentTurn | true | Request-time pruning is not compaction: everything since the last real user prompt stays, so the live turn can always finish. false is upstream's aggressive behaviour. |
truncateHeadChars | 300 | Upstream default, measured in characters of the result text. |
maxStateTokens | 25000 | Upstream default; the fitting stages shrink the state until it fits. |
maxRequestTokens | 30000 | Upstream default; batches split so that state plus questions fit one request. |
minMessageChars | 60000 | Plugin-only. Below this the plugin does not call Jev at all, so short sessions cost nothing. |
timeoutMs | 8000 | Plugin-only. Hard timeout per Jev attempt; on timeout the request goes out unpruned (fail-open), and timeouts are not retried. |
Guardrails
Two non-Jev limits keep the pruner honest:
| Guard | Default | Why |
|---|---|---|
protectedTools / protectedToolsMode | ["shell","bash","write","edit","patch","question","execute","subagent"], "truncate" | A dropped call erases the memory that the action happened, and some calls are expensive or impossible to reproduce. Protected calls are never dropped; truncate still bounds their result (so the fact they ran survives), pin leaves both completely untouched. Names are matched case-insensitively. patch is grouped with edit/write because an applied patch is a mutation; question is on the list because a user's answer is irreproducible; execute because Code Mode code can call any tool, including writers. An explicit protectedTools list replaces the default, so an existing config must add patch to keep parity. |
maxRewritesPerRequest | 0 (off) | Ceiling on calls dropped plus results truncated in one request, oldest calls first. Off by default because pruning is not cumulative — each request is rebuilt from stored history — so a low ceiling re-applies the same oldest rewrites forever and the backlog never converges (measured on a live long session: 26 % saved with a ceiling of 60, 89 % without). Keep it only as a deliberate blast-radius bound. |
Fail-open bounds
Three checks keep a bad input from becoming a bad rewrite:
- A call is structurally eligible only when it is not pinned and, with a
live-turn boundary, both
callIndexandresultIndexsit strictly before the last real prompt. A call that starts before the prompt but whose result arrives after it stays, so the live turn can never lose an orphan call. - Jev answers are validated at the transport boundary: every question asked must
come back as a finite
noulin[0,1]. A missing, negative, NaN or out-of-range value aborts the whole request and caches nothing, instead of being read as a confident drop. Cached entries are checked the same way on load, so a corrupt entry is re-asked rather than trusted. - Tool payloads are checked before selection: a malformed message (bad role or
non-array content), a
tool-callwithoutinput, or atool-resultthat does not match its variant (textneeds a stringvalue;json/errorneedvalue;contententries needtextor afilewith stringuri/mimeand an optional stringname) fails the whole request open rather than letting the adapter drop half a pair. Provider-executed and non-tool parts are never inspected.
Where this diverges from upstream
- Batching. No fixed 50-calls-per-request cap: batching follows the vendored
library's token budget (
maxRequestTokens), so a huge transcript may need more — and a small one fewer — requests than upstream'sceil(N/50)heuristic. - Transport. Jev is called directly at the configured
baseUrl(defaulthttps://api.typesafe.ai/v1/systemone) instead of shelling out to thejevCLI: the plugin runs inside the OpenCode server and the vendoredJevAskerinterface is HTTP-shaped. KeepbaseUrlon HTTPS — the key is sent to it. - Skip guards. A transcript under three messages, a fully frozen message
array (
Object.isFrozen), or no detectable user prompt (whilepreserveCurrentTurnis on) skips or fails closed instead of pruning. Anevent.messagesproperty that cannot be replaced also leaves the request intact (selection still runs). A partially readonly array — a non-writable index, not a frozen array — still admits a new published view via copy-on-write (selftest 18). Log entries cap their decision list. - Truncation wording.
truncatedResultTextmirrors the library's private truncation note (the original is not exported);selftest.tspins the exact wording so drift fails. - Module layout.
prune.tskeeps orchestration (candidate selection, batching, decisions, stats and logging) in one module on purpose: config lives inconfig.ts, the Jev transport injev.tsand file logging inlog.ts, but splitting the orchestration further would add indirection without a second consumer.
Evidence
- Dry run over live sessions reads
log.jsonl: projected character reduction reached 72–92 % once enabled, with cached steps at 1–5 ms. bun livecheck.tsasks Jev with a synthetic transcript: 2 questions, 1 request, ~600 ms.bun selftest.tspins the adapter, the boundary rules and the decision mapping; it never touches the network.