English | 简体中文
Request-level image lifecycle middleware for AI coding agents: keep current-turn images hot, downgrade recent history to optional thumbnails, move old pixels to restorable placeholders — before they hit your token bill, your prompt cache, or a 413.
Zero runtime dependencies. Framework-agnostic core. Works with Anthropic Messages, OpenAI Chat Completions, and OpenAI Responses payload shapes.
Your agent is resending pixels the model already saw.
In one real UI-development session, screenshots consumed 86.3% of the context window — 6,988 KB of base64 against 235 KB of actual assistant responses (claude-code#24298). Codex users have observed 8.34 MB request bodies and ~5.7M prompt tokens, where a single historical tool message carried a 7.68M-char PNG data URL (codex#28316). Pasted screenshots have broken /compact itself, leaving sessions unrecoverable short of deleting the session file (claude-code#16649), and oversized image payloads can 413 and permanently brick a session (claude-code#9269).
Images are ephemeral input. After the model has seen and acted on a screenshot, the pixels are dead weight — but most agents keep them in every subsequent request, burning tokens, destabilizing prompt caches, and eventually hitting request-size limits.
Compaction can't save you here, for a structural reason: a 413 happens the moment the request leaves your process — before any compaction runs — and images can break compaction itself (claude-code#16649). The only layer where this problem is fully fixable is request construction.
image-context-cascade fixes it there.
On every provider request:
- Hot: current-turn images stay intact. The model sees what you just attached.
-
Warm: recent historical images can become thumbnails. Hosts can inject a deterministic
thumbnailer; core does not ship image-processing dependencies. - Cold: older images become stable, restorable placeholders. With an opt-in source store, the placeholder hash is also a restore key. Without a store, the default placeholder remains byte-compatible with v0.1.
- Everything is measured. Telemetry reports counts, tiers, dedupe, store errors, and estimated savings — and never contains image data.
Measured on a real 1.3 MB PNG payload: 1,296,014 chars → 315 chars (−99.98%). In a live session, downgrading four historical images saved ~4.17M chars per request; input tokens fell from 91,734 to 1,910 while cache reads recovered from 11,776 to 100,352.
- Not a prompt technique. A prompt cannot delete bytes from the request payload; this is middleware.
- Not generic context compression. It only manages images. Your text history is untouched.
- Not automatic for every agent. For fully automatic per-request handling, the host must provide request-construction hooks (Pi does; your own custom agent does too). For hosts that lack these—like Claude Code, Codex, etc.—the fallback is to use session-boundary hooks or the CLI — the per-host table .
Whatever coding agent you use, paste this and let it set itself up. It reads the setup guides, identifies its own host, and applies the matching guide:
Read and follow https://raw.githubusercontent.com/dlgod7/image-context-cascade/main/docs/setup/README.md
— identify which agent host YOU are running in, then apply the guide for YOUR host
(Pi / Claude Code / Codex / generic). This is configuration only: do NOT read, list,
or rewrite any session/transcript files. When done, report what you changed and how to undo it.
Per-host guides, if you'd rather read them yourself: Pi · Claude Code · Codex · everything else. If your agent can't fetch URLs, open the guide in a browser and paste it in.
Setup never reads a session file. This second paste does — it lists your session files and, only after showing you numbers, rewrites the ones you approve (with backups):
Show me what image-context-cascade would save on my existing agent sessions.
Heads-up: this task lists my session files and rewrites the ones I approve (with backups).
1. Ensure the CLI is available (image-cascade --version, or use npx @image-cascade/cli).
2. Find MY host's session directory (Claude Code: ~/.claude/projects/*/*.jsonl; Codex:
~/.codex/sessions/*/*/*/rollout-*.jsonl; other hosts: locate your transcript directory —
on Windows these live under %USERPROFILE%). List the 5 largest files by size.
3. Safety: never touch the session file of THIS conversation, and skip any session that
might be open in another window (ask me if unsure).
4. Dry-run each candidate: image-cascade rescue <file> (writes nothing; note the numbers)
5. Show me the dry-run table and ask which files to apply. For approved files only:
image-cascade rescue <file> --yes --store
(.icc-backup is created next to each file; --store makes every removed image restorable
via `image-cascade restore <hash>`.)
6. Report: file, bytes before → after, images archived, backup path. Do not delete backups.
| Host | Mechanism | What you get |
|---|---|---|
| Pi | bundled adapter → before_provider_request
|
Fully automatic, every request, in-process, with restorable archive — the reference integration |
| Your own agent / framework |
cascadeImages() at request construction |
Fully automatic, every request |
| Claude Code |
SessionEnd hook → image-cascade hook claude-code
|
Automatic at every session end; resumes load the lean transcript |
| Codex |
AGENTS.md guidance + manual rescue
|
Semi-automatic — the agent proposes, you approve |
| Anything else | npx @image-cascade/cli rescue |
Manual, works on any JSON/JSONL transcript |
Design guarantees that hold in every mode:
-
Archive, not delete. Hook-triggered runs always use the source store plus a
.icc-backup; every archived image is restorable by hash. Nothing is ever unrecoverable. - No content judgement. Classification is positional and deterministic — the current turn is always kept intact. No model decides which of your images "look important".
- Nothing resident. No daemon, no watcher; hooks run for milliseconds at session boundaries and are a no-op when there is nothing to archive (idempotent).
-
Concurrent-write guard.
rescuere-checks the file's size/mtime before swapping in the rewrite and aborts if another process touched it mid-flight. -
Kill switch.
ICC_DISABLE=1disables hook-triggered processing (manual commands still work);ICC_STORE_DIRrelocates the default store. Uninstall = delete the hook entry or marked block.
The CLI covers hosts without a request hook, one-off rescues of oversized sessions, and restores:
npm install -g @image-cascade/cli # binary: image-cascade (npx @image-cascade/cli also works)
image-cascade rescue session.jsonl # dry-run: shows what would be saved
image-cascade rescue session.jsonl --yes # backs up the original, then rewrites
image-cascade rescue session.jsonl --yes --store # also archives originals for restore
image-cascade restore a1b2c3d4e5f6 --out img.png # bring any archived image back
image-cascade hook claude-code # SessionEnd hook entry point (stdin payload)Two streaming passes, O(1) memory, automatic backup, atomic write, malformed lines passed through untouched, idempotent. --store writes a local content-addressed store under ~/.image-cascade/store (override with ICC_STORE_DIR). The placeholder hash identifies the stored object — it is the hash of the original base64 text, not the byte hash of the restored file.
Where session files live:
-
Claude Code:
~/.claude/projects/<project>/*.jsonl— measured on a real 381-line session: 6.26 MB → 1.36 MB (−78%), 35 historical attachments downgraded, every line still valid JSON. -
Codex:
~/.codex/sessions/<year>/<month>/<day>/rollout-*.jsonl— measured on a real 332-line rollout: 50.2 MB → 2.26 MB (−95.5%). Codex stores each generated image twice as bare base64 (image_generation_callresponse item +image_generation_endevent), so image-heavy Codex sessions collapse dramatically.
Do not rewrite a session that is currently open. The agent process may be appending to the file. Close the session first, or work on a copy. Rescuing other sessions while an agent is running is fine.
With rescue --yes --store, a cold placeholder contains the short hash needed to restore the original bytes. Claude Code does not need an MCP server for the loop: if the agent has a shell tool and a file-reading/image-reading tool, it can restore the image as a normal file and inspect it as current-turn content.
Example flow:
User: Please look again at Image a1b2c3d4e5f6.
Assistant shell: image-cascade restore a1b2c3d4e5f6 --out restored-a1b2c3d4e5f6.png
Assistant: reads restored-a1b2c3d4e5f6.png with the host file/image tool, then answers from the restored current-turn content.
Restore appends new current content. It does not rewrite old transcript bytes, so it does not destroy prompt-cache prefixes.
If your agent has a request-construction hook, run the cascade in-process — that's the fully-automatic tier in the table above:
npm install image-context-cascadeimport { cascadeImages } from "image-context-cascade";
// Wherever your agent builds the provider request:
const { payload, mutated, telemetry } = cascadeImages(requestPayload);
// payload: historical images replaced with stable placeholders,
// current-turn images untouched
// telemetry: { found, current, downgraded, estimatedSavedChars, ... }
// — counts and hashes only, never image dataThat's the default positional strategy: images at or after the last user message are current; everything earlier is downgraded. It is stateless — safe across restarts, and correct for proxies that see each request fresh.
┌────────────────────────────────────────────┐
agent loop │ provider request payload │
───────────► build │ [ msg1(img A) msg2 msg3(img B) msg4(img C)│──► cascade ──► wire
└────────────────────────────────────────────┘ │
▼
img A, B → [Image a1b2c3d4e5f6 omitted …]
img C (current turn) → sent intact
-
Find — walk the payload; match image blocks per provider format (Anthropic base64 blocks, OpenAI Chat
image_url, OpenAI Responsesinput_image, data URIs, and bare-base64image_generation_callresults), plus Anthropic base64documentattachments (e.g. PDFs), which cause the same historical-resend problem. Blocks are matched wherever they sit — content arrays, item-level entries in a message list, or object fields in a transcript line. -
Classify — each image is
current,historical, orunknown, via a pluggable strategy. - Tier — current images remain hot; historical images can be warm thumbnails or cold placeholders, according to an injected tier policy and thumbnailer.
-
Store and restore — when a source store is enabled, original bytes are stored locally by content hash, and cold placeholders can be restored later with
image-cascade restore <hash>. - Report — telemetry with counts, per-format stats, tiers, dedupe, store errors, and estimated savings. No base64, ever.
Classifies by position: images at or after the last user message in a message array are current; earlier ones are historical.
Why this is safe: any image sitting in an earlier message was necessarily sent in full in an earlier request — the model has already seen it. No cross-request state is needed, so restarts lose nothing and stateless proxies work. When no user-message boundary can be found, it fails open and retains everything.
For hosts that know exactly which images belong to the current turn (e.g. an agent framework with a turn-start hook):
import { cascadeImages, trackerStrategy, InMemoryTracker } from "image-context-cascade";
const tracker = new InMemoryTracker(); // LRU, 200 entries
// at turn start: collect hashes of newly attached images into currentTurnHashes
const result = cascadeImages(payload, {
strategy: trackerStrategy({ currentTurnHashes, tracker }),
});Tracker mode adds one safety refinement: an image that is neither current nor previously tracked is unknown, and passes through intact once before becoming eligible for downgrade. Use it when transcripts may be edited out-of-band or when you need per-image lifecycle control.
An adapter is the glue between your agent's hooks and the core — the Pi reference adapter is 99 lines including the full store/restore wiring:
- On request construction, call
cascadeImages(payload, options)and forward the (possibly mutated) payload. - Optionally, on turn start, record current-turn image hashes and use
trackerStrategy. - Send
telemetrywherever your host logs — it is safe by construction (no image data fields exist in its type).
New provider format? Implement a BlockMatcher (match(block) / replace(block, text)) and pass it via formats. Do not reimplement classification, placeholders, or traversal — that's how behavior drift happens. Run the conformance suite (@image-cascade/conformance) to verify your adapter preserves current-turn images, downgrades historical ones, keeps placeholders byte-stable, and leaks nothing into telemetry.
- No image data leaves the process boundary it was already in. The library only ever removes image bytes from payloads; it adds nothing but short text placeholders.
-
Telemetry cannot contain image data. The
CascadeTelemetrytype has no field that could hold base64; a conformance test additionally asserts the serialized telemetry never matches image-data patterns. - Only a 12-char one-way hash prefix enters the context. No filenames, no paths, no pixel data.
-
Current-turn images are never touched. Enforced by tests (
current_turn_images_never_touched), and the positional strategy fails open when it cannot establish a boundary.
- Remote URL references are not stored. The source store persists base64/data-URI bytes that are present in the payload; it does not fetch URLs.
- Exact-byte identity only: the same image re-encoded or resized hashes differently. Perceptual hashing is future research.
- Warm thumbnails require a host-injected deterministic
thumbnailer. Core and CLI do not depend on Sharp or any other image-processing library. - Closed agents without request-construction hooks cannot run the middleware in-process. Claude Code gets the next-best thing — automatic archiving at session boundaries via its
SessionEndhook; Codex is manual/agent-suggested today (its new hooks system has no session-end event yet, and transcript rewrite mid-session is riskier than at boundaries). - Rescue guarantees the rewritten file is lean, valid JSONL, and fully restorable — it does not guarantee the host will resume an old session. Field test: a 17.4 MB Codex rollout that 413'd on resume (dead) became sendable after rescue (2.5 MB), but Codex then refused it on a validation unrelated to our rewrite (zero image fields left in the file; the trigger sits in encrypted reasoning). Old-session resume is best-effort; the
.icc-backupalways restores the original bytes.
- v0.1 — core with positional + tracker strategies, built-in matchers for three provider image formats plus Anthropic document attachments, session rescue CLI, Pi reference adapter, conformance harness with a language-neutral corpus, verified benchmarks.
-
v0.2 — opt-in source store, hot/warm/cold tiered downgrade model, injected thumbnailer interface, restorable placeholders,
image-cascade restore, same-payload byte dedupe, and the Claude Code zero-component restore loop through shell + file tools. -
v0.2.1 — Codex rollout sessions verified end-to-end; new matcher for bare-base64
image_generation_call/image_generation_endresults; traversal now catches item-level and object-field blocks, not just content-array members. -
v0.2.2 (this release) — hands-free Claude Code integration (
image-cascade hook claude-code+ SessionEnd hook),ICC_DISABLEkill switch,ICC_STORE_DIR, concurrent-write guard, decoded-byte magic sniffing (adds BMP/TIFF/AVIF/HEIC, fixes RIFF false positives), format-derived restore filenames. -
v0.2.3 — fix: CLI entrypoint was a silent no-op after
npm install -gon macOS/Linux (symlink broke theimport.meta.urlself-check); also fix Windows path mismatch by usingpathToFileURLfor cross-platform correctness. Thanks to @dafang for the report and initial fix (#1, #2). -
v0.3 planned — budget-driven downgrade, more lifecycle-hook hosts (Cursor ships
sessionEnd; Codex hooks are new and still lack one), an optional MCP server for hosts without a shell tool, and a local proxy mode for request-time downgrade under any agent. - Future research — perceptual hashing for near-duplicate images; persistent cross-session trackers with safe privacy defaults.
The Claude Code and Codex communities independently articulated this problem and sketched similar solutions — image-aware compaction, ephemeral image flags, /drop-images, and sha256 placeholders (claude-code#24298, codex#28316). This project exists to turn those sketches into a correct, installable, framework-agnostic implementation. If coding agents ship native image lifecycle management, this project has done its job.
Related projects solving different problems:
-
pi-vision-proxy and opencode-vision-paste route images through a vision model so text-only models can use them. They make images readable;
image-context-cascademanages the lifecycle of images a multimodal model has already read. The two are complementary. - context-cascade is a Claude Code plugin architecture for layered context loading — no relation beyond the name.
- Thanks to the linux.do community for the feedback, discussions, and inspiration during the development process.
- Thanks to Fable-5 (under Claude Code) for their contributions to the project.
Apache-2.0. Contributions welcome — see the adapter guide and conformance suite for the fastest path to supporting a new agent or provider format.