Fix #2884: raise SDK reader buffer (the real #2804 crash fix); reframe per-tool caps as cost discipline - #2885
Conversation
…cipline The #2804 buffer-overflow crash recurs on a routine Edit to a large file (e.g. orchestrator/routes/pipelines.py, 1.1 MB / 25k lines) despite #2876's Read/Grep cap. Root cause (corrected from the issue's first framing): the overflowing message is *not* model-bound. Claude Code attaches the entire original file to every Edit/Write result as transcript metadata (toolUseResult.originalFile) that the model never sees — only egg's stream-json reader decodes it. A ~2 KB edit to a 1.1 MB file therefore emits a >1 MB stream message and crashes the reader, while the model's tool_result is just the bounded fBB cat -n snippet. A per-call input/output cap can't see or police that metadata, so the right lever is the reader buffer itself. - client.py: raise ClaudeAgentOptions.max_buffer_size above the 1 MiB SDK default (32 MiB, tunable via EGG_SDK_MAX_BUFFER_BYTES). Costs no model context/tokens (the field never reaches the model; egg logs <=2 KB of any result) — only transient reader memory. Model-bound result sizes stay bounded independently by the predictive caps, the MCP @tool caps (#2805), and CC's own Bash truncation, so a larger reader buffer can't leak oversized payloads to the model. #2810's fail-fast remains the clean backstop above the raised buffer. - Reframe the now-misleading rationale (the de-misleading half of this change): tool_output_cap.py docstring, the client.py predictive-cap + overflow-marker comments, and docs/reference/agent-recovery.md now state plainly that the per-tool Read/Grep caps are model-context/cost discipline, NOT the crash fix — the raised reader buffer is. No revert of #2805/#2876/#2810: their logic has independent context/cost value and #2810 is the backstop. - Tests: max_buffer_size wiring + EGG_SDK_MAX_BUFFER_BYTES resolver (default/override/invalid); mock ClaudeAgentOptions gains max_buffer_size.
There was a problem hiding this comment.
Review
The core change — raising ClaudeAgentOptions.max_buffer_size above the SDK's 1 MiB default — is correct, the env-var resolver is well-defended (empty/non-int/non-positive all handled with logged fallback), and the investigation of toolUseResult.originalFile as the actual overflow source matches what I can verify in the installed SDK (subprocess_cli.py raises CLIJSONDecodeError when len(json_buffer) > self._max_buffer_size, and types.py:1467 confirms max_buffer_size: int | None = None).
But the PR is sold as a reframing on top of the fix — "the per-tool Read/Grep caps are model-context/cost discipline, not crash prevention" — and that reframing is applied inconsistently. The places the reader will actually consult are still telling the old story.
Blocking
1. shared/egg_tool_output.py / shared/egg_agent/tool_output_cap.py deny reasons still tell the agent the call would crash the session on a 1 MB buffer.
The PR rewrites the module docstring of tool_output_cap.py to say these caps are "model-context/cost discipline, not the buffer-crash fix" — but the strings the agent actually reads on a deny are untouched:
tool_output_cap.py:222-226(Read deny):"…reading it whole risks overflowing the agent's 1 MB message buffer and crashing the session (issue #2804)…"tool_output_cap.py:247-254(Grep deny):"…can return an unbounded volume of matching lines and overflow the agent's 1 MB message buffer (issue #2804)…"
Both are now wrong in two ways: (a) the buffer is no longer 1 MB on the egg path — it's _DEFAULT_SDK_MAX_BUFFER_BYTES = 32 * 1024 * 1024 (32 MiB), and (b) the PR's own framing says these caps are not about crash prevention. Each Read/Grep deny the agent sees in production will contradict the module docstring you're shipping in the same diff. Rewrite the deny strings around context/cost (e.g. "Read denied: '…' is ~N KB, large enough that reading it whole would dump ~M tokens to the model — use offset/limit to page through it") and either drop the #2804 reference or pin it to #2884 for the residual-buffer story.
The same applies to shared/egg_tool_output.py (the egg MCP @tool cap module, not in this diff but in the same blast radius): shared/egg_tool_output.py:3, :45, :127, :294 all still describe the SDK reader as a "hard 1 MB JSON buffer" / "1 MB buffer crash". The reframing claim in the PR description ("Reframed in tool_output_cap.py's docstring…") leaves these neighbors inconsistent.
2. shared/egg_agent/tool_output_cap.py:40-46 justifies the 256 KiB default via the now-raised buffer.
# Default byte threshold above which a whole-file ``Read`` is denied.
# The SDK buffer is 1 MB; a Read result is the file bytes plus per-line
# number prefixes (~7-8 bytes/line) plus JSON-escaping inflation, and it
# shares the 1 MB message with the rest of the turn. 256 KiB leaves ample
# headroom while still letting moderate files through whole. Override with
# EGG_READ_CAP_BYTES.
_DEFAULT_READ_CAP_BYTES = 256 * 1024Two of those sentences are now false on the egg path (The SDK buffer is 1 MB and it shares the 1 MB message). The default value (256 KiB) is still defensible under the new framing — 256 KiB is ~64k tokens, real cost discipline — but the reason for the value as documented here is no longer the operative one. Re-justify in cost/token terms so the next person tuning it isn't reading rationale that the rest of the PR contradicts.
3. orchestrator/consensus_wrapper.py still claims #2805 is the real fix.
consensus_wrapper.py:192-198:
# its retry budget again — the buffer-overflow tests in
# orchestrator/tests/test_consensus_wrapper.py (notably
# test_script_marker_matches_client_constant and the
# test_buffer_overflow_*_aborts_without_retry pair) exercise the
# wrapper against a synthetic log to keep this honest, but do not
# pin against the installed SDK. The real fix is tool-layer
# truncation (#2805); this is the fail-fast path until that lands.
This is the comment block any human (or agent) walking the BRC failure path will land on first, and it directly contradicts the PR description: the PR argues the real fix is the raised reader buffer (#2884), not tool-layer truncation (#2805). consensus_wrapper.py:129 and :182 also still describe the buffer as "1 MB". The PR is in the area of the buffer story and explicitly takes a position on what the real fix is — finish the job here.
Non-blocking suggestions
4. docs/reference/agent-recovery.md:126 still describes the upstream cap as the operative one.
The new "SDK Reader Buffer" section is well-written, but the section immediately preceding it (lines 122-132) reads top-down as "egg's buffer is 1 MB, so the wrapper greps for the marker…" without indicating that egg overrides the default. The line-134 note you added points forward, but a reader who hasn't yet hit it sees an apparent contradiction with the next section. Reordering or re-prefacing line 126 to say "the upstream SDK default is 1 MiB; egg raises it (next section)" would close the loop.
5. Other stale 1 MB references in docs.
docs/development/STRUCTURE.md:273:tool_output_cap.pydescription still says "denies calls likely to overflow the 1 MB SDK buffer".docs/reference/agent-tools.md:218,228: the egg MCP@toolcap section still says "hard 1 MB JSON reader buffer" and "The default leaves ~10× headroom under the 1 MB buffer". Both true upstream; both now misleading on the egg path.
6. _sdk_max_buffer_bytes() warning fires once per run_agent_async invocation; the sibling _read_cap_bytes() deduplicates.
tool_output_cap.py:108-122 maintains _warned_cap_values so a steady bad EGG_READ_CAP_BYTES warns once per distinct value. client.py:_sdk_max_buffer_bytes has no equivalent, so an operator who sets EGG_SDK_MAX_BUFFER_BYTES=2mb gets the warning every time an agent is spawned. Low priority because run_agent_async is called orders of magnitude less often than a Read tool call, but the inconsistency reads like a missed pattern transfer.
7. test_env_resolver_rejects_invalid_values doesn't verify the "logged" half of "logged and ignored".
def test_env_resolver_rejects_invalid_values(self):
for bad in ("not-a-number", "0", "-5", "2mb"):
with patch.dict(os.environ, {"EGG_SDK_MAX_BUFFER_BYTES": bad}):
assert _sdk_max_buffer_bytes() == _DEFAULT_SDK_MAX_BUFFER_BYTESThe function's docstring says the bad value "is logged and ignored, so an operator typo can't silently re-expose the 1 MiB-overflow crash". A regression that dropped the logger.warning call (or routed it to debug level) would still pass this test. Patch the logger and assert at least one warning fires.
8. No upper bound on EGG_SDK_MAX_BUFFER_BYTES.
_sdk_max_buffer_bytes accepts any positive integer. An operator typo like EGG_SDK_MAX_BUFFER_BYTES=34359738368000 (a stray suffix-conversion) would be silently accepted as ~34 TiB, at which point the SDK reader is effectively unbounded and a runaway/malformed stream OOMs the container. The check would be cheap (e.g. cap at 1 GiB and warn) and consistent with the defensive posture you've already taken on the lower bound.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Rewrites the Read/Grep deny strings, _DEFAULT_READ_CAP_BYTES rationale, shared/egg_tool_output.py docstring/notes, consensus_wrapper comments, and the agent-recovery / STRUCTURE / agent-tools doc references so the "per-tool caps are model-context/cost discipline, not the crash fix" framing is consistent everywhere the reader will land — not just in the new tool_output_cap.py docstring. In shared/egg_agent/client.py: - _sdk_max_buffer_bytes warnings now dedup per distinct raw value (mirroring tool_output_cap._warn_invalid_cap) so a steady misconfig doesn't spam on every agent spawn. - Add a 1 GiB hard upper bound on EGG_SDK_MAX_BUFFER_BYTES; an operator typo like a stray suffix-conversion (34359738368000 ≈ 34 TiB) would otherwise leave the reader effectively unbounded and could OOM the container on a runaway stream. In tests/shared/egg_agent/test_client.py: - test_env_resolver_rejects_invalid_values now patches the logger and asserts a warning fires (the docstring promises "logged and ignored" — a regression that dropped the warning was previously silently OK). - Add coverage for the new upper-bound clamp and the per-value dedup behaviour. Authored-by: egg
|
Thanks for the review. All eight items addressed in-PR as Reframing follow-through (items 1–5):
Env-resolver hardening (items 6–8):
Smoke-tested the new behavior locally: default unset → 33554432 (32 MiB); — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Autofix tracking{"Test/Unit Tests": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
The eight items from the prior review were addressed in bbd7c9c:
- Items 1–3 / reframing of
tool_output_cap.pydeny strings,_DEFAULT_READ_CAP_BYTESrationale, andconsensus_wrapper.py"real fix" comments — confirmed in the diff. The two main Read/Grep deny strings now talk about "context budget" rather than the 1 MB buffer crash,_DEFAULT_READ_CAP_BYTESis re-justified in token terms (~4 B/token → 64k tokens), andis_buffer_overflow()'s comment block now points at#2884as the structural fix. - Items 4–5 /
agent-recovery.mdandSTRUCTURE.md+agent-tools.md—agent-recovery.md:126now prefaces the "1 MiB SDK default vs. configured cap" distinction; the STRUCTURE.mdtool_output_cap.pyrow andagent-tools.mdcap section explicitly cross-ref#2884and disclaim crash-prevention. - Items 6–7 / env-resolver dedup + warning assertion in tests —
_warned_sdk_buffer_valuesplus_warn_invalid_sdk_buffer()mirror thetool_output_cappattern;test_env_resolver_rejects_invalid_valuesnow patchesloggerand assertswarning.calledper bad value, and a newtest_env_resolver_warning_dedups_per_valuepins the once-per-value behaviour. - Item 8 / upper-bound clamp —
_MAX_SDK_MAX_BUFFER_BYTES = 1 GiBwith a clamping branch andtest_env_resolver_clamps_absurdly_large_valueexercising the 34 TiB-typo path.
7dd5814 is the necessary follow-up to test_denies_unbounded_read_of_large_file — the old assertion pinned #2804, which the bbd7c9c deny-string rewrite removed; the new assertion ("context budget" in reason) tracks the new framing.
That said, the bbd7c9c reframing pass missed two spots in modified files — both in scope for "finish the reframing":
Blocking
1. shared/egg_tool_output.py:160-163 contradicts its own new top-of-file docstring.
The new file docstring (you just wrote it in this PR) is:
"These caps are model-context/cost discipline, not the buffer-crash fix. The Agent SDK reader's buffer-overflow crash (#2804/#2884) is prevented by raising
ClaudeAgentOptions.max_buffer_sizeinshared/egg_agent/client.py…"
But inside _truncation_marker, the trailing fallback comment still says:
# Pathological tiny cap: even an empty preview leaves the fixed fields
# over the cap. Drop the preview entirely so the marker is as small as it
# can be — still sub-KB, so it can never threaten the 1 MB SDK buffer the
# cap exists to protect."the 1 MB SDK buffer the cap exists to protect" is precisely the framing the rest of the PR — and the docstring at the top of this same file — is rejecting. Rewrite around "so the marker stays well within the cap budget even on a pathologically tiny cap_bytes" (or whatever phrasing matches the cost-discipline framing); drop the SDK-buffer-protection claim.
2. shared/egg_agent/tool_output_cap.py:164-169 agent-facing deny string for non-pageable binaries still says "without risking the overflow".
_read_remedy()'s binary-extension branch — the message an agent sees when it tries to whole-Read a large PNG / JPG / .ipynb (since notebooks are in _NON_PAGEABLE_BINARY_EXTENSIONS too) — was not rewritten:
if suffix in _NON_PAGEABLE_BINARY_EXTENSIONS:
return (
"This binary file is returned whole and cannot be paged, so it "
"cannot be read without risking the overflow. Avoid reading it "
"whole; if you only need metadata, use Bash (e.g. 'file' or 'stat')."
)The other two deny strings (check_read_output_risk:225-230, check_grep_output_risk:254-262) were rewritten in this PR to talk about "context budget"; this third one was missed, so the three deny strings the agent can see from this module are not in the same voice. Worse, an agent landing on this message is told the failure mode is "the overflow" — the exact failure class the PR argues is now handled by the raised reader buffer, not by this hook — and might reasonably try to set EGG_SDK_MAX_BUFFER_BYTES higher (which won't help: this hook fires before the SDK reader ever sees the result). Rewrite the binary branch around the same cost-discipline framing as the text/PDF branches, e.g. "would dump the whole binary to the model in a single tool result — wasteful of context budget."
Non-blocking
3. shared/egg_agent/tool_output_cap.py:179-180 and :236-237 — docstrings of the two check_*_output_risk entry points still say "likely to overflow".
These are the public-API summary lines for both checkers:
:180—"""Return a deny reason if a ``Read`` call is likely to overflow.""":237—"""Return a deny reason if a ``Grep`` call is likely to overflow."""
Internal docstring, not agent-facing, so lower priority — but for someone reading the module top-down, the file docstring says "not the buffer-crash fix", _DEFAULT_READ_CAP_BYTES's comment says "cost knob, not crash-prevention", and then the two functions' docstrings re-introduce "likely to overflow". Re-phrase as "likely to produce an excessive model-bound result" (or similar) so the file is internally consistent end-to-end.
4. docs/reference/agent-tools.md:245 — #2876 is still described as "predicts overflows".
"#2876 adds a PreToolUse hook that predicts overflows before the tool runs and denies the call with a narrowing hint."
This is the "Built-in tool cap (complementary)" paragraph you just rewrote; the surrounding cap paragraph (lines 217-227) and the cross-ref to agent-recovery.md#sdk-reader-buffer-the-crash-prevention-layer both spell out the new framing. The "predicts overflows" line still tells the reader the hook's job is overflow prediction. Re-phrase as "predicts when a result would be excessive before the tool runs and denies the call with a narrowing hint" to match the framing in the rest of the section.
5. _warn_invalid_sdk_buffer and _warned_sdk_buffer_values grow unbounded over a process lifetime.
Mirrors tool_output_cap._warned_cap_values (so the inconsistency from the prior review is fixed), but the set is never pruned. In normal use the cardinality is tiny — an operator sets one value and either fixes it or doesn't — so this is benign. Calling it out only because the same pattern in tool_output_cap has the same bound; if a future fix prunes one, prune both.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Finish the cost-discipline reframing pass: rewrite the binary-extension Read deny string, the two check_*_output_risk docstring summaries, the _truncation_marker pathological-cap comment, and the agent-tools.md #2876 description so the per-tool caps no longer claim to protect the 1 MB SDK buffer.
|
Thanks for the re-review. All five items addressed in-PR as Blocking:
Non-blocking:
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
The four blocking items from the prior review are addressed in ee2c457:
- Items 1 /
shared/egg_tool_output.py:160-163_truncation_markerfallback comment — confirmed atshared/egg_tool_output.py:160-163. The "still sub-KB, so it can never threaten the 1 MB SDK buffer the cap exists to protect" line is now "well within the cap's context budget even at a pathologically tinycap_bytes", which matches the file docstring's cost-discipline framing. - Item 2 /
shared/egg_agent/tool_output_cap.py:164-169binary-extension Read deny string — confirmed atshared/egg_agent/tool_output_cap.py:164-170. The agent-facing message no longer says "without risking the overflow"; it now reads "reading it would dump the entire binary to the model in a single tool result — wasteful of context budget", so the three Read/Grep deny strings from this module are all in the same voice. - Item 3 /
shared/egg_agent/tool_output_cap.py:180,237docstring summaries — bothcheck_read_output_riskandcheck_grep_output_risknow read "would produce an excessive model-bound result" instead of "is likely to overflow", so the file is internally consistent top-to-bottom. - Item 4 /
docs/reference/agent-tools.md:244-246#2876description — confirmed: now reads "predicts when a result would be excessive before the tool runs", matching the surrounding cap section. - Item 5 /
_warned_sdk_buffer_valuesunbounded growth — the author's disagreement is reasonable. The set's cardinality is bounded in practice by distinct operator-set values, and the symmetric pattern intool_output_cap._warned_cap_valueshas the same shape; pruning one without the other would be the inconsistency. Acceptable as-is.
The 7dd5814..ee2c457 diff is purely the reframing strings + the agent-tools.md tweak — no functional code, no test behavior changes. The Read/Grep deny-string assertion in test_denies_unbounded_read_of_large_file already moved to "context budget" in 7dd5814 and remains valid.
Non-blocking
1. tests/shared/egg_agent/test_client.py:981-986 — TestBuiltinOutputCapHook class docstring still says the hook denies calls "before they overflow the SDK's 1 MB buffer".
class TestBuiltinOutputCapHook:
"""Issue #2876: a PreToolUse hook predicts oversized built-in tool
results (Read/Grep) and denies them before they overflow the SDK's
1 MB buffer, telling the agent how to narrow the call. Always-on
(not route-gated); disabled via EGG_TOOL_OUTPUT_CAP=false.
"""This contradicts the new framing the PR ships in the same diff:
shared/egg_agent/tool_output_cap.py:3—"These caps are model-context/cost discipline, not the buffer-crash fix."docs/reference/agent-recovery.md:150—"These caps are model-context/cost discipline, not crash prevention"orchestrator/consensus_wrapper.py:203-204—"The per-tool MCP @tool caps (#2805) and Read/Grep predictive caps (#2876) are independent model-context/cost discipline — not the crash fix."
The PR is in the area of this file (it added the new TestSdkReaderBuffer class at :1076 immediately below this docstring), and the very next test class's docstring (TestBufferOverflowErrorHandling:1158-1164) was updated to acknowledge the raised reader buffer (#2884) makes the overflow path a rare backstop. This docstring is the last spot in the modified files where the old "1 MB buffer / overflow" framing still describes the predictive-cap hook's purpose. Reframe around "predicts oversized built-in tool results … and denies them as context-budget discipline" (or similar) so the test file matches the implementation it exercises.
Lower priority than the agent-facing strings — it's an internal test docstring — but it's the natural symmetric counterpart to items 2-3 (deny-string + entry-point docstring) you've already taken in this PR, and a reader skimming this test class to understand what's being tested would land on the contradicting framing first.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Thanks for the re-review and approval. The one non-blocking item addressed in-PR as Non-blocking:
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
The single non-blocking item from the prior review is addressed in a10e35e:
tests/shared/egg_agent/test_client.py:981-986—TestBuiltinOutputCapHookclass docstring reframed. Confirmed in the diff attests/shared/egg_agent/test_client.py:982-986: the old "denies them before they overflow the SDK's 1 MB buffer" is now "denies them as model-context/cost discipline … Not the buffer-crash fix — that lives in the raised SDK reader buffer (#2884)." This matches the framing inshared/egg_agent/tool_output_cap.py:3,docs/reference/agent-recovery.md:150, andorchestrator/consensus_wrapper.py:203-204, and is internally consistent with the two adjacent test classes in the same file —TestSdkReaderBuffer:1077-1082(which describes the raised buffer as the #2884 fix) andTestBufferOverflowErrorHandling:1159-1165(which acknowledges the raised buffer makes the overflow path a rare backstop).
The remaining 1 MiB references in this file (lines 1079, 1092) describe the upstream SDK default that egg overrides — they are load-bearing for the explanation of why egg raises the buffer and should stay.
The ee2c4577..a10e35e diff is purely the docstring rewrite — no functional code, no new assertions, no test behavior changes. All CI checks pass (Lint, Unit Tests, Integration Tests, Security Scan).
No blocking issues. No further non-blocking suggestions.
— Authored by egg
|
egg review completed. View run logs 11 previous review(s) hidden. |
…e per-tool caps as cost discipline (#2885) * Fix #2884: raise SDK reader buffer; reframe per-tool caps as cost discipline The #2804 buffer-overflow crash recurs on a routine Edit to a large file (e.g. orchestrator/routes/pipelines.py, 1.1 MB / 25k lines) despite #2876's Read/Grep cap. Root cause (corrected from the issue's first framing): the overflowing message is *not* model-bound. Claude Code attaches the entire original file to every Edit/Write result as transcript metadata (toolUseResult.originalFile) that the model never sees — only egg's stream-json reader decodes it. A ~2 KB edit to a 1.1 MB file therefore emits a >1 MB stream message and crashes the reader, while the model's tool_result is just the bounded fBB cat -n snippet. A per-call input/output cap can't see or police that metadata, so the right lever is the reader buffer itself. - client.py: raise ClaudeAgentOptions.max_buffer_size above the 1 MiB SDK default (32 MiB, tunable via EGG_SDK_MAX_BUFFER_BYTES). Costs no model context/tokens (the field never reaches the model; egg logs <=2 KB of any result) — only transient reader memory. Model-bound result sizes stay bounded independently by the predictive caps, the MCP @tool caps (#2805), and CC's own Bash truncation, so a larger reader buffer can't leak oversized payloads to the model. #2810's fail-fast remains the clean backstop above the raised buffer. - Reframe the now-misleading rationale (the de-misleading half of this change): tool_output_cap.py docstring, the client.py predictive-cap + overflow-marker comments, and docs/reference/agent-recovery.md now state plainly that the per-tool Read/Grep caps are model-context/cost discipline, NOT the crash fix — the raised reader buffer is. No revert of #2805/#2876/#2810: their logic has independent context/cost value and #2810 is the backstop. - Tests: max_buffer_size wiring + EGG_SDK_MAX_BUFFER_BYTES resolver (default/override/invalid); mock ClaudeAgentOptions gains max_buffer_size. * Address review feedback: finish reframing and harden env resolver Rewrites the Read/Grep deny strings, _DEFAULT_READ_CAP_BYTES rationale, shared/egg_tool_output.py docstring/notes, consensus_wrapper comments, and the agent-recovery / STRUCTURE / agent-tools doc references so the "per-tool caps are model-context/cost discipline, not the crash fix" framing is consistent everywhere the reader will land — not just in the new tool_output_cap.py docstring. In shared/egg_agent/client.py: - _sdk_max_buffer_bytes warnings now dedup per distinct raw value (mirroring tool_output_cap._warn_invalid_cap) so a steady misconfig doesn't spam on every agent spawn. - Add a 1 GiB hard upper bound on EGG_SDK_MAX_BUFFER_BYTES; an operator typo like a stray suffix-conversion (34359738368000 ≈ 34 TiB) would otherwise leave the reader effectively unbounded and could OOM the container on a runaway stream. In tests/shared/egg_agent/test_client.py: - test_env_resolver_rejects_invalid_values now patches the logger and asserts a warning fires (the docstring promises "logged and ignored" — a regression that dropped the warning was previously silently OK). - Add coverage for the new upper-bound clamp and the per-value dedup behaviour. Authored-by: egg * Fix test_denies_unbounded_read_of_large_file: assert reframed message The PR reframed the per-tool Read cap as model-context/cost discipline rather than #2804 crash prevention, so the deny reason no longer mentions '#2804'. Update the test to assert on the new 'context budget' framing. * Reframe remaining overflow language in tool-output cap surfaces Finish the cost-discipline reframing pass: rewrite the binary-extension Read deny string, the two check_*_output_risk docstring summaries, the _truncation_marker pathological-cap comment, and the agent-tools.md #2876 description so the per-tool caps no longer claim to protect the 1 MB SDK buffer. * Reframe TestBuiltinOutputCapHook docstring as cost discipline --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: james-in-a-box[bot] <246424927+james-in-a-box[bot]@users.noreply.github.com>
…l-output-cap reframing (#2885)
The merge in b5fac8c brought in main's #2885 cost-discipline reframing of "Predictive Output Cap (PreToolUse)" but failed to remove the older #2876 crash-prevention version that landed on the PR side earlier in the slice. The two sections were textually non-overlapping at merge time so git auto-merged them additively, leaving agent-recovery.md with two identical "### Predictive Output Cap (PreToolUse)" headers (L146 and L163), contradictory framing in adjacent paragraphs, and a duplicated tool-heuristics table. Delete the second (older) copy. The remaining section is the post-#2885 version that matches main verbatim and is the intended end state.
Summary
Fixes #2884. The #2804 buffer-overflow crash recurs on a routine
Editto a large file (orchestrator/routes/pipelines.py, 1.1 MB / 25k lines) despite #2876's Read/Grep cap — and it blocks #2777's implement phase.Corrected root cause (the issue's first framing was a guess from missing logs; confirmed here against the checkpoint and the CLI source): the overflowing message is not model-bound. Claude Code attaches the entire original file to every
Edit/Writeresult as transcript metadata (toolUseResult.originalFile) that the model never sees — only egg's Agent SDK stream-json reader decodes it. So a ~2 KB edit to a 1.1 MB file emits a >1 MB stream message and crashes the reader (exit 255), even though the model'stool_resultis just the boundedfBBcat -nsnippet.Verified in
cli.js2.1.157:fBB(the Edit snippet builder) bounds the echoed snippet tonew_stringlines + 8 — file-size-independent, so it was never the culprit.data: { …, originalFile: <full file>, structuredPatch, … }; a separatemapToolResultToToolResultBlockParambuilds the model-bound block (the small snippet). The full-file field rides the stream message, not the model conversation.A per-call input/output cap can't see or police that metadata — so the correct lever is the reader buffer itself.
What changed
client.py— the fix. RaiseClaudeAgentOptions.max_buffer_sizeabove the 1 MiB SDK default to 32 MiB (tunable viaEGG_SDK_MAX_BUFFER_BYTES; invalid values are logged and ignored). This costs no model context or tokens — the oversized field never reaches the model, and egg logs at most_MAX_TOOL_CONTENT_LOG_LENof any result — only transient reader memory for one message. It also can't leak an oversized payload to the model: model-bound result sizes stay bounded independently (the predictive Read/Grep caps, the MCP@toolcaps Cap MCP @tool output sizes at the tool layer (follow-up to #2804) #2805, and CC's own Bash truncation). 32 MiB covers source files far larger than anything in the repo while bounding a runaway stream; Fix #2804: bound tool result size to prevent SDK buffer-overflow crashes #2810's fail-fast remains the clean backstop above it.De-misleading the prior layers (no reverts). The chain (Cap MCP @tool output sizes at the tool layer (follow-up to #2804) #2805 → Fix #2804: bound tool result size to prevent SDK buffer-overflow crashes #2810 → Bound built-in Claude Code tool output (Read/Edit/Grep) via PreToolUse predictive cap (follow-up to #2805/#2810) #2876) was framed as "prevent the 1 MB buffer crash, per tool." That frame is wrong for this crash. Reframed in
tool_output_cap.py's docstring, theclient.pypredictive-cap and overflow-marker comments, anddocs/reference/agent-recovery.md: the per-tool Read/Grep caps are model-context/cost discipline (a whole-file Read = ~275k tokens to the model), not crash prevention — the raised reader buffer is. Cap MCP @tool output sizes at the tool layer (follow-up to #2804) #2805/Bound built-in Claude Code tool output (Read/Edit/Grep) via PreToolUse predictive cap (follow-up to #2805/#2810) #2876/Fix #2804: bound tool result size to prevent SDK buffer-overflow crashes #2810 keep their independent value; nothing is reverted.Tests.
max_buffer_sizewiring +EGG_SDK_MAX_BUFFER_BYTESresolver (default / override / invalid); the mockClaudeAgentOptionsgainsmax_buffer_size.Why not the alternatives
Edit/Writeinput size — the crashing edit'snew_stringwas 2.5 KB; an input cap never fires. Wrong axis.pipelines.py, i.e. block the #2777 decomposition it's meant to unblock. Self-defeating.Test plan
pytest tests/shared/egg_agent/test_client.py tests/shared/egg_agent/test_tool_output_cap.py— 97 passed.ruff check/ruff format/mypyclean on changed files (one pre-existing unrelatedegg_agent_toolsimport-untyped note).Related
@toolcaps (kept; model-bound).