fix(session): port upstream bounded history images (anomalyco/opencode#62270)

DE
2d ago
+247-2

Issue for this PR

Closes #1712

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

User-visible failure: every image a user pastes (or a tool like read returns) stays in history and is re-sent inline on every request. Images cost almost no tokens, so compaction never drops them. After enough screenshots the request body exceeds the provider's limit and every following turn fails with Request body too large / multimodal data size is too large. Those messages weren't classified as overflow, so auto-compaction never ran and the session stayed broken.

Port of upstream anomalyco/opencode#62270 (fixes upstream #39929 / #59794), adapted to the fork's v1 MessageV2.toModelMessagesEffect:

  • MessageV2.boundImages runs after convertToModelMessages. Once inline image bytes (user file/image parts and images inside tool-result content) pass 25 MiB, the oldest are replaced with a text placeholder telling the model the image is gone, until the rest fit 15 MiB. The gap between the two values means the cut only moves every ~10 MiB of new images, so the replaced prefix and the provider prompt cache stay the same between cuts. Messages that don't change keep their object identity.
  • Remote http(s) URLs count as 0 bytes and are never replaced. Upstream's first version could still use up its cut on remote images (upstream issue anomalyco/opencode#61391), so this port only replaces images that have an inline payload.
  • request body too large and multimodal data size is too large are added to the overflow patterns in both provider/error.ts and packages/llm/src/provider-error.ts, so these errors trigger compaction instead of a hard failure.

All changes are wrapped in altimate_change start — upstream_fix: markers. Stored history is untouched; only the outgoing request is trimmed.

How did you verify your code works?

  • New session.message-v2.boundImages tests: below-trigger no-op, oldest-first replacement down to target, stable prefix as images arrive, remote URLs ignored and kept inside the cut, nested tool-result media, and end-to-end through toModelMessages (tool-result images and synthetic user images for OpenAI-compatible providers). New overflow cases for the two messages. Without the source change, 8 of the new tests fail.
  • bun test test/session/message-v2.test.ts test/provider in packages/opencode: 936 pass, 0 fail. packages/llm tests: 0 fail.
  • bun typecheck (turbo, all 13 packages) passes.
  • bun run script/upstream/analyze.ts --markers --base origin/main --strict passes. Caveat: with upstream/dev now on the v2 layout, the audit sees no shared files and passes without checking anything. I also ran it with the upstream remote temporarily pointed at a local mirror whose dev is v1.17.9: 3 upstream-shared files checked, all marked.

Cost side effects

None. No new model calls, queries or resources. This reduces request payload size.

Screenshots / recordings

N/A, no UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

Tracked in #1478.

Written by Devin


Summary by cubic

Ports the upstream fix that bounds images re-sent from session history, so sessions no longer break when pasted screenshots accumulate and push the request body over the provider's limit. Closes #1712.

  • Once inline images pass 25 MiB, the oldest are replaced with a text placeholder until the rest fits 15 MiB; the gap keeps the replaced prefix stable so the provider prompt cache survives. Remote image URLs are never replaced.
  • request body too large and multimodal data size is too large now count as context overflow, so these failures trigger auto-compaction instead of a hard error.

Stored history is unchanged; only the outgoing request is trimmed.