Repository navigation
Let the compactor see the images in a message, so summaries say what they show - #284
Merged
Merged
Conversation
A message's leaf summary now gets the images it names, after the task text, so its line says what they show; a message with images is summarized even when short. A text-only compactor model gets none, and a provider that rejects them is asked once more by name only. Merges stay text.
…m pages, tidy - A short image message whose request fails for any non-retryable reason is asked once more by name only; if that fails too, the leaf is retried later. - A zoom result's page doesn't send its images again. - One short flag, required Compression.images, task() reads the current images. - Export imageNames from core/images.ts. - Split the compactor tests; add tests for a failed image read, a refused short image summary, and the Anthropic cache marks.
There was a problem hiding this comment.
🟡 Changes recommended
Image attachments can lose or mismatch their reference names, and a fifth-attempt image error can bypass the names-only fallback, causing source content to be permanently kept or clipped instead of retried.
2 open issues
What changed in this PR
This PR adds image-aware leaf summaries to shared core memory, so Pi and Pi Durable can carry image details into higher-level text summaries.
Changes:
- Load stored images for leaf compaction, including short messages, while avoiding duplicate attachments from zoom results.
- Add image guidance, model-capability handling, and a names-only fallback for rejected image requests.
- Export
imageNamesand update tests and documentation.
| File | Description |
|---|---|
test/images.test.ts |
Tests image-aware memory leaves, read failures, and refusals. |
test/compactor.test.ts |
Tests image placement, fallback behavior, and cache boundaries. |
test/compactor-size.test.ts |
Updates test inputs with the required images field. |
src/core/prompts.ts |
Adds leaf-only image description and reference guidance. |
src/core/memory.ts |
Loads leaf images and compresses short image messages. |
src/core/index.ts |
Exports imageNames. |
src/core/images.ts |
Extracts shared image-marker parsing. |
src/core/compactor.ts |
Attaches images and handles model support and rejection fallback. |
README.md |
Describes image-aware summaries. |
docs/recipe-differences.md |
Documents image compaction behavior and evaluation results. |
docs/core.md |
Documents the new image-name helper export. |
Review effort: Balanced
…d source, keep the names-only call out of the 5 tries
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The compactor never sees images. It gets only memory's text, where an image is a bare
[image <name>]. Worse, a message of 512 bytes or less is never summarized at all, so an image-only message (or one with a short caption) staysuser: [image …]in the view forever. Merges above it then know only that an image was sent, plus whatever the agent's reply happened to say about it.How
All in core, so Pi and Pi Durable both get it:
src/core/memory.ts): a message's leaf loads the images its text names with core's existingloadImages(store, source). If it finds any, the leaf is summarized even when short, and the images go to the compressor with their names asCompression.images({ name, image }[]). They are loaded from the source as the compactor gets it, so a huge message's cut middle sends neither its names nor their images. A deleted or never-stored image is skipped, so the message behaves as today. A thrown image read (an R2 outage, say) fails the build, which is retried every 10 s like any other store error (Stop retrying a summary the model refuses forever, and cap what one message sends the summarizer #204), so that leaf and the summaries above it wait until the read works again. A zoom result's page names images its message's own leaf already saw, so it doesn't send them again.src/core/compactor.ts): the images go after the task text, each after its[image <name>]label, so the cached view prefix andprimeFirstare unchanged, and a skipped image can't shift the others onto the wrong names. A line about images may be longer than the names it replaces, so the "line must shrink its source" rule is skipped for it. A model without image input gets no images, and a short message stays word for word, as before. Any non-retryable error with images attached is asked once more with names only: if the images were the problem (too big, too many), that call succeeds and a short message stays word for word; if not (a bad key, no quota, a dropped connection), it fails again and the leaf is retried later, images and all. That call doesn't count as one of the 5 tries. So a rejected image can't be retried forever, and an outage can't lose one.src/core/prompts.ts): one leaf-only sentence when images are attached: "After , each image it names follows its [image ]. Say what matters in each, and keep each [image ] as written, so OptChat can zoom it."Higher levels stay text: the A/B shows the leaf's text carries the image facts up the tree. No setting, no per-message image cap (the provider-rejection fallback covers it), and no change to handoff summaries or the view doc.
Why this way
An A/B with the default compactor (Sonnet 5.5 at medium, 6 synthetic images: error screenshot, chart, diagram, receipt, UI bug, config diff; 18 chats per arm, Sonnet grader):
No invented image content in either arm. Cost is about 1¢ per image (~850 tokens at 800 px).
Notes
Compressoris now also called for short messages that name a kept image, withimagesset (imagesis now always present, empty for merges). It can returninput.sourceto keep such a message word for word, as before.Tests
test/images.test.ts, through a realMemory: a short message naming a kept image is summarized with that image, while one naming no kept image, or a zoom page, stays word for word; a failed image read leaves the leaf to be built later, with the image; a refused short image summary keeps its words.test/compactor.test.ts: images go after the view and the task, with the prompt sentence, and a line about them may outgrow its source; a text-only model gets none. Rejected images are asked about once more by name only (short and long), and an error that isn't theirs throws so the leaf retries. On Anthropic (pi-ai's real client), the cache marks stay on the view, with the task and images after them.All but the refused-summary test fail on main.