Skip to content

session: all history images are re-sent on every request, permanently breaking sessions on image-capped backends #59794

Description

@jamesdolezal

Summary

OpenCode re-sends every image that was ever pasted or returned by Read in a session on every model request, with no pruning. On backends that enforce a per-prompt image limit (e.g. vLLM with --limit-mm-per-prompt '{"image":4}'), the session becomes permanently unusable once the history accumulates more images than the cap: every subsequent request fails instantly with a 400, and since the offending images are in history, re-prompting can never recover the session.

Environment

  • opencode version: 2.2.0
  • OS: Linux 7.0.0-38-generic (Ubuntu, x86_64)
  • Terminal: TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/bash
  • Install/channel: latest (opencode v2.2.0 via ~/.opencode/bin)
  • Active plugins: none at time of failure (a local image-budget plugin was added afterwards as a workaround)

Reproduction

  1. Point OpenCode at an OpenAI-compatible server that caps images per request, e.g. vLLM started with --limit-mm-per-prompt '{"image":4}'.
  2. In one session, paste 2 screenshots and let the agent Read 3 PNG files across later turns (5 images total in history).
  3. Ask anything, or ask the agent to check one more rendered image.

Expected Behavior

The request still succeeds: stale images (tool-result screenshots the model already described in text, superseded screenshots) are pruned from the outgoing request — e.g. replaced by a short text placeholder like Claude Code does — keeping only the newest N images, where N matches the provider's supported per-prompt image count (configurable per model/provider).

Actual Behavior

Every request carries all 5 history images and is rejected at ~46 ms:

Error: At most 4 image(s) may be provided in one prompt. (parameter=image)

The failure is sticky forever: the images live in persisted history and are re-sent on every turn, including new user messages, so the session can never continue. The only current recovery is manual (undo across image turns, or editing the session database).

Additional Context

  • Reproduced consistently; any session that iterates on rendered screenshots (a very common agent loop: render → Read PNG → adjust) inevitably crosses the cap.
  • Anthropic-compatible clients such as Claude Code avoid this by stripping old image payloads from the request history after the model has consumed them, leaving a text placeholder; persisted history is unaffected. OpenCode sending history verbatim is the difference, not the server cap.
  • The V2 plugin system can half-fix it (ctx.session.hook("context") mutating event.messages), which I'm using now, but a built-in policy is better: it needs no per-user plugin, and only OpenCode knows which images are stale vs. freshly attached in the current turn.
  • Suggested design: a per-model/provider setting (e.g. limit.imagesPerPrompt or reuse attachment/modalities metadata) plus default pruning of image parts older than the current turn (or last-N), with the trimmed placeholder text explaining the image was pruned.

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed write-up and for sharing your workaround!

    In short, you're asking for OpenCode to drop old images from the request it sends to the model (keeping the newest N, or only the current turn's, with a text placeholder for the rest), and for a per-model/provider setting for how many images one prompt may include. That way, backends that limit images per prompt, like vLLM with --limit-mm-per-prompt, don't end up rejecting every request in a session.

    Right now OpenCode sends the full history to the model as stored, and it has no setting for a per-prompt image limit. So I've filed this as a feature request rather than a bug. Until there's a built-in option, a plugin using the context session hook, like the one you're running, is the way to handle it. Maintainers will consider the request.

  2. Sugamsss commented on Oct 28, 2026

    @Sugamsss

    One thing worth knowing before this lands: #59303 caps at a fixed 30 images. Your cap is 4, so a session with more than 30 screenshots would still fail for you — 30 images go out, the backend rejects the request.

    Until core has a per-model number, the workaround is to set it yourself. I maintain opencode-prune-images, which reads OPENCODE_MAX_IMAGES, so OPENCODE_MAX_IMAGES=4 keeps the newest 4 as real images and turns the rest into text cards that carry a path to read them again.

    Your setup is the useful data point here: 30 is fine for the hosted gateway routes, but a self-hosted vLLM can be set to any number.

  3. chipfighter commented on Nov 3, 2026

    @chipfighter

    One option alongside the fixed last-N policy is to retain references to earlier images and let the agent retrieve an original when needed. That keeps a long visual task usable without making every previous screenshot part of every request.

    Disclosure: I maintain FrameKeeper, which implements this locally: earlier-turn images become short numbered references, and the model can fetch an original through an MCP tool. There is a new OpenCode adapter using the provider's base URL setting; no panel is required.

    I have not validated that adapter against your OpenCode 2.2.0 / vLLM setup, so this is a candidate to experiment with, not a confirmed fix. It also does not guarantee your four-image cap: images needed within the current turn can still exceed it. A provider-aware budget remains useful even with retrieval. Your existing context-hook workaround addresses that stricter requirement directly.

    Sharing in case on-demand access to older screenshots is useful to this workflow. Written with Codex under the project owner's direction.

  4. Sugamsss commented on Nov 4, 2026

    @Sugamsss

    Yes, that’s the approach opencode-prune-images already takes: older screenshots become context cards with observations and a cached file path, so the agent can read the original again when needed. Recall is best-effort because the cache is bounded, but the goal is exactly this—keep useful context without resending every image on every turn.

  5. holny commented on Nov 4, 2026

    @holny
    Contributor

    I'd like to take this. The shape I'd go with: prune image parts from older turns at request-assembly time, keep the newest N images (configurable, with a default that works under common caps like vLLM's 4 and the 30-image ceiling from #59303), and replace each pruned image with a short text placeholder noting the original file path so Read can pull it back when the agent needs it. That follows the direction discussed here without conflicting with #59303's fixed cap. I'll base it on origin/dev and cover both the pruning and the placeholder path with tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions