SLOPSHOPPER

jevcomp

Jev decides what Claude Code keeps when it compacts, instead of a model-written summary.

newprocess
v0.7.6MITupdated 2026-10-09jevcomp/jevcomp
A shopper browsing a rack in a slop shop
README

jevcomp

When a conversation gets long, Codex, Claude Code and Antigravity compact it: they replace the old history with a summary written by the model. Exact details get lost that way (an error message, a file already read, a test result), and writing the summary costs a large request.

jevcomp uses Jev, a small, fast AI model that only answers yes/no questions, to decide which old command outputs still matter:

  • Codex: started with jevcomp codex, the compaction request never reaches OpenAI: jevcomp answers it with what Jev kept.
  • Claude Code: there is no summary. Jev removes or shortens old command outputs and the rest of the conversation stays word for word, which also skips the summary request.
  • Antigravity (experimental): jevcomp agy runs it through jevcomp, but saves nothing yet.

Your own messages are never removed. If anything fails, the agent compacts the way it normally does. Each compaction makes a few small Jev requests, billed to your OpenRouter or TypeSafe key.

Before you start

  • Node.js 20 or newer (nodejs.org).
  • Git, used to download jevcomp.
  • An API key from OpenRouter or TypeSafe.

Install

The same for every agent:

npm install -g --install-links github:jevcomp/jevcomp
jevcomp install

install asks which agents to connect (Codex, Claude Code, Antigravity or all), the provider and the key. To skip the questions: jevcomp install openrouter codex (or typesafe, and claude, agy or all). Run it again to change the key or add an agent. The agents share the key, the settings, the history and the dashboard.

Use

After jevcomp install, open each agent like this:

AgentOpen it withWhat changes
Codexjevcomp codexSaves tokens: jevcomp answers the compaction itself. Plain codex saves nothing.
Claude Codeclaude (as usual)Saves tokens on every compaction. The first time, restart Claude Code once.
Antigravity (experimental)jevcomp agyRuns through jevcomp but saves nothing yet: jevcomp does not know yet which Antigravity request is the compaction, so it has nothing to replace.
  • Codex: always start it with jevcomp codex. To force a compaction and see it work, type /compact.
  • Antigravity: jevcomp install agy adds a local certificate so jevcomp can read the conversation with the model (the Google sign-in passes through untouched); on Windows, click Yes once in the warning. It needs OpenSSL (winget install ShiningLight.OpenSSL.Light). Without the certificate, jevcomp agy opens plain Antigravity. JEVCOMP_CAPTURE=1 saves the requests it sees.
  • Dashboard: http://127.0.0.1:43127/ opens with Codex and Claude Code sessions; jevcomp dashboard opens it by hand.

Claude Code only: as a plugin

Claude Code can also get jevcomp as a plugin, without npm:

claude plugin marketplace add jevcomp/jevcomp
claude plugin install jevcomp@jevcomp

It saves the same as the npm install. The key comes from one saved by jevcomp install, from OPENROUTER_API_KEY / TYPESAFE_API_KEY, or from /plugin configure jevcomp@jevcomp in Claude Code.

Check

Run jevcomp doctor (plugin: ask Claude Code Check jevcomp). It is working when the agent shows jevcomp dashboard: http://127.0.0.1:43127/ as a session starts.

How Codex saves tokens

jevcomp codex [args] runs Codex through a local proxy. When Jev returns a usable result that meets the minimum reduction setting, the proxy answers Codex's compaction request itself instead of sending it to OpenAI. If Jev fails, or its result is too big or cuts too little, the request goes to OpenAI as usual. In the dashboard, a compaction answered by Jev shows as completed.

Uninstall

RunWhat it removes
jevcomp uninstalljevcomp from every agent, and stops the dashboard
jevcomp uninstall codex / claude / agyOnly from that agent (agy also removes the certificate); the dashboard stays while another agent uses it
claude plugin uninstall jevcomp@jevcomp then claude plugin marketplace remove jevcompThe Claude Code plugin

Remove everything

  1. Run jevcomp uninstall (and remove the Claude Code plugin, if you used it).
  2. Remove the command: npm uninstall -g jevcomp.
  3. Delete your key and settings, ~/.config/jevcomp, and your history, ~/.jevcomp.
  4. Claude Code only: remove the "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" line that jevcomp added under env in ~/.claude/settings.json, unless another plugin needs it.

Dashboard

Open http://127.0.0.1:43127/. It starts by itself when a Codex or Claude Code session starts and keeps running until you restart the computer.

  • Forgot the address? Ask the agent Where is the jevcomp dashboard? (or run jevcomp doctor).
  • Restart it: jevcomp dashboard, or ask the agent to restart the jevcomp dashboard.
  • Another port: set JEVCOMP_DASHBOARD_PORT, for example 43200.

It shows only what jevcomp can measure: characters before and after each compaction, what was sent back, each Keep / Shorten / Remove decision with its risk, Jev requests and tokens, and runs that were skipped or failed. It does not estimate billing-token savings.

Settings

Run jevcomp settings, use the Configurações page of the dashboard, or ask the agent to change a jevcomp setting. Each program has its own settings. The defaults suit most people.

SettingDefaultMeaning
pin-recent-messages6Newest messages Jev never touches.
loss-threshold0.5Jev removes or shortens an output only when its risk of losing something still needed is below this. Higher cuts more.
min-reduction-ratio0.15jevcomp acts only when it cuts at least this share of the text.

Settings are saved in ~/.config/jevcomp/settings.json. The variables JEVCOMP_PIN_RECENT_MESSAGES, JEVCOMP_LOSS_THRESHOLD and JEVCOMP_MIN_REDUCTION_RATIO override them.

Advanced options

Rarely needed; set them as environment variables.

OptionDefaultWhat it controls
JEVCOMP_CONCURRENCY4Jev requests at the same time.
JEVCOMP_MAX_STATE_TOKENS24000Size of the conversation copy shown to Jev.
JEVCOMP_MAX_REQUEST_TOKENS30000Size of one Jev request.
JEVCOMP_TRUNCATE_HEAD_CHARS300Characters kept when an output is shortened.
JEVCOMP_TIMEOUT_MS20000Time limit for one Jev request.
JEVCOMP_RETRIES1Retries after a network failure.
JEVCOMP_GOALautomaticTask description given to Jev instead of your recent messages.
JEVCOMP_DATA_DIR~/.jevcompWhere history and session files go.

JEV_MODEL, JEV_BASE_URL, OPENROUTER_JEV_MODEL, OPENROUTER_JEV_URL and OPENROUTER_HTTP_REFERER change the Jev endpoint, for development.

API key

jevcomp install (or Configure jevcomp with ... in Codex) saves the provider and key in ~/.config/jevcomp; run it again to change them. OPENROUTER_API_KEY or TYPESAFE_API_KEY in the environment take precedence, with JEVCOMP_PROVIDER to pick one. A key can also come from a file: OPENROUTER_API_KEY_FILE, TYPESAFE_API_KEY_FILE or JEVCOMP_KEY_FILE.

How it works

For each finished command, Jev answers two questions: would removing the command and its output lose something still needed, and would shortening the output. The answer becomes Keep, Shorten (the first characters plus a note) or Remove (the command can be run again). Your messages and Codex's instructions are never removed. Common secret patterns are hidden before anything goes to Jev, but not every possible secret can be recognised.

Codex runs through jevcomp codex. Its local proxy answers compaction requests with Jev's cuts when they meet the configured reduction threshold; otherwise the request goes to OpenAI as usual.

Claude Code lets a plugin replace the compaction itself (an early-access feature called function hooks), so there jevcomp hands back the conversation with Jev's cuts instead of a summary.

jevcomp does not judge content it cannot read (images, audio, encrypted agent messages): then the agent compacts normally. In Codex, the experimental token-budget reset also goes through jevcomp; turn jevcomp off for that if you want a completely clean context.

To preview what Jev would keep from a Codex rollout without changing anything: jevcomp compact rollout.jsonl --context retained.txt --json retained.json.

Data

Everything is in ~/.jevcomp (or JEVCOMP_DATA_DIR): history.jsonl for the dashboard. History from versions before 0.7.0, in ~/.codex/jevcomp and the old Codex plugin data folder, is still read.

Development

npm run check

No third-party runtime dependencies. dist/ is committed so the plugins run without a build. Tests use a fake Jev and make no paid requests.

Credits

Independent implementation informed by these MIT-licensed projects:

  • IAmUnbounded/save-token-jev-clean
  • leonaaardob/fast-dev-compaction
  • tamaratran/fast-jev-compaction
  • fatelei/jevcomp (post-compaction membership check)

AUDIT.md has the detailed comparison and compatibility notes.

Source 2 files
hooks/claude.js 41 lines
1import { applyJevCut, toJevMessages } from '../dist/claude.js';
2
3// The module runs without Node, so the Jev work runs in a node process started from this plugin's own dist/.
4function cliPath() {
5  const path = decodeURIComponent(new URL('../dist/cli.js', import.meta.url).pathname);
6  return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
7}
8
9async function askJev($, e, options) {
10  let sessionId;
11  try { sessionId = await $.session.id(); } catch {}
12  const input = JSON.stringify({ sessionId, agentId: e.agentId, trigger: e.trigger, provider: options.provider, apiKey: options.apiKey, messages: toJevMessages(e.messages) });
13  const run = await $.process.run(['node', cliPath(), 'claude-compact'], { stdin: input, timeoutMs: 300_000 });
14  if (run.exitCode !== 0) throw new Error(String(run.stderr).trim() || `jevcomp exited with ${run.exitCode}`);
15  return JSON.parse(String(run.stdout));
16}
17
18function note($, text) {
19  $.ui.log(`jevcomp: ${text}`);
20}
21
22/** @type {import('claude-code').Register} */
23export const register = (on, options) => {
24  on('session.compact', async ($, e, next) => {
25    // A precomputed summary would be the model-written one this plugin exists to avoid.
26    if (e.trigger === 'precompute') return { skip: 'jevcomp compacts when the compaction happens' };
27    try {
28      const cut = await askJev($, e, options);
29      if (!cut.apply) {
30        note($, `Claude summary used (${cut.reason})`);
31        return next(e);
32      }
33      note($, `kept what still matters from old tool output (${cut.summary})`);
34      return { messages: applyJevCut(e.messages, cut) };
35    } catch (error) {
36      note($, `Claude summary used (${error instanceof Error ? error.message : String(error)})`);
37      return next(e);
38    }
39  });
40};
41
dist/claude.js 32 lines
1export function toJevMessages(messages) {
2    return messages.map((message) => ({
3        role: message.role,
4        text: message.text,
5        toolCalls: message.toolUses.map((use) => ({ id: use.tool_use_id, name: use.tool, input: use.input })),
6        ...(message.toolResults?.length ? { toolResults: message.toolResults.map((result) => ({ callId: result.tool_use_id, output: result.text, isError: result.isError })) } : {}),
7    }));
8}
9/** Messages Jev left alone go back as the engine's own objects; edited ones are rebuilt without the engine's handle. */
10export function applyJevCut(messages, cut) {
11    const dropped = new Set(cut.dropped);
12    const out = [];
13    for (const message of messages) {
14        const toolUses = message.toolUses
15            .filter((use) => !dropped.has(use.tool_use_id))
16            .map((use) => (use.tool_use_id in cut.truncated ? { ...use, text: cut.truncated[use.tool_use_id] } : use));
17        const toolResults = (message.toolResults ?? [])
18            .filter((result) => !dropped.has(result.tool_use_id))
19            .map((result) => (result.tool_use_id in cut.truncated ? { tool_use_id: result.tool_use_id, text: cut.truncated[result.tool_use_id], isError: result.isError } : result));
20        const changed = toolUses.length !== message.toolUses.length || toolResults.length !== (message.toolResults?.length ?? 0) ||
21            toolUses.some((use, i) => use !== message.toolUses[i]) || toolResults.some((result, i) => result !== message.toolResults?.[i]);
22        if (!changed) {
23            out.push(message);
24            continue;
25        }
26        if (!message.text.trim() && !toolUses.length && !toolResults.length)
27            continue;
28        out.push({ role: message.role, text: message.text, toolUses, ...(toolResults.length ? { toolResults } : {}) });
29    }
30    return out;
31}
32