fix(compaction): survive a provider request-body 413 while making room - #2
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.
结论:可以合并独立审查(只读 diff 与源码,未在本机构建/跑测试)。修复思路正确:413 是请求体的字节上限,token 侧压缩预算看不见 base64 膨胀,因此对一次拒绝过的压缩请求做字节级降级(重编码 → 替换为文本说明)是合理的,且状态机不会死循环或重发同一请求。整体设计干净、边界考虑周全,建议合并。 发现的问题(按严重程度排序)1.(中)阶梯第 1 档把「有图但低于 2MiB 预算」误报成「无图可重编码」,并放弃第 2 档
但
2.(低)
|
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`.
复核结论:修复到位,可以合并增量提交 逐条核对第 1 条(中)——「有图但低于 2MiB 预算被误报成无图、放弃第 2 档」:✅ 已真修好
是否引入新问题:无
另两条:
我实际读过的文件/行
仍未覆盖 / 不确定的部分
署名🤖 由 SpikeBot 003(ClaudeCode-JP) 生成 |
… 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).
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 during compaction on a sessionwhose history held 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.
Review and CI follow-up
above; it was fixed in
ef5071605with a test that refuses an in-budgetrequest and expects the direct-to-replace path.
clippy::ptr_argon the newer stable toolchain: the retry helper now takes&mut [Message].CHANGELOG.md; the packed slice wasregenerated with
scripts/sync-changelog.sh, and the website copy withweb/scripts/derive-changelog.mjs.mainwas merged in (afd6e3e2e), so the PR sits on the currenttree.
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.shlocally green (receipts + versionstate), with
scripts/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)