0.13.0: legible profile default for claude-opus-5 breaks long sessions - collapse bails, raw multi-megabyte history sent as text, upstream 400s
Summary
Since 0.13.0 (#170), claude-opus-5 defaults to the legible history geometry (172 cols, jetbrains-mono-14) instead of the dense one (spleen-5x8, 312 cols). Only collapsed history changes; the static slab and tool-result pages stay dense. On long sessions this makes the rendered history ~7x larger per character (production median 6.7 to 48.3 image_bytes/collapsed_chars, see below). Once the render exceeds the image budget, the atomic-admission check (transform.ts:1991-1998, 2579-2586) abandons the collapse entirely and forwards the raw history as plain text - observed outgoing_text_chars 2.43-2.56M, which the proxy's own count_tokens probe measured at 1.65-1.73M tokens (baseline_tokens, baseline_probe_status: "ok").
Forwards of that size are rejected by the upstream API with 400: of the 8 bail events, the 6 measuring 1.65-1.73M tokens returned 400, while the two that stayed at 825k/832k chars (619k/621k tokens) completed normally (end_turn / tool_use). No 4xx response body was captured, so the threshold is bounded by observation rather than read off an error message.
The turn was re-attempted repeatedly - 4 distinct request bodies (req_body_sha8), two of them sent twice, gaps 9.7-186 s, 7.2 minutes end to end. From the second bail onward the session was already on the cold-cache repack path (history_pack_fill: true), which produced 400s of its own (see below).
Note on budgets: the admission check runs against imageByteHeadroom = 18 MiB minus the caller's native image bytes and the image bytes already admitted for this request (slab, tool-result pages) (transform.ts:1847, limit at transform.ts:158), so the logged image_bytes of the 400 events (1.72-1.77 MiB) is consumption without the abandoned history render.
Environment
- pxpipe-proxy 0.13.0 (regression; window is clean-cut, see Evidence); since downgraded to 0.11.1
- Node v26.4.0, Linux, systemd service
PXPIPE_MODELS=claude-fable-5,claude-opus-5
Evidence (from ~/.pxpipe/events.jsonl)
- Regression window is clean-cut: 0
history_reason: "image_bytes" events in 97.5k logged requests (2026-07-04 through the restart into 0.13.0 at 14:50:50Z); first bail 32 s after that restart. After downgrading to 0.11.1 at 15:22:35Z: 508 requests up to 16:04Z, 234 of them claude-opus-5 with orig_chars > 200k, 0 x 4xx (three transient upstream 502s, all on normal-sized requests).
- All primary-class events carry
history_reason: "image_bytes" - 8 bail events total: 6 x 400, 2 x 200.
- Median render cost
image_bytes/collapsed_chars for claude-opus-5: 6.7 (n=44,411 requests before the restart) to 48.3 (n=85 requests on 0.13.0), measured at the exact restart into 0.13.0.
- Failing requests:
outgoing_text_chars 2.43-2.56M sent uncollapsed (orig_chars of the static slab stays ~204-208k); the count_tokens probe puts them at 1.65-1.73M tokens.
- Secondary failure class: 8 x 400 with
history_reason: "collapsed" and normal-looking outgoing_text_chars (69.7-139.7k), 14:55:34Z-15:14:58Z, 6 of them carrying history_pack_fill: true - all after the first bail and interleaved with the tail of the primary class. No 4xx body captured, so cause unconfirmed.
Expected behavior
If the legible geometry blows the image budget, degrade to the dense profile (or split/chunk the render) instead of abandoning the collapse. Sending 1.65-1.73M tokens of raw text as the fallback cannot succeed - bailing must not produce a request larger than the render it refused to send.
Repro
- Session where the legible render of the collapsed history exceeds the 18 MiB headroom with
claude-opus-5 on 0.13.0 defaults (in our case the abandoned raw forward was 2.4-2.6M chars; the static slab ~205k chars).
- Watch events.jsonl:
history_reason: "image_bytes", then plain-text forward, upstream 400.
Workarounds
- Downgrade to a pre-legible-default version (we run 0.11.1) - verified in production, numbers above.
- A
PXPIPE_GPT_PROFILES entry for the model id should equally avoid the bail - the env map is consulted before the built-in Claude table (gpt-model-profiles.ts, resolveGptProfile) and accepts both fields: PXPIPE_GPT_PROFILES='{"claude-opus-5":{"historyStripCols":312,"historyStyle":{"font":"spleen-5x8"}}}'. Set both together - omitted fields inherit from the legible built-in. Not verified in production.
Related: #210 (frozen history re-rendered every turn - the legible default multiplies that cost as well).
0.13.0: legible profile default for claude-opus-5 breaks long sessions - collapse bails, raw multi-megabyte history sent as text, upstream 400s
Summary
Since 0.13.0 (#170),
claude-opus-5defaults to the legible history geometry (172 cols,jetbrains-mono-14) instead of the dense one (spleen-5x8, 312 cols). Only collapsed history changes; the static slab and tool-result pages stay dense. On long sessions this makes the rendered history ~7x larger per character (production median 6.7 to 48.3image_bytes/collapsed_chars, see below). Once the render exceeds the image budget, the atomic-admission check (transform.ts:1991-1998,2579-2586) abandons the collapse entirely and forwards the raw history as plain text - observedoutgoing_text_chars2.43-2.56M, which the proxy's owncount_tokensprobe measured at 1.65-1.73M tokens (baseline_tokens,baseline_probe_status: "ok").Forwards of that size are rejected by the upstream API with 400: of the 8 bail events, the 6 measuring 1.65-1.73M tokens returned 400, while the two that stayed at 825k/832k chars (619k/621k tokens) completed normally (
end_turn/tool_use). No 4xx response body was captured, so the threshold is bounded by observation rather than read off an error message.The turn was re-attempted repeatedly - 4 distinct request bodies (
req_body_sha8), two of them sent twice, gaps 9.7-186 s, 7.2 minutes end to end. From the second bail onward the session was already on the cold-cache repack path (history_pack_fill: true), which produced 400s of its own (see below).Note on budgets: the admission check runs against
imageByteHeadroom= 18 MiB minus the caller's native image bytes and the image bytes already admitted for this request (slab, tool-result pages) (transform.ts:1847, limit attransform.ts:158), so the loggedimage_bytesof the 400 events (1.72-1.77 MiB) is consumption without the abandoned history render.Environment
PXPIPE_MODELS=claude-fable-5,claude-opus-5Evidence (from
~/.pxpipe/events.jsonl)history_reason: "image_bytes"events in 97.5k logged requests (2026-07-04 through the restart into 0.13.0 at 14:50:50Z); first bail 32 s after that restart. After downgrading to 0.11.1 at 15:22:35Z: 508 requests up to 16:04Z, 234 of themclaude-opus-5withorig_chars> 200k, 0 x 4xx (three transient upstream 502s, all on normal-sized requests).history_reason: "image_bytes"- 8 bail events total: 6 x 400, 2 x 200.image_bytes/collapsed_charsforclaude-opus-5: 6.7 (n=44,411 requests before the restart) to 48.3 (n=85 requests on 0.13.0), measured at the exact restart into 0.13.0.outgoing_text_chars2.43-2.56M sent uncollapsed (orig_charsof the static slab stays ~204-208k); thecount_tokensprobe puts them at 1.65-1.73M tokens.history_reason: "collapsed"and normal-lookingoutgoing_text_chars(69.7-139.7k), 14:55:34Z-15:14:58Z, 6 of them carryinghistory_pack_fill: true- all after the first bail and interleaved with the tail of the primary class. No 4xx body captured, so cause unconfirmed.Expected behavior
If the legible geometry blows the image budget, degrade to the dense profile (or split/chunk the render) instead of abandoning the collapse. Sending 1.65-1.73M tokens of raw text as the fallback cannot succeed - bailing must not produce a request larger than the render it refused to send.
Repro
claude-opus-5on 0.13.0 defaults (in our case the abandoned raw forward was 2.4-2.6M chars; the static slab ~205k chars).history_reason: "image_bytes", then plain-text forward, upstream 400.Workarounds
PXPIPE_GPT_PROFILESentry for the model id should equally avoid the bail - the env map is consulted before the built-in Claude table (gpt-model-profiles.ts,resolveGptProfile) and accepts both fields:PXPIPE_GPT_PROFILES='{"claude-opus-5":{"historyStripCols":312,"historyStyle":{"font":"spleen-5x8"}}}'. Set both together - omitted fields inherit from the legible built-in. Not verified in production.Related: #210 (frozen history re-rendered every turn - the legible default multiplies that cost as well).