#19135: fix(agents): prune excess images from conversation history (#19099)
gateway
agents
size: M
## Problem
In long sessions, every image sent by the user or returned by a tool accumulates in the conversation history. Providers that enforce a per-request image cap (commonly **8 images**) then reject subsequent requests with:
```
HTTP 400 – Max images exceeded
```
Fixes #19099
## Solution
Rather than letting images pile up indefinitely, we now keep only the **8 most recent** image blocks across the full conversation history. Older image blocks are replaced with a lightweight text placeholder (`[session:history] image removed from history`) so the message structure stays valid for all providers.
The limit is controlled by the new `DEFAULT_MAX_HISTORY_IMAGES = 8` constant in `images.ts` and is surfaced through `TranscriptPolicy.maxHistoryImages`. It can be overridden per-provider if needed.
## Changes
| File | Change |
|------|--------|
| `src/agents/pi-embedded-helpers/images.ts` | Add `DEFAULT_MAX_HISTORY_IMAGES`, `countHistoryImages`, `dropOldestImageBlocks` helpers; extend `sanitizeSessionMessagesImages()` with `maxHistoryImages` option |
| `src/agents/transcript-policy.ts` | Add `maxHistoryImages` field to `TranscriptPolicy`; default to `DEFAULT_MAX_HISTORY_IMAGES` in `resolveTranscriptPolicy()` |
| `src/agents/pi-embedded-runner/google.ts` | Forward `policy.maxHistoryImages` into `sanitizeSessionHistory()` |
| `src/agents/pi-embedded-helpers.ts` | Re-export `DEFAULT_MAX_HISTORY_IMAGES` |
| `src/agents/pi-embedded-helpers.sanitize-session-messages-images.prunes-history-images.test.ts` | Unit tests for the new pruning logic |
## Testing
- New unit tests verify that images beyond the cap are replaced with placeholders and that the N most recent images are always preserved.
- TypeScript compiles without errors (`npx tsc --noEmit`).
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds image-count pruning to conversation history to prevent HTTP 400 "Max images exceeded" errors from providers that cap per-request images (commonly 8). When the total image count exceeds `maxHistoryImages` (default 8), the oldest image blocks are replaced with lightweight text placeholders (`[session:history] image removed from history`), preserving message structure while staying within provider limits.
- Adds `DEFAULT_MAX_HISTORY_IMAGES = 8` constant, `countHistoryImages`, and `dropOldestImageBlocks` helper functions in `images.ts`
- Extends `sanitizeSessionMessagesImages()` with a `maxHistoryImages` option that prunes oldest images using a shared mutable counter across all messages
- Adds `maxHistoryImages` field to `TranscriptPolicy` type, defaulting to 8 for all providers via `resolveTranscriptPolicy()`
- Forwards the option through both the `google.ts` runner and the main `attempt.ts` runner paths (via `sanitizeSessionHistory`)
- Includes 6 unit tests covering within-limit, over-limit, disabled, zero, text-only, and newest-preserved scenarios
- Separately, adds explicit `"utf-8"` encoding to SSE `res.write()` calls in the gateway HTTP handlers (defensive improvement)
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge with minimal risk; the pruning logic is well-structured, properly tested, and correctly integrated into all existing code paths.
- The implementation is clean, well-documented, and follows existing patterns. The mutable counter approach for tracking drops across messages is correct. Tests cover key edge cases. The universal 8-image cap for all providers is a conservative safe default. The gateway UTF-8 changes are trivially correct. No logic bugs or security issues found.
- No files require special attention. The core logic in `src/agents/pi-embedded-helpers/images.ts` has been thoroughly reviewed and the pruning approach is sound.
<sub>Last reviewed commit: 236f287</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#23639: fix(agents): stop re-resizing session history images on every turn ...
by yinghaosang · 2026-02-22
79.7%
#23662: fix: cache sanitized images to avoid redundant re-processing per turn
by davidemanuelDEV · 2026-02-22
77.8%
#10465: Context pruning: strip image blocks instead of skipping
by quentintou · 2026-02-06
77.3%
#9598: fix(agents): check base64 string length against 5MB API limit
by BlockBB · 2026-02-05
76.6%
#2958: fix(media): wire tools.media.image.maxBytes config to image processin…
by shamsulalam1114 · 2026-01-27
76.4%
#8172: fix(sessions_list): strip base64 image data to prevent context over...
by Flamrru · 2026-02-03
76.4%
#5817: fix: strip old images during compaction to prevent 413 session bloat
by jduartedj · 2026-02-01
75.5%
#13388: feat(session): Auto-prune consecutive NO_REPLY messages from context
by Masha-L · 2026-02-10
74.5%
#23706: perf: cache image resize results to avoid redundant processing (#23...
by echoVic · 2026-02-22
74.4%
#19094: Fix empty tool_call_id and function names in provider transcript pa...
by yxshee · 2026-02-17
73.9%