Pixel compression
Pixel compression renders text into PNG image pages instead of sending it as text tokens. It is lossy, allowlist-gated to models with measured render legibility, and it only ever ships when a profitability gate says the image is actually cheaper than the text it replaces.
S4lossy · recoverable This is a lossy transform — it changes the model-visible bytes by turning text into image blocks. It is ported from pxpipe (MIT) into public/engine/pixel/, and it is never wired into the engine's Detect router: nothing routes to it automatically. It only runs where a caller explicitly asks for it — caveman wrap --pixel and caveman convert.
Why an image can be cheaper than text
Providers price image input by pixel area, not by character count. Anthropic's rate is 750 pixels per token (AnthropicPixelsPerToken in public/engine/pixel/gate.go). If a block of text renders small enough, the image can cost fewer tokens than the text tokens it replaces — but for most short or already-dense text, it doesn't. A profitability gate decides per request, per skill, per render: it estimates image tokens (rendered pixel area, with a 10% safety margin) against text tokens (via the offline counter) and only proceeds when the image side is smaller.
Plain rendering usually loses
A plain, non-reflowed render is frequently larger in tokens than the text it replaces — this is documented directly in the caveman convert implementation (public/cli/src/index.ts:914-917): a 5 KB skill body renders to 1,491 estimated image tokens against 1,178 text tokens plain — worse than the original. The same body in dense mode renders to 313 tokens — about a 73% reduction on that fixture. Dense packing (tighter columns, a fuller page, line breaks preserved as a visible sentinel glyph ↵) is the mode that wins. Every number here is inferred and specific to the fixtures it was measured on — actual results depend on the text.
The gate, not a flag
Nothing pixel-related is unconditional:
- Model allowlist —
pixel.Allowed(model)(public/engine/pixel/applicability.go) checks the request's model againstCAVE_PIXEL_MODELS, which defaults toclaude-fable-5,gpt-5.6— models with measured render legibility. Any other model is untouched. - Profitability gate —
EvalCompressionProfitability/IsCompressionProfitable(gate.go) compare estimated image tokens (plus any cache-warm burn cost) against estimated text tokens. The transform only proceeds when the image side is strictly smaller. - Fail-closed on parse or render errors — any error, or a render that drops characters it can't represent, aborts the transform and the original bytes pass through unchanged.
Where it runs
caveman wrap --pixel — live request/response streams
Passing --pixel to caveman wrap starts the local proxy in pixel mode instead of record or --compress. The standalone proxy (public/proxy/internal/gateway/pixel.go) applies the gate to each provider request — rendering only the last live user turn's oversized blocks, a lighter subset of the full pxpipe-parity transform (it does not do system-prompt relocation, tool-doc stubbing, system-reminder compression, or history collapse; those live in public/engine/pixel/transform_*.go and today run only under the caveman-engine pixel simulate diagnostic). It stores the original via the recovery store before publishing the transformed bytes, and reports the resulting token estimate as inferred. --pixel-models overrides the allowlist per invocation. --pixel cannot be combined with --compress or --toon — pick one wrap mode.
caveman wrap --pixel claude
caveman wrap --pixel --pixel-models claude-fable-5,gpt-5.6 claudeRecovery for this path is through caveman mem recover or the Caveman MCP tool — the standalone proxy always stores the pre-transform original before it forwards a pixel-compressed request.
caveman convert — agent skill files
caveman convert pixel-compresses installed agent skills (SKILL.md) in place. Frontmatter (YAML between --- fences) stays text, so agent discovery and triggering still work; the body gets rendered to PNG pages and replaced with a short stub telling the agent to read those images.
caveman convert [--agent <id>] [--project] [--skill <name>] [--dir <path>] [--revert] [--dry-run] [--force]Preview with --dry-run
Renders every discovered skill and reports what would change, without touching any files.
caveman convert --dry-runConvert
Runs the same render, then for each skill that clears the gate: writes the untouched original bytes to SKILL.orig.md, moves the rendered pages into the skill directory as SKILL.px1.png, SKILL.px2.png, …, and rewrites SKILL.md as frontmatter + a marker comment + the stub telling the agent to view the images.
caveman convertRevert if you want the plain text back
Restores SKILL.orig.md over SKILL.md and deletes the SKILL.px*.png pages and SKILL.orig.md itself.
caveman convert --revertDiscovery walks every agent profile in the registry that declares a skills block — today that's Claude Code (~/.claude/skills, plus .claude/skills under --project) and Codex (~/.codex/skills). --agent <id> filters to one profile, --skill <name> filters to one skill directory, --dir <path> converts an arbitrary directory of skill subdirectories directly.
What makes a skill get skipped
Every skip prints its reason instead of silently doing nothing:
| Skip reason | When |
|---|---|
no frontmatter — discovery needs it as text | SKILL.md has no --- frontmatter block |
already converted | body already starts with the pixel marker (use --force to re-convert from SKILL.orig.md) |
stale SKILL.orig.md exists — use --force to overwrite | a previous conversion's original is present but the stub isn't |
caveman-engine not found — kept as text; run \caveman setup`` | the engine binary can't be resolved |
render dropped chars — kept as text | the renderer couldn't represent every character (fail-closed on lossiness) |
not smaller — kept as text | the profitability gate says the image + stub cost is not less than the text cost |
Recovery is byte-exact, not through CCR
caveman convert does not use the engine's CCR store. Recovery is a direct copy: the untouched original bytes are written to SKILL.orig.md before anything else changes, so --revert (or a --force re-run) restores the exact original even if a previous run failed partway through a page swap.
Reading the report
caveman convert prints, per skill, <name>: <text tokens> → <after tokens> est tokens (−NN% inferred), plus a total line when at least one skill converted. Everything is labeled inferred — it's the offline engine token counter's estimate on that one skill body, not a measured provider bill.
Illustrative (composited from the CLI's test fixtures — two separate skills, one converts and one is skipped as not smaller):
$ caveman convert
fat: 5000 → 520 est tokens (−90% inferred)
plain: skipped — not smaller
total: 5000 → 520 est tokens (−90% inferred)