Skip to content

Raise the CLI stdout line cap so image reads stop aborting turns - #424

Merged
pufit merged 1 commit into
mainfrom
pufit/sdk-stdout-buffer-images
Sep 5, 2026
Merged

Raise the CLI stdout line cap so image reads stop aborting turns#424
pufit merged 1 commit into
mainfrom
pufit/sdk-stdout-buffer-images

Conversation

@pufit

@pufit pufit commented Sep 4, 2026

Copy link
Copy Markdown
Member

Problem

Sessions sometimes die mid-turn right after the agent Reads an image, with

claude_agent_sdk._internal.query: Fatal error in message reader: Failed to decode JSON: JSON message exceeded maximum buffer size of 1048576 bytes...
nerve.agent.engine: Agent error: Failed to decode JSON: JSON message exceeded maximum buffer size of 1048576 bytes...

14 occurrences in the gateway log since Jul 8, 6 of them in the last two days (screenshot-heavy UI work).

Root cause. The Agent SDK's SubprocessCLITransport bounds one stream-json stdout line at max_buffer_size, default 1 MiB (_DEFAULT_MAX_BUFFER_SIZE, unchanged upstream as of 0.2.152), and a line over the bound raises CLIJSONDecodeError inside the reader task — fatal for the whole turn, not just the one tool call. Nerve never set the option. A Read of an image blows through 1 MiB easily because the CLI

  1. ships the base64 payload twice per line (the tool_result content block and the top-level tool_use_result), and
  2. resizes + re-encodes anything over 2000 px (image_limits {maxWidth: 2000, maxHeight: 2000, maxBase64Size: 5 MiB}), which can inflate a PNG.

Measured with the bundled CLI (2.1.257): a 269 KB, 1280×2400 screenshot came out as a 1,304,261-byte line; a 420 KB, 1429×1370 one (no re-encode) as 1,121,668 bytes. So the crash threshold is roughly a 390 KB encoded image — ordinary Retina/full-page screenshots. Parallel reads are separate lines; one oversized image is enough.

The existing PreToolUse image validator only checked magic bytes and the 5 MB API limit (it was written for the poisoned-context bug), so these images sailed through it.

Fix

  • agent.cli_max_message_bytes (new, default 64 MiB) → passed as ClaudeAgentOptions.max_buffer_size. The CLI caps a single image at 5 MiB of base64, so ~10.5 MB is the per-image worst case; 64 MiB leaves headroom for document blocks. Documented in docs/config.md.
  • validate_image_file(..., max_message_bytes=) refuses an image whose encoded line (2 × base64 + envelope) cannot fit under the bound, so a too-big image fails as a tool error with a downscale hint instead of aborting the turn. The Read hook passes the configured bound.
  • Engine: the buffer-overflow exception is rendered as a readable error that names the knob (the session stays resumable — the CLI transcript is intact).

Testing

  • tests/test_image_validation.py (new): wire-size estimate (both copies, 5 MiB saturation), transport refusal vs. roomy cap, API limit still wins, bad magic still refused, non-image extensions ignored.
  • tests/test_engine.py: max_buffer_size reaches ClaudeAgentOptions (default and override); Read hook denies an oversized image under a 1 MiB cap and allows it under the default.
  • Full suite: 3377 passed.

Takes effect on the next gateway restart.

Generated by Nerve

The Agent SDK caps one stream-json line at 1 MiB by default and kills the
reader (and the turn) when a line exceeds it. A Read of a screenshot
routinely does: the CLI ships the base64 twice per line and re-encodes
anything over 2000 px. Expose agent.cli_max_message_bytes (64 MiB), pass
it as max_buffer_size, refuse images that cannot fit under it in the Read
hook, and name the cause in the error message.
@pufit
pufit merged commit f96618b into main Sep 5, 2026
3 checks passed
@pufit
pufit deleted the pufit/sdk-stdout-buffer-images branch September 5, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant