Skip to content

[BUG] Agent accumulated 51 images via read tool, session bricked by provider 50-image count limit with no recovery #47487

Closed as not planned

Description

@royalpinto007

Description

Agent bricked its own session by accumulating images, user got stuck and cannot chat anymore.

Session ses_f9337a042ffebyl9CiHpjtKvlu titled Facetworks (816 messages, 3377 parts) ended in an unrecoverable loop:

Error from provider (Console Go): Upstream request failed: [invalid_request_error] request contains 51 images, exceeding the maximum of 50 allowed per request

Full error from DB:

  • statusCode 400, isRetryable False
  • responseBody: {"model":"muse-spark-1.3-contributor","error":{"param":null,"type":"invalid_request_error","message":"Error from provider (Console Go): Upstream request failed: [invalid_request_error] request contains 51 images, exceeding the maximum of 50 allowed per request"}}
  • metadata url: https://opencode.ai/zen/go/v1/responses
  • first failure 2026-09-05T13:13:44Z, then every retry fails identically including plain user messages.

What exactly happened, verified from local ~/.local/share/opencode/opencode.db:

  • 51 parts with image attachments in this session
  • 48x read tool reading screenshots: /tmp/profile-readme/covers/*.png, /tmp/undertow-shot*.png, /tmp/lichen-*.png, /tmp/abyss-shot*.png, /tmp/drift-shot*.png, /tmp/facet-wide.png, /tmp/facet-mobile.png, /tmp/gummy-*.png, /tmp/jelly-thumb.jpg, /tmp/src-check.png, plus projects/lichen/docs/screenshot.png and projects/drift/docs/screenshot.png
  • 3x huggingface_gr1_z_image_turbo_generate outputs with mime image/webp attachments
  • Agent was doing visual verification loops across undertow, lichen, abyss, drift, facetworks, gummy. User did not paste 51 images manually.

Why it is bricked:

  • History already contains 51 images, so every subsequent LLM call fails before the model runs.
  • User tried okay continue now?, use less images then, can something be done? - all failed with the same 51-image error.
  • No way to /compact, fork without images, prune old tool attachments, or delete messages from TUI to recover.

Expected:

  • OpenCode should enforce a client-side image budget before hitting the provider (warn, stop attaching old images, downscale to text placeholder, or auto-prune oldest image attachments).
  • Compaction and retry paths should strip or summarize images so a session never becomes permanently dead.
  • Provide recovery: remove message, fork from message without images, or compact that excludes image bytes.

Related but not duplicates (all size or invalid file, not count):

Plugins

No response - stock build agent with huggingface MCP image tool available

OpenCode version

1.18.27 at session time, binary now 1.18.29

Steps to reproduce

  1. Start session on opencode-go/muse-spark-1.3-contributor variant xhigh in a repo with browser screenshot workflow
  2. Let agent iteratively take screenshots to /tmp and read them for visual check (repeat 40+ times) plus a few image generations
  3. Once attachment count in history exceeds 50, next step fails with 400 invalid_request_error
  4. Send any follow-up message, it fails with the same error. Session cannot continue.

Screenshot and/or share link

No share link, private code. Error text above is verbatim from session message msg_071b40311001TNVEKL93snfrs2. Can provide DB part counts on request.

Operating System

Ubuntu 24.04.4 LTS, kernel 7.0.0-30-generic

Terminal

xterm-256color via OpenCode TUI/CLI

Image

Activity

  1. github-actions commented on Sep 5, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of an existing issue. Please check:

  2. royalpinto007 commented on Sep 5, 2026

    @royalpinto007
    Author

    Fix proposed in #47493: classify image-count rejections as context overflow and cap outgoing images at 40 newest per request with placeholders for older ones. Verified with new tests plus existing session suites.

  3. Sugamsss commented on Sep 9, 2026

    @Sugamsss

    We hit this exact problem while doing visual verification loops with Playwright — once you accumulate screenshots, sessions hit 413 payload caps or provider count limits (like the 50-image ceiling here). When that happens, the session gets wedged because the raw base64 stays in the transcript.

    While core handling is being designed, I put together a zero-dependency plugin that solved this for our workflow: opencode-prune-images (added in PR #48096).

    How it works:

    • It hooks into context right before dispatch so your persisted SQLite history is never touched or corrupted.
    • It keeps the latest 7 images as normal raw pictures for the model.
    • Any older images beyond the 7 are transformed in-memory into a 3-point text card capturing the file path, what the model observed in that turn, and the user's intent.
    • If you ask about an older image later ("remember that screenshot with the alignment bug?"), the model sees the path on the card and simply uses its read tool to pull that specific image back into the active 7 slots.
    • It backs up ephemeral /tmp screenshots and pasted base64 data to a small rolling local cache (~/.cache/opencode/recent-images/, capped at 100 files FIFO) so images don't vanish if /tmp gets cleared.

    Sharing here in case it helps unblock anyone running into this right now.

  4. ez2sarang commented on Sep 16, 2026

    @ez2sarang

    Same failure on 1.18.30, provider opencode / muse-spark-1.3-contributor-free (Console), macOS 26.6.2: a UI-iteration session where the agent read 52 screenshots ended with

    Error from provider (Console): Upstream request failed: [invalid_request_error] request contains 52 images, exceeding the maximum of 50 allowed per request
    

    and every later prompt (plain text included) failed identically. Auto-recovery never ran, consistent with the classification gap in #39677.

    A second gap that I don't think has been mentioned yet: the existing prune pass can't help even when it does run, because it budgets by text only:

    Suggestion, complementary to #47493's per-request cap: make prune count attachments too (a fixed token weight per image attachment, and/or an attachment-count budget), so old screenshots get time.compacted like any other large tool output. Once the flag is set, toModelMessages already does the right thing: [Old tool result content cleared] + attachments dropped (https://github.com/anomalyco/opencode/blob/3104c1428e/packages/opencode/src/session/message-v2.ts#L293-L296).

    Recovery that works today for a bricked session (same mechanism prune uses, fully reversible — just delete the key):

    import sqlite3, json, time
    db = sqlite3.connect('~/.local/share/opencode/opencode.db'.replace('~', __import__('os').path.expanduser('~')))
    sid = 'ses_...'; keep_after_ms = 0  # e.g. time_created of the first part you still want as an image
    rows = db.execute("""select id, data from part where session_id=? and json_extract(data,'$.type')='tool'
      and json_extract(data,'$.state.status')='completed'
      and json_array_length(json_extract(data,'$.state.attachments'))>0
      and json_extract(data,'$.state.time.compacted') is null and time_created < ?""", (sid, keep_after_ms)).fetchall()
    now = int(time.time()*1000)
    for pid, data in rows:
        d = json.loads(data); d['state']['time']['compacted'] = now
        db.execute("update part set data=?, time_updated=? where id=?", (json.dumps(d, ensure_ascii=False), now, pid))
    db.commit()

    Back up opencode.db first. Verified on 1.18.30: 35 old image parts flagged → 17 images sent → session usable again, no restart needed (history is re-read from the DB on each turn). Only state.time.compacted is touched — don't put anything non-object into the part's top-level metadata, that field is forwarded as providerOptions and will fail the AI SDK ModelMessage[] schema.

    Also, reading the code, /compact on 1.18.30 serializes history to text with [Attached image/png: file] placeholders (compaction.ts L73-L78), so it shouldn't hit the image cap either; I didn't test it because I'd already recovered via the flag.

  5. Sugamsss commented on Sep 17, 2026

    @Sugamsss

    Thanks for the clear notes. We hit the same wall. Past the image cap, even plain text fails and recovery never kicks in.

    Your second gap looks right. Prune budgets text output only, so image reads barely count and nothing gets flagged. Counting attachments makes sense, next to the cap in 47493. And once flagged, core already drops attachments as it should.

    Thanks for the reversible fix too. Backup first, touch only time.compacted, stay out of metadata. Noted on providerOptions.

    On compact, you are right. V1 uses text placeholders, so it should not hit the cap like normal dispatch. Forcing compaction on every image cap still feels heavy to me. It rewrites history to clear old results. I prefer a rolling window first. History stays intact, newest images stay raw, older ones become cards.

    That is what my plugin does. Latest 7 within 16 MiB stay raw. Older ones become small cards with path and context. No DB writes. I also fixed pruned V1 attachments breaking downstream shapes. They now leave the array and their cards join the tool output.

    Plugin here if useful: https://github.com/Sugamsss/opencode-prune-images

    Shared goal. Sessions should not die from screenshots.

  6. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Metadata

Metadata

Assignees

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