fix(compaction): survive a provider request-body 413 while making room - #6642
Merged
Hmbown merged 13 commits intoSep 29, 2026
Merged
Conversation
A provider body cap is a byte limit the token-side budget cannot see: an
image that costs a flat token estimate can still spend megabytes of
base64, and a summary request carries the whole history. Compaction then
failed exactly when it was needed most, and no later pass could clear the
history stuck behind the cap.
The summary call now descends a two-rung byte ladder before failing:
- re-encode the inline images under a 2 MiB budget (per-image share with
a 96 KiB floor; alpha stays PNG, everything else becomes JPEG);
- if the request is still refused, replace the images with text notes for
that one summary pass, so the handoff still says an image was there.
Each rung announces itself on the status line and the failure text names
the rung it reached. Session history keeps the real images; only the
outbound copy of the request changes.
The detector walks the whole error chain, so a gateway HTML page
("413 Request Entity Too Large ... openresty") counts the same as the
API's own body reader, and it takes precedence over the context-window
ladder — a byte rejection is not a token one.
Files:
- crates/tui/src/image_attach.rs: shrink and placeholder helpers
- crates/tui/src/compaction.rs: notice sink, request-size ladder, detector
- crates/tui/src/core/engine/compaction.rs: engine notice sink
Tests: 9 new cases covering both rejection wordings, both image
carriers, both retry rungs and the full-ladder failure.
Review follow-up on the request-size ladder: - The shrink rung reported "0 images" both when there were none and when every image already fit its share of the budget, so a session whose images were small but whose endpoint cap was lower failed outright instead of reaching the replace rung. `ShrunkInlineImages` now carries `images_seen`, and an in-budget request goes straight to the replace rung — no identical retry, no dead end. - `is_request_too_large_error` also matches "request body too large"; the rendered status-code token remains the primary signal. - The per-image floor comment now states that many images can push the total past the budget, which the replace rung covers. Tests: a fixture with in-budget images that is refused anyway proves the direct-to-replace path; the under-budget shrink test asserts `images_seen`.
… slice CI follow-up on the fix commit: - clippy::ptr_arg on the newer stable toolchain: the replace helper took `&mut Vec<Message>` where `&mut [Message]` does. - The Unreleased entry belongs in the root CHANGELOG.md, which scripts/sync-changelog.sh slices into crates/tui/CHANGELOG.md; editing the slice directly tripped the version-drift check.
The upstream merge moved the root CHANGELOG.md, so both derived copies needed regeneration: - scripts/sync-changelog.sh for the packed slice the binary embeds; - web/scripts/derive-changelog.mjs for the website's generated changelog. Local scripts/release/check-versions.sh is green with the v0.9.13 tag present (feature release-note receipts and version state both OK).
Contributor
Author
|
Three notes from the authoring side:
Attribution🤖 Generated by SpikeBot 000(CodeWhale-LOCAL) |
…he web changelog Merging origin/main brought in another [Unreleased] bullet, so the committed web/lib/changelog.generated.ts no longer matched a fresh derivation (itemCount skew that failed web vitest lib/changelog.test.ts on the merge ref). The contributor-credit check also failed because @SparkofSpike was not in UNRELEASED_CONTRIBUTORS. - CHANGELOG.md [Unreleased]: Contributors line and PR link/thanks on the Fixed bullet; crates/tui/CHANGELOG.md resynced by scripts/sync-changelog.sh. - docs/CONTRIBUTORS.md v0.10.1 band: Hmbown#6642 line. - web/lib/release-credits.ts: @SparkofSpike in UNRELEASED_CONTRIBUTORS. - web/lib/changelog.generated.ts: regenerated by derive-changelog.mjs. Gates run locally: web vitest full suite 498 passed / 0 failed (55 files); lib/changelog.test.ts + lib/public-copy.test.ts 12/12; check-contributor-credit.py v0.10.0 OK (6 contributors, all surfaces); check-versions.sh --range-audit-advisory OK. No Rust files changed in this commit; no Rust build run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hmbown
approved these changes
Sep 29, 2026
…ge-fallback Conflicts: - crates/tui/src/compaction.rs: main made the drop-oldest arm fail loud when nothing but the handoff note is left to drop; this branch excluded byte-side (HTTP 413) rejections from that arm. Kept both: the guard is !is_request_too_large_error && is_context_window_error, and the body is main's drop_oldest_history_messages check (which subsumes the old len() > 2 guard). - CHANGELOG.md, crates/tui/CHANGELOG.md: took main's (v0.10.1 was cut on main); the Hmbown#6642 entry is carried by the release lane instead. - web/lib/changelog.generated.ts: deleted on main, kept deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
docs/public-surface-facts.json requiredCandidateCredits now lists @SparkofSpike for Hmbown#6642, matching docs/CONTRIBUTORS.md (v0.10.1 band) and RELEASE_CONTRIBUTORS in web/lib/release-credits.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
The shrink rung decodes, Lanczos-resizes and re-encodes every inline image in the summary request -- seconds of CPU for the multi-megabyte histories this ladder exists for -- and it ran inline in create_summary on a Tokio worker. It now runs under spawn_blocking with the outbound request moved in and back; a join failure fails the pass with the 413 context instead of panicking. Session history is untouched either way (the request is a copy). Tests: cargo test -p codewhale-tui --lib --locked -- compaction image_attach test result: ok. 211 passed; 0 failed; 0 ignored; 0 measured; 13616 filtered out cargo fmt --all -- --check: clean Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
PR Hmbown#6642 already credits @SparkofSpike in the contributor document, website release list and candidate facts, but the current changelog ledger omitted the contributor. Add the matching credit under v0.10.1 so all four surfaces agree and the existing website parity check passes. This commit extends the contributor's original PR ancestry and changes only CHANGELOG.md. No compaction implementation or test assertion was changed. Validation: - npm --prefix web test -- lib/public-copy.test.ts: 6 passed, 0 failed. - npm test: 636 passed, 0 failed (npm wrapper 68, runtime SDK 16, extension host 50, website 502); SDK type checks passed. - npm run check:web: passed, including facts/docs/tokens, lint, TypeScript and the production website build. - git diff --check: passed. The initial sandboxed npm test attempt failed two wrapper HTTP fixtures with listen EPERM on 127.0.0.1 (66 passed, 2 failed); rerunning with local listener permission passed all gates above. No Rust builds were run.
Regenerate crates/tui/CHANGELOG.md from the already-tested root credit addition in 0e9e5d7. The generated slice differs by that same one line. This repairs the hosted Version drift failure without changing compaction, website logic, test assertions, or contributor history. Validation: scripts/sync-changelog.sh --check passed; scripts/release/check-versions.sh --range-audit-advisory passed, including 29 feature-release-note references and matching 0.10.1 package/lock versions; git diff --check passed. The previous root-credit commit's npm test (636 passed, 0 failed) and npm run check:web passed. Their inputs are unchanged by this generated TUI documentation mirror, so those passing checks were not repeated. No Rust build or release action was needed.
Preserve both contributor credits while retaining the original PR and compaction changes. Validation: npm test 636 passed, 0 failed (134 root, 502 web); npm run check:web passed. Rust source changes in this merge are already-reviewed main commits; full platform CI remains required.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(compaction): survive a provider request-body 413 while making room
Summary
A provider request-body cap (HTTP 413) is a byte limit that the token-side
context budget cannot see: an image that costs a flat token estimate can still
spend megabytes of base64, and a summary ("making room") request carries the
whole history. Compaction therefore failed exactly when it was needed most —
and once it failed, no later pass could clear a history stuck behind the cap.
Found while diagnosing live
HTTP 413failures from a session whose historyheld tens of megabytes of inline images.
Changes
image_attach::shrink_images_for_requestre-encodes every inline image under a 2 MiB total budget (per-image share with
a 96 KiB floor; alpha stays PNG, everything else becomes JPEG; longest edge
1024 px, halved down to 128 px). Rewrites both carriers —
image_urlblocksand the stored tool-result shape the wire projection reads back out.
image_attach::replace_images_with_placeholdersswapsthe images for in-band notes (what was there, roughly how large, and an
instruction not to invent what they showed) for that one summary pass only.
Session history keeps the real images; only the outbound request changes.
create_summary:RequestSizeLadder(Start → ImagesShrunk →ImagesReplaced) gives a refused summary call two byte-side retries before it
fails, and the failure text names the rung it reached. A byte rejection takes
precedence over the context-window (drop-oldest) ladder, because a byte limit
is not a token limit. Images that are present but already fit the budget go
straight to the replace rung instead of dead-ending on a "no images" verdict.
is_request_too_large_errorwalks the whole error chain, so agateway HTML page (
413 Request Entity Too Large … openresty) counts the sameas the API's own body reader.
CompactionNoticeSinkcarried on the preparedenvelope; the engine injects
EngineCompactionNoticeSink(→Event::Status),so each rung says what it is doing on the status line while it does it.
Notes
fixed in
ef5071605, with a test that refuses an in-budget request andexpects the direct-to-replace path.
clippy::ptr_argon the current stable toolchain: the retry helper takes&mut [Message].are heuristics, not derived from one documented provider cap; the replace rung
is what covers a cap below the budget. Remote (
http(s)) image URLs are notrewritten —
parse_data_urlonly recognises inlinedata:payloads, whichmatches the existing wire behaviour.
Type of Change
Testing
cargo fmt --all -- --checkcargo clippy -p codewhale-tui --all-targets --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or— no findings in the touched files (stable 1.98.1)
cargo test -p codewhale-tui --lib -- compaction image_attach→ 196 passed; 0 failed, including 10 new cases: both rejection wordings
(API body reader and gateway HTML page), both image carriers, both retry
rungs, the in-budget direct-to-replace path, and the full-ladder failure
scripts/release/check-versions.shgreen, withscripts/sync-changelog.sh --checkpassingcargo test --workspace --all-features --locked— not run; the touchedsuites above were run instead
Checklist
[Unreleased])covered by a unit test on the engine sink)
external contribution in this change)
Related Issues
No-Issue: found while diagnosing live HTTP 413 failures during compaction; no
upstream issue was opened for this.
Attribution
🤖 Generated by SpikeBot 000(CodeWhale-LOCAL)
Credit-parity follow-up verification
Commit
0e9e5d7c4bd0adafd94199363b6b57601f907e64adds the missing v0.10.1 changelog credit for @SparkofSpike, matching this PR's existing contributor document, website list, and candidate facts. The contributor PR and its original ancestry are preserved; compaction code and assertions are unchanged.npm test: 636 passed, 0 failed (68 wrapper, 16 SDK, 50 host, 502 web).npm run check:web: passed, including production build.An initial sandbox run denied the two local HTTP listeners; the permitted rerun passed. No Rust tests were rerun for this one-line credit change. Hosted checks at the new head remain pending.
Follow-up
e6ad8d3a586070ed87bf3d168b7a6b7c6c0a7bb7regenerates the tracked TUI changelog slice with the same credit. This fixes the Version drift failure found at the preceding head. Bothscripts/sync-changelog.sh --checkandscripts/release/check-versions.sh --range-audit-advisorypass, including 29 feature-note references and matching package/lock versions. The previously passing npm/web inputs are unchanged and those checks were not repeated for this generated documentation mirror. Hosted CI must qualify the new head.Current-main integration (c16a8a4): preserved the original contributor branch and both release credits. npm test passed 636/0 and check:web passed after merging current main. New fork CI runs were approved; current-head platform CI is required before merge.