Skip to content

fix(#2866): persist LlmMessage.images / ImagePart in file-backed snap…#91

Merged
Skobeltsyn merged 1 commit into
mainfrom
fix/2866-snapshot-images
May 30, 2026
Merged

fix(#2866): persist LlmMessage.images / ImagePart in file-backed snap…#91
Skobeltsyn merged 1 commit into
mainfrom
fix/2866-snapshot-images

Conversation

@Skobeltsyn
Copy link
Copy Markdown
Contributor

…shot

Pre-#2866 SnapshotJson.encodeMessage serialised only role / content / toolCalls per message. Any vision attachments on the conversation (LlmMessage.images: List<ImagePart>? from #2470) were dropped on encode and decodeMessage always rehydrated images = null. File-backed snapshot/resume silently lost image context; SSE-style apps that checkpointed mid-conversation could not resume with the original images attached.

Fix in core/Snapshot.kt:

  • encodeMessage now writes an images array when non-null, with per-part {base64, mime}. Wire shape stays byte-identical for messages without images (the images key is omitted), so pre-#2866 snapshots and current image-less calls produce the same JSON.
  • decodeMessage rehydrates each ImagePart via a decodeWireMime helper that maps the four shipped MIMEs (PNG / JPEG / GIF / WEBP) back to the closed WireMime sealed type. Unknown MIMEs are skipped defensively (a hand-edited or future-extended snapshot doesn't crash resume — the unknown part drops, the rest of the message rehydrates).

Encoding choice: base64 directly inside the message rather than ContentRef.hash + BlobStore re-fetch on resume. Self-contained snapshots — no BlobStore dependency at restore time — match the audit story better. Cost is the snapshot file size (~5KB per image vs. ~80 bytes for a ref), accepted as the right trade for the 0.6.x line.

Regression coverage in new SnapshotImagesRoundTripTest:

  • single image round-trips byte-for-byte
  • mixed-MIME ordered list round-trips
  • no-image message back-compat (the images key is omitted)
  • legacy pre-#2866 snapshot decodes with null images
  • unknown-MIME entries skip on decode (defensive)

Full ./gradlew test + detekt green. detekt baseline regenerated to include the new test file's package-naming entry (same as the other agents_engine.* packages).

…shot

Pre-#2866 `SnapshotJson.encodeMessage` serialised only role / content /
toolCalls per message. Any vision attachments on the conversation
(`LlmMessage.images: List<ImagePart>?` from #2470) were dropped on
encode and `decodeMessage` always rehydrated images = null. File-backed
snapshot/resume silently lost image context; SSE-style apps that
checkpointed mid-conversation could not resume with the original images
attached.

Fix in `core/Snapshot.kt`:
- `encodeMessage` now writes an `images` array when non-null, with
  per-part `{base64, mime}`. Wire shape stays byte-identical for
  messages without images (the `images` key is omitted), so pre-#2866
  snapshots and current image-less calls produce the same JSON.
- `decodeMessage` rehydrates each `ImagePart` via a `decodeWireMime`
  helper that maps the four shipped MIMEs (PNG / JPEG / GIF / WEBP) back
  to the closed `WireMime` sealed type. Unknown MIMEs are skipped
  defensively (a hand-edited or future-extended snapshot doesn't crash
  resume — the unknown part drops, the rest of the message rehydrates).

Encoding choice: base64 directly inside the message rather than
`ContentRef.hash` + BlobStore re-fetch on resume. Self-contained
snapshots — no `BlobStore` dependency at restore time — match the
audit story better. Cost is the snapshot file size (~5KB per image vs.
~80 bytes for a ref), accepted as the right trade for the 0.6.x line.

Regression coverage in new `SnapshotImagesRoundTripTest`:
- single image round-trips byte-for-byte
- mixed-MIME ordered list round-trips
- no-image message back-compat (the `images` key is omitted)
- legacy pre-#2866 snapshot decodes with null images
- unknown-MIME entries skip on decode (defensive)

Full ./gradlew test + detekt green. detekt baseline regenerated to
include the new test file's package-naming entry (same as the other
agents_engine.* packages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Skobeltsyn Skobeltsyn merged commit f643928 into main May 30, 2026
3 checks passed
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.

1 participant