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

FieldCode defaultMeaning
enabledtrueMaster switch.
dryRuntrueDecide and log, but never modify the request.
keepThreshold0.5Minimum 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.
preserveRecentMessages6Newest messages never touched (the first message is always kept).
truncateHeadChars300Head kept when only a result is dropped.
maxStateTokens25000Ceiling for the state sent to Jev.
maxRequestTokens30000Ceiling for state plus one batch of questions.
minMessageChars60000Below this many message characters the hook skips scoring/pruning.
traceHooksfalseLog every hook invocation, even skipped ones.
preserveCurrentTurntrueNever 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.
maxRewritesPerRequest0 (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.
debugDumpMessagesfalseDev-only: write the first hook payload per session to debug/ for fixture captures. Contains the conversation verbatim.
timeoutMs8000Hard timeout per Jev attempt; timeouts are not retried.
retryDelayMs400Delay before the single transport retry; 0 disables it. Timeouts, aborts and HTTP errors are never retried.
model, baseUrljev-latest, https://api.typesafe.ai/v1/systemoneJev transport. Keep baseUrl on HTTPS: the API key is sent to it.
goal""Fixed goal; empty uses the last three user prompts.
logMaxBytes2097152Rotate 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

WantEdit
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 sessionslower "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

  1. grep skip log.jsonl | tailmissing TYPESAFE_API_KEY means the server never saw the key.
  2. The session is below minMessageChars, or every call is pinned (first message, newest preserveRecentMessages, or the live turn) — nothing to do.
  3. config.json has enabled: false or dryRun: true; a fresh clone has no config.json, so it runs observe-only by default.
  4. After code edits the reloader usually picks them up; opencode service restart when in doubt. FAST_JEV_ENABLED/FAST_JEV_DRYRUN only 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.