Configuration
September 19, 2026 · View on GitHub
All fields live in config.json next to the plugin. It is not tracked: the
repo ships config.example.json, which matches the
compiled-in DEFAULTS (observe-only). Copy it to start:
cp config.example.json config.json # optional: the defaults apply without it
config.json is re-read whenever its mtime changes, so edits apply to the next
model request without a restart. Code edits under the plugin directory are
picked up by the reloader too; if something looks stale, run opencode service restart.
Fields
| Field | Code default | Meaning |
|---|---|---|
enabled | true | Master switch. |
dryRun | true | Decide and log, but never modify the request. |
keepThreshold | 0.5 | Minimum Jev probability, in [0,1], for a call/result to stay. Higher is more aggressive (keeps fewer calls); invalid values fall back to 0.5. |
preserveRecentMessages | 6 | Newest messages never touched (the first message is always kept). |
truncateHeadChars | 300 | Head kept when only a result is dropped. |
maxStateTokens | 25000 | Ceiling for the state sent to Jev. |
maxRequestTokens | 30000 | Ceiling for state plus one batch of questions. |
minMessageChars | 60000 | Below this many message characters the hook skips scoring/pruning. |
traceHooks | false | Log every hook invocation, even skipped ones. |
preserveCurrentTurn | true | Never drop anything at or after the last user prompt. |
protectedTools | ["shell","bash","write","edit","patch","question","execute","subagent"] | Tool names whose calls are never dropped; results may still be truncated. Matching is case-insensitive. |
protectedToolsMode | "truncate" | truncate bounds a protected result; pin leaves both alone. |
maxRewritesPerRequest | 0 (off) | Ceiling on rewrites per request, oldest first. Off by default: pruning is not cumulative, so a low ceiling also caps how small a request can get. |
debugDumpMessages | false | Dev-only: write the first hook payload per session to debug/ for fixture captures. Contains the conversation verbatim. |
timeoutMs | 8000 | Hard timeout per Jev attempt; timeouts are not retried. |
retryDelayMs | 400 | Delay before the single transport retry; 0 disables it. Timeouts, aborts and HTTP errors are never retried. |
model, baseUrl | jev-latest, https://api.typesafe.ai/v1/systemone | Jev transport. Keep baseUrl on HTTPS: the API key is sent to it. |
goal | "" | Fixed goal; empty uses the last three user prompts. |
logMaxBytes | 2097152 | Rotate log.jsonl past this size. |
protectedTools replaces the shipped list rather than extending it, so an
existing config.json keeps its own set. When the defaults gain an entry, add
it to your explicit list to match: patch joined the defaults so an applied
patch keeps its call like edit and write (its result is still bounded in
truncate mode).
Common edits
| Want | Edit |
|---|---|
| Turn it off | "enabled": false |
| Observe only | "dryRun": true |
| More aggressive (keep fewer calls) | raise "keepThreshold" (default 0.5) |
| More conservative (keep more calls) | lower "keepThreshold" |
| Allow pruning inside the live turn | "preserveCurrentTurn": false |
| Try it on short sessions | lower "minMessageChars" |
| Pin the goal | "goal": "migrate the auth schema" |
| Never touch mutating tools | "protectedToolsMode": "pin" |
| Protect a different tool set | "protectedTools": ["shell"] |
| Cap rewrites in one request (stalls the backlog) | "maxRewritesPerRequest": 30 — usually leave off |
Environment overrides
TYPESAFE_API_KEY is read from the process environment first and then from
~/.config/opencode/.env (the file the jev CLI uses); the key is sent only to
the configured baseUrl, so keep it on HTTPS. Without a key the plugin stands
down and logs the reason.
FAST_JEV_ENABLED and FAST_JEV_DRYRUN (1/true/yes/on) override the
file for quick A/B. They come from the process environment, so they only change
with an OpenCode restart.
A missing, unreadable or invalid config.json falls back to observe-only
defaults: FAST_JEV_ENABLED=0 can still disable the plugin, but
FAST_JEV_DRYRUN=0 cannot turn pruning on — with no readable config the plugin
must never prune. See SECURITY.md for exactly what is sent
to Jev and what is persisted.
Logs
log.jsonl gets one JSON line per request that reaches the candidate stage:
dryRun, character counts before/after, decision counts, state size, the
fitting stage, request count and latency. savedByDrops and
savedByTruncations split the computed saving by action, and each entry of
changed carries its own savedChars (the first 20 are logged).
The only identifiers it holds are the run's model label (providerID/id), the
configured model string, the configured host, the session id and tool names /
tool-call ids. It never contains prompts, tool results or inputs, file contents,
the goal, the full baseUrl or raw error text. Hook traces are logged only
with traceHooks. A blocking condition (missing key, no user prompt, unreadable
config or .env) logs its first occurrence once per process, and failures log a
fixed category (jev-http (500), jev-transport, jev-answer, internal, …)
rather than the caught error, so a body, URL or cause cannot leak. Cache
lifecycle (cache-loaded, cache-load-error, cache-flush-error) and
/jev-prune (command, command-error) log on their own events.
console.log/console.error lines with the [fast-jev] prefix also land in
the OpenCode server log.
With dryRun: true, callsDropped/resultsTruncated are projections built
from the effective actions (guards, no-op truncations and the rewrite ceiling
already applied); in live runs they are what was actually rewritten. The
residual delta is message-wrapper overhead, which only the live
charsBefore/charsAfter pair measures. To observe first, set dryRun: true,
let a long session run, inspect log.jsonl, then set it back to false.
tail -1 log.jsonl | jq
grep '"event":"prune"' log.jsonl |
jq -r '[.t,.savedRatio,.callsDropped,.resultsTruncated,.askedNow,.ms]|@tsv' |
tail -10
askedNow: 0 means the step was served entirely from the persistent cache;
anything above zero means new Jev questions.
Troubleshooting
grep skip log.jsonl | tail—missing TYPESAFE_API_KEYmeans the server never saw the key.- The session is below
minMessageChars, or every call is pinned (first message, newestpreserveRecentMessages, or the live turn) — nothing to do. config.jsonhasenabled: falseordryRun: true; a fresh clone has noconfig.json, so it runs observe-only by default.- After code edits the reloader usually picks them up;
opencode service restartwhen in doubt.FAST_JEV_ENABLED/FAST_JEV_DRYRUNonly change with a restart.
Decision cache
Decisions are cached per tool_use_id in the plugin's durable storage
(ctx.storage, key decisions, format { v: 1, entries }), so later requests
and reloads can reuse them instead of asking Jev again. The in-memory cache and
the prune summary are per plugin instance and use whatever storage the host
supplies. Persistence is bounded at 4096 entries, evicted in write order (FIFO)
— the cache is read in full on every request, so LRU would not keep anything
different alive. Writes are debounced by two seconds and flushed on unload;
close() is idempotent and awaits the last snapshot. An abrupt stop or a
storage failure can drop writes still inside the debounce window: those calls
are simply re-asked on a later request, as are calls evicted from the cache.
Development
Setup, checks, the SDK type check, fixtures and the recapture recipe live in
CONTRIBUTING.md and
tests/fixtures/README.md. The thresholds and
guardrails behind these defaults are explained in DESIGN.md.