Skip to content

Let the compactor see the images in a message, so summaries say what they show - #284

Merged
jonaslsaa merged 4 commits into
mainfrom
feature/compactor-sees-images
Nov 3, 2026
Merged

jonaslsaa merged 4 commits into
mainfrom
feature/compactor-sees-images

Conversation

@jonaslsaa

@jonaslsaa jonaslsaa commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

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) stays user: [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:

  • Memory (src/core/memory.ts): a message's leaf loads the images its text names with core's existing loadImages(store, source). If it finds any, the leaf is summarized even when short, and the images go to the compressor with their names as Compression.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.
  • Compactor (src/core/compactor.ts): the images go after the task text, each after its [image <name>] label, so the cached view prefix and primeFirst are 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.
  • Prompt (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):

Image facts in leaf in pair in root
Before 0% 4% 1%
After 100% 99% 97%

No invented image content in either arm. Cost is about 1¢ per image (~850 tokens at 800 px).

Notes

  • Custom compressors: a host's own Compressor is now also called for short messages that name a kept image, with images set (images is now always present, empty for merges). It can return input.source to keep such a message word for word, as before.
  • Text-only compactor models: memory still loads a leaf's images before the compactor drops them, so an unreadable image delays that leaf even though the model would never see it. All of Pi's default compactors take images, so this is documented rather than fixed.

Tests

  • test/images.test.ts, through a real Memory: 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.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 imageNames and 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

Comment thread src/core/compactor.ts Resolved
Comment thread src/core/prompts.ts Resolved
…d source, keep the names-only call out of the 5 tries

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The image-aware memory compaction changes have no active review issues.

1 open issue
1 resolved since last review

Review effort: Balanced

@jonaslsaa
jonaslsaa merged commit 5a5aaf3 into main Nov 3, 2026
5 checks passed
@jonaslsaa
jonaslsaa deleted the feature/compactor-sees-images branch November 3, 2026 01:15
@jonaslsaa jonaslsaa mentioned this pull request Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants