Skip to content

fix(core): recover sessions after HTTP 413 payload rejections #53107

Description

@TonyYanS

Description

V2 classifies a generic HTTP 413 as payload-too-large, but SessionStep only invokes recovery for context-overflow. A session containing inline images can therefore fail on every continuation even with automatic compaction enabled and plenty of token capacity left.

The compaction implementation already supports removing media after a payload rejection; the primary request path never reaches it. Confirmed in 2.0.20 and reproduced by runner tests on current v2 (f72a103a). Expected: compact and rebuild once before any durable output, respecting compaction.auto; keep original history stored.

Plugins

Production: local continuity and compaction-archive plugins. The failing runner regression uses the repository test fixture without those plugins.

OpenCode version

2.0.20 (local build with unrelated TUI fixes); current v2 also reproduces.

Steps to reproduce

  1. Use an OpenAI-compatible gateway with an 8 MiB HTTP body limit and a model with a large token context.
  2. Read enough images for the request body to exceed that limit, while remaining below the default 25 MiB image-history pruning trigger.
  3. Have the gateway return HTTP 413 with {"error":{"code":"proxy_error","message":"Request body exceeds the configured limit."}}.
  4. Continue the session. No recovery compaction occurs; the same history fails again.

Changing only the error code to request_too_large enters token-overflow recovery, exposing the missing handling of the separate payload classification. A fix should retain that distinction and start the recovery summary without inline media.

Screenshot and/or share link

No private session shared. Synthetic runner regression available in the accompanying patch. Related historical report: #36552.

Operating System

macOS, arm64

Terminal

Ghostty; failure is in the session runner, independent of the client.

Activity

  1. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

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

    • #36552: V2 inline-image payloads cause HTTP 413 failures and prevent session recovery.
    • #14562: Earlier report of HTTP 413 errors with images leaving sessions unrecoverable.
  2. olobarius commented on Oct 18, 2026

    @olobarius

    Additional data point: the oversized-body rejection does not always come from the provider. With the third-party Anthropic auth plugin @ex-machina/opencode-anthropic-auth (<= 2.0.0-next.5), the plugin's own pre-send check throws Anthropic request body exceeds 10485760 byte limit before any HTTP request is made. OpenCode records it as type: "unknown" without a status, so neither the payload-too-large path (#53108) nor the unclassified-4xx path (#56767) applies, and the session dead-ends the same way.

    Session: 20 screenshots ≈ 13.7 MB since the last compaction, far below the token limit, so token-based auto-compaction never triggered. A manual compaction recovered it (the compaction request was ~175 KB because media are not sent). The plugin has since raised its limit to 32 MiB (ex-machina-co/opencode-anthropic-auth#282), but errors thrown from request hooks for oversized bodies (or any failure before a response on a request above a few MiB) could also be treated as payload rejections. OpenCode 2.0.32, Windows.

  3. opencodejr commented on Oct 27, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I've reproduced this on 2.0.20, and it still happens on the latest dev (c250c272).

    What I did: I pointed a mock OpenAI-compatible gateway at OpenCode. It answers any request body over 200 KB with HTTP 413 and {"error":{"code":"proxy_error","message":"Request body exceeds the configured limit."}}. The model has a 1M-token context and compaction.auto is on. I sent one text turn, then a turn with an image of about 480 KB, then a plain --continue.

    What happened: the image turn and the next turn both fail with Error: Request body exceeds the configured limit. The gateway only ever receives requests that still contain the image, and no compaction request is sent.

    Control: if only the error code changes to request_too_large, OpenCode does send a compaction request without the image. That matches your analysis: the runner in packages/core/src/session/runner/step.ts only starts recovery for context-overflow, not payload-too-large. In that control run, the request rebuilt after compaction still included the image and was rejected again, so that is worth checking in a fix as well.

    The script below runs the reproduction. Set ERR_CODE=request_too_large for the control. I couldn't search existing issues, so I haven't checked how #36552 relates.

    Reproduction script (run.sh)
    #!/usr/bin/env bash
    # Reproduces #53107: HTTP 413 "payload-too-large" never triggers recovery compaction.
    # Usage: ERR_CODE=proxy_error|request_too_large ./run.sh   (OC="bun ... packages/cli/src/index.ts" to run from source)
    set -u
    cd "$(dirname "$0")"
    OC=${OC:-opencode}
    PORT=${PORT:-4599}
    ERR_CODE=${ERR_CODE:-proxy_error} PORT=$PORT bun mock.ts > mock.log 2>&1 &
    MOCK=$!; sleep 1; pkill -f "[s]erve --service"; sleep 1
    PROJ=$(mktemp -d /tmp/opencode/p413.XXXX); cp big.png "$PROJ/"; mkdir -p "$PROJ"/{home,data,cfg,state,cache}; cd "$PROJ"; git init -q
    # From source: the CLI respawns itself as `bun src/index.ts serve --service`, which needs the opentui preload.
    [ "$OC" != opencode ] && echo 'preload = ["/workspace/repo/packages/tui/node_modules/@opentui/solid/scripts/preload.js"]' > bunfig.toml
    export HOME=$PROJ/home XDG_DATA_HOME=$PROJ/data XDG_CONFIG_HOME=$PROJ/cfg XDG_STATE_HOME=$PROJ/state XDG_CACHE_HOME=$PROJ/cache
    export OPENCODE_CONFIG_CONTENT=$(cat <<JSON
    {"update":"disable","model":"gw/big","compaction":{"auto":true},
     "providers":{"gw":{"name":"Gateway","package":"aisdk:@ai-sdk/openai-compatible",
      "settings":{"apiKey":"k","baseURL":"http://127.0.0.1:$PORT/v1"},
      "models":{"big":{"name":"Big","capabilities":{"tools":true,"input":["text","image"],"output":["text"]},
       "cost":{"input":0,"output":0},"limit":{"context":1000000,"output":10000}}}}}}
    JSON
    )
    echo "== turn 1 (text)"; timeout 120 $OC run "hello" 2>&1 | tail -5
    echo "== turn 2 (with image, body > 200KB gateway limit)"; timeout 120 $OC run --continue -f big.png "what is in this image" 2>&1 | tail -8
    echo "== turn 3 (continue)"; timeout 120 $OC run --continue "continue" 2>&1 | tail -8
    pkill -f "[s]erve --service"; kill $MOCK; echo "== mock log"; cat "$OLDPWD/mock.log" 2>/dev/null || cat /workspace/repro/mock.log

    Run it: bash run.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions