Jev-guided, verbatim context compaction for Claude Code.

Portable, Jev-guided context compaction for coding agents.
Instead of asking another LLM to rewrite old context into a lossy summary, save-token-jev asks Jev which tool calls and results still matter. User and assistant text is kept verbatim. A tool call can be kept with its full result, kept with a bounded result, or removed together with its result.
The algorithm is implemented behind a normalized transcript model and host adapters so the same compaction policy works across multiple coding-agent runtimes.
| Host / format | Integration | Behavior |
|---|---|---|
| Codex CLI/app | Codex plugin hooks | Scores before built-in compaction and restores retained verbatim context immediately afterward |
| OpenCode V2 | npm plugin | Replaces the compaction summary through session.hook("compaction") |
| Claude Code | function-hook plugin | Directly replaces compaction with the retained message list |
| Anthropic API | transcript adapter | Decodes tool_use / tool_result blocks for applications |
| OpenAI Responses | transcript adapter | Decodes messages, function calls, custom calls, and outputs |
| OpenAI Chat Completions | transcript adapter | Decodes assistant tool_calls and tool messages |
| Any agent | generic adapter / CLI | Accepts the normalized { role, parts } format; custom adapters implement one small interface |
Codex currently does not expose a command-hook response that replaces its compacted transcript. Its integration therefore uses the documented PreCompact plus SessionStart(source="compact") lifecycle. OpenCode exposes a direct compaction-result hook, so its integration replaces the summary.
npm install
npm run check
export TYPESAFE_API_KEY="..."
Node 20 or newer is required. The runtime package has no third-party dependencies.
This repository is a Codex plugin (.codex-plugin/plugin.json and hooks/hooks.json). Build it before installing or linking it because the hook runs dist/cli.js.
For a project-local setup without a marketplace, copy the contents of hooks/hooks.json into .codex/hooks.json, replace ${PLUGIN_ROOT} with this repository's absolute path, then open /hooks in Codex and trust the hook definition. Set TYPESAFE_API_KEY in the environment that launches Codex.
To enable it for every Codex project and session on your machine, copy the same hook definition to the user-level Codex configuration instead:
cp hooks/hooks.json ~/.codex/hooks.json
perl -0pi -e 's/\$\{PLUGIN_ROOT\}/\/absolute\/path\/to\/save-token-jev/g' ~/.codex/hooks.json
Replace /absolute/path/to/save-token-jev with the actual checkout path. Restart Codex after changing hooks and approve the hook if Codex asks you to trust it. User-level hooks are loaded independently of the current project, while project-local hooks require that project to be trusted.
The repository also includes a ready-to-use local configuration at .codex/hooks.json for this checkout. It points at the built dist/cli.js, so run npm run build after cloning or changing source files.
The flow is fail-open:
PreCompact reads the Codex rollout JSONL and asks Jev about completed tool calls.SessionStart for source: compact injects the retained context before the next model request.Codex documents transcript_path as convenient but not stable. All parsing is isolated in src/adapters/codex.ts and covered by fixtures so a future rollout change only needs an adapter update.
Install the package in the OpenCode config directory, then list it as a plugin:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["save-token-jev"]
}
The package's default export is a V2 plugin object with id and setup(). It registers the compaction hook during setup. On a Jev error or insufficient reduction it leaves event.result unset, which lets OpenCode run its normal compaction.
Claude Code's function hooks can directly replace the message list, so this integration has the same no-summary behavior as the reference project. Build first, opt into function hooks, and load the dedicated plugin directory:
export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1
export TYPESAFE_API_KEY="..."
claude --plugin-dir ./plugins/claude-save-token-jev
The Claude manifest is intentionally nested because Codex and Claude use incompatible hooks/hooks.json schemas.
npm run build emits a sandbox-contained runtime under plugins/claude-save-token-jev/dist/, alongside the repository-root dist/. Claude Code resolves the module named in hooks/hooks.json against the plugin directory and refuses any path that leaves it, so the hook loads from the plugin-local build rather than the root one.
Claude Code function hooks run in a restricted sandbox with no process and no Node builtins, so the Claude integration deliberately does not import the Node client or its Keychain resolver. It takes the key from the plugin runtime instead, in this order:
apiKey setting,TYPESAFE_API_KEY in the environment Claude was launched with,env.TYPESAFE_API_KEY in Claude's settings.The key therefore has to reach Claude itself; the hook cannot look it up on its own. If you already keep it in the Login Keychain for the CLI, read that item into the launch environment rather than writing the key into repository configuration or shell startup files:
export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1
export TYPESAFE_API_KEY="$(security find-generic-password -a "$USER" -s save-token-jev -w)"
claude --plugin-dir ./plugins/claude-save-token-jev
With no key from any of the three sources, the hook logs one line and defers to Claude's built-in compaction.
Compact a normalized, Anthropic, OpenAI, or OpenCode JSON transcript:
save-token-jev compact \
--format anthropic \
--input transcript.json \
--output compacted.json
Read a Codex rollout JSONL:
save-token-jev compact --format codex-jsonl --input rollout.jsonl
Inspect environment readiness:
save-token-jev doctor
The CLI prints the compacted normalized transcript, decisions, and statistics as JSON. Diagnostics go to stderr, so stdout remains pipeable.
You can start the local dashboard manually:
node dist/cli.js dashboard
It prints a localhost URL such as http://127.0.0.1:43127/. Open that link to see total compactions, characters saved, estimated tokens saved, per-tool savings, and the individual compaction runs. The dashboard binds to loopback only and reads the Codex history stored under the same data directory as the hooks. Use --port 43127 for a stable port or --data-dir DIR to inspect another hook data directory.
The Codex PreCompact and SessionStart hooks also start the dashboard automatically on port 43127 and include the link in their visible status/context. The link is http://127.0.0.1:43127/. Set SAVE_TOKEN_JEV_DASHBOARD_PORT to choose another port. Set SAVE_TOKEN_JEV_DASHBOARD=off only if you intentionally do not want the dashboard process started; hook messages still show the configured link.
import { compactMessages, type TranscriptMessage } from 'save-token-jev';
const messages: TranscriptMessage[] = [
{ role: 'user', parts: [{ type: 'text', text: 'Fix the test. Never edit generated files.' }] },
{
role: 'assistant',
parts: [{ type: 'tool_call', id: 'call-1', name: 'read', input: { path: 'src/a.ts' } }],
},
{
role: 'tool',
parts: [{ type: 'tool_result', callId: 'call-1', output: 'file contents' }],
},
];
const result = await compactMessages(messages, {
preserveRecentMessages: 6,
keepThreshold: 0.5,
});
Bring your own Jev-compatible transport for tests, gateways, or non-HTTP runtimes:
import { compact, type JevAsker } from 'save-token-jev';
const asker: JevAsker = {
async ask(state, questions) {
return myTransport(state, questions);
},
};
const result = await compact(messages, asker);
To support another host, implement TranscriptAdapter<T> with canDecode and decode, then pass it to decodeTranscript. Unknown blocks should become { type: "opaque", value }; opaque content is never selected for deletion.
| Environment variable | Default | Meaning |
|---|---|---|
TYPESAFE_API_KEY | required unless stored in the macOS Keychain (CLI and library only) | TypeSafe/Jev API key |
JEV_MODEL | jev-latest | Jev model |
JEV_BASE_URL | System One endpoint | Alternate compatible endpoint |
SAVE_TOKEN_JEV_KEEP_THRESHOLD | 0.5 | Minimum keep probability |
SAVE_TOKEN_JEV_PRESERVE_RECENT | 6 | Newest messages pinned from deletion |
SAVE_TOKEN_JEV_MIN_REDUCTION | 0.15 | Minimum reduction for host integration takeover |
SAVE_TOKEN_JEV_MAX_STATE_TOKENS | 25000 | Estimated state budget |
SAVE_TOKEN_JEV_MAX_REQUEST_TOKENS | 30000 | Estimated state + question budget |
SAVE_TOKEN_JEV_TRUNCATE_HEAD_CHARS | 300 | Result prefix retained when only the call matters |
SAVE_TOKEN_JEV_MAX_CONCURRENT_REQUESTS | 4 | Jev requests allowed in flight at once |
SAVE_TOKEN_JEV_TIMEOUT_MS | 30000 | Provider request timeout before fail-open fallback |
Token counts are conservative estimates, not tokenizer-exact values. Jev probabilities are decisions, not proofs; use a higher threshold for sessions with expensive or irreproducible tool output.
On macOS, the CLI and library also check the Login Keychain for a generic password whose service is save-token-jev and whose account is the current OS username. This keeps the API key out of repository configuration and shell startup files. The Claude Code hook runs in a sandbox without Node builtins and does not perform this lookup; see Credentials under the function-hook sandbox.
npm run typecheck
npm test
npm run build
The test suite uses a fake Jev transport and never sends network requests.
Not captured.