Skip to content

fix(proxy): keep codex image-generation WS turns alive through the relay#1000

Merged
JerrettDavis merged 1 commit into
headroomlabs-ai:mainfrom
lifeodyssey:fix/codex-imagegen-ws-relay
Jun 16, 2026
Merged

fix(proxy): keep codex image-generation WS turns alive through the relay#1000
JerrettDavis merged 1 commit into
headroomlabs-ai:mainfrom
lifeodyssey:fix/codex-imagegen-ws-relay

Conversation

@lifeodyssey

@lifeodyssey lifeodyssey commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Description

Image generation through the proxy fails. Driving Codex (/v1/responses over
WebSocket) through Headroom, an image-generation turn never returns an image —
the client retries (Reconnecting… n/5) and gives up, while the same prompt
works when Codex talks to ChatGPT directly.

Root cause: two independent defects on the upstream websockets.connect(), both
specific to how image generation behaves on the wire:

  1. Pong deadline kills the silent render. An image turn emits a single
    response.image_generation_call.generating event and then goes silent for
    20–60s while the model renders (no data frames). The hard-coded
    ping_timeout=20 treats that healthy-but-quiet connection as dead and tears
    it down as upstream_error mid-render, before the image is ready.
  2. 1 MiB frame cap drops the image. The finished image comes back inline as
    a single base64 frame that exceeds the websockets default max_size=2**20
    (1 MiB), raising PayloadTooBig exactly as the image lands.

They compound: with only ping fixed, the session survives the silent phase
(observed ~20s → ~54s) but then dies on the oversized image frame. Normal
text/tool turns stream tokens continuously and stay well under 1 MiB, so neither
defect affects them — which is why this only ever bit image generation.

Closes: N/A (no tracking issue)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Code refactoring (no functional changes)

Changes Made

  • headroom/proxy/handlers/openai.py: on the upstream /v1/responses connect,
    set ping_timeout=None (keep ping_interval=20 for NAT keepalive) so a long
    silent render is not torn down on a missing pong, and max_size=None so the
    inline base64 image frame is accepted instead of raising PayloadTooBig.
  • tests/test_openai_codex_ws_lifecycle.py: add
    test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline, which
    captures the upstream connect kwargs and pins ping_timeout is None /
    max_size is None.

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • New tests added for new functionality
  • Manual testing performed

Test Output

$ ruff check .
All checks passed!

$ mypy headroom --ignore-missing-imports
Success: no issues found in 358 source files

$ pytest tests/test_openai_codex_ws_lifecycle.py -q
collected 15 items
tests/test_openai_codex_ws_lifecycle.py ..............                   [100%]
============================== 15 passed in 0.72s ==============================

$ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q
collected 15 items / 14 deselected / 1 selected
tests/test_openai_codex_ws_lifecycle.py .                                [100%]
======================= 1 passed, 14 deselected in 0.35s =======================

End-to-end (managed Codex image generation through the running proxy):

# BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only)
#   WS /v1/responses completed (cause=upstream_error,
#       last_upstream_type=response.image_generation_call.generating)
#   -> client "Reconnecting… n/5", no image produced

# AFTER fix:
[codex] Image ready; stopping the turn.
Saved image: /tmp/headroom-imagegen-test.png
$ file /tmp/headroom-imagegen-test.png
PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced   (908 KB)
# proxy session count +1 -> the turn DID traverse the proxy and completed.

Real Behavior Proof

  • Environment: macOS, headroom 0.23.0 running as the Codex model_provider
    (proxy on 127.0.0.1:8787), Codex CLI 0.139.0 driving a managed
    /v1/responses image-generation turn through the proxy.
  • Exact command / steps: trigger a Codex image-generation turn (gpt-image-2)
    with the proxy in front; observe the upstream /v1/responses WS session in
    proxy.log and whether a PNG is returned.
  • Observed result: before the change the session dies with upstream_error
    while last_upstream_type=response.image_generation_call.generating and no
    image is produced; after the change a valid 1254×1254 PNG is returned and the
    session traverses the proxy normally.
  • Not tested: the full pytest suite was not run locally — this machine has no
    Rust toolchain to rebuild the matching _core extension, so the complete
    suite (incl. the pyo3 tests) is left to CI. The affected
    test_openai_codex_ws_lifecycle.py module was run against the installed
    extension and passes 15/15; ruff check . and mypy headroom were run in
    full and pass.

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the CHANGELOG.md if applicable

Screenshots (if applicable)

N/A.

Additional Notes

  • ruff check . (whole repo) and mypy headroom --ignore-missing-imports
    (358 source files) were run locally and pass. The only pytest not run
    locally is the full suite, because the Rust _core cannot be rebuilt here
    without a toolchain; the directly affected lifecycle module passes 15/15 and
    CI runs the rest.
  • Documentation / CHANGELOG left unchecked — this is a focused two-line
    behavioral fix on the upstream WS connect; happy to add a CHANGELOG entry if
    preferred.
  • ping_timeout=None keeps ping_interval for NAT keepalive; if you'd rather
    bound it, a generous finite value (e.g. 300s) would also fix the render case —
    happy to switch.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR follows the template and is marked ready for human review.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ci failing Required or reported CI checks are failing status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: needs author action Pull request body or readiness checklist still needs author updates status: ready for review Pull request body is complete and the author marked it ready for human review labels Jun 15, 2026
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Codex /v1/responses image-generation turns failed through the proxy in two
compounding ways the upstream WS connect was blind to:

1. The render phase goes silent for 20-60s (a single
   image_generation_call.generating event, then no data frames). The 20s
   ping_timeout pong deadline false-killed the still-healthy upstream
   mid-render with upstream_error before the image landed.
2. The finished image arrives inline as a single base64 frame larger than the
   websockets default 1 MiB max_size, raising PayloadTooBig exactly as it
   landed.

Set ping_timeout=None (keep ping_interval for NAT keepalive) and max_size=None
on the upstream /v1/responses connect so long silent renders survive and the
image frame is accepted. Text/tool turns stream continuously and are
unaffected. Adds a regression test pinning both connect kwargs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lifeodyssey lifeodyssey force-pushed the fix/codex-imagegen-ws-relay branch from b3dd38c to dbf8897 Compare June 15, 2026 09:48
@github-actions github-actions Bot removed the status: ci failing Required or reported CI checks are failing label Jun 15, 2026

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for the contribution.

@JerrettDavis JerrettDavis merged commit 7dbbb40 into headroomlabs-ai:main Jun 16, 2026
23 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 16, 2026
chopratejas pushed a commit that referenced this pull request Jun 16, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.26.0</summary>

##
[0.26.0](v0.25.0...v0.26.0)
(2026-06-16)


### Features

* add Copilot BYOK provider wrapper utilities and CLI support
([#1041](#1041))
([e67ee2a](e67ee2a))
* add dashboard agent usage stats
([#814](#814))
([6d3f39f](6d3f39f))
* Add support for Mistral Vibe CLI
([#935](#935))
([0932b8b](0932b8b))
* attribute reread waste to over-compression via marker check
([#901](#901))
([f928576](f928576))
* **bedrock:** cross-region + Converse compression; bundle proxy binary
in images ([#999](#999))
([0dc2e1c](0dc2e1c))
* **dashboard:** surface compression-vs-cache net impact in Prefix Cache
panel ([#913](#913))
([2a4d300](2a4d300))
* **evals:** adversarial-input robustness grid for compressors
([#918](#918))
([5939004](5939004))
* **parser:** detect re-issued identical tool calls as reread waste
([#909](#909))
([7d4ae86](7d4ae86))
* **policy:** batch deep edits through one cache-bust
([#856](#856) P3a)
([#1015](#1015))
([c2e52fe](c2e52fe))
* **policy:** consume net-cost mutation gate in ContentRouter
([#856](#856) P2)
([#905](#905))
([553ade4](553ade4))
* **proxy:** compress AWS Bedrock InvokeModel requests via configurable
upstream ([#720](#720))
([7edb27a](7edb27a))


### Bug Fixes

* **anthropic:** strip styled Claude model ids
([#651](#651))
([0c5c89d](0c5c89d))
* **anyllm:** forward openai api_base/api_key to the any-llm backend
([#942](#942))
([#954](#954))
([a7ee8a6](a7ee8a6))
* **cache:** guard None exemplar embeddings in dynamic detector
([#950](#950))
([1ec9320](1ec9320))
* **cache:** name the missing piece in semantic detector guard
([#1018](#1018))
([3b0bcee](3b0bcee))
* **ci:** check out repo in PR Governance label job
([#1021](#1021))
([4558bc2](4558bc2))
* **ci:** make PR governance advisory
([#1047](#1047))
([74dff94](74dff94))
* **codex:** compute waste signals on the OpenAI Responses path
([#898](#898))
([b9e2761](b9e2761))
* **codex:** poll /wham/usage for subscription limits (handshake no
longer sends x-codex-* headers)
([#924](#924))
([8c00f71](8c00f71))
* **codex:** PR health label check state
([#986](#986))
([99c874d](99c874d))
* **codex:** retag thread providers so history menu stays whole across
the proxy boundary
([#1034](#1034))
([74ae781](74ae781))
* **codex:** write canonical hooks feature flag and migrate deprecated
codex_hooks ([#743](#743))
([dff6a19](dff6a19))
* **compression:** convert tree-sitter byte offsets to char offsets
([#892](#892))
([b1f700f](b1f700f))
* **compression:** correct JSON array item counting and entropy gate
([#887](#887))
([d6f0f0f](d6f0f0f))
* **compression:** keep container bodies compressible in code handler
([#890](#890))
([16ed73b](16ed73b))
* **compression:** measure short-value threshold on payload, not token
([#889](#889))
([65b0e8c](65b0e8c))
* **compression:** use thread-local tree-sitter parsers in code handler
([#893](#893))
([6cdb846](6cdb846))
* **gemini:** surface functionResponse payloads to waste-signal
detection ([#897](#897))
([9b0c840](9b0c840))
* **learn:** decode directory names with spaces in Windows project paths
([#997](#997))
([#1027](#1027))
([2d3701b](2d3701b))
* **learn:** scan subagent and workflow transcripts
([#1045](#1045))
([0ddd4ed](0ddd4ed))
* **openclaw:** declare headroom_retrieve tool contract
([#947](#947))
([7c8c909](7c8c909))
* **policy:** correct warm-cache penalty in net_mutation_gain to (S +
dT) ([#903](#903))
([0632eba](0632eba))
* **proxy:** add native Bedrock converse-stream route
([#917](#917))
([b08ec15](b08ec15))
* **proxy:** keep codex image-generation WS turns alive through the
relay ([#1000](#1000))
([7dbbb40](7dbbb40))
* **proxy:** make budget enforcement actually work
([#885](#885))
([a14ab45](a14ab45))
* **proxy:** read RTK gain stats globally by default
([#957](#957))
([b70fccb](b70fccb))
* route v1internal code assist requests to cloudcode-pa.googleapis…
([#821](#821))
([e20f16b](e20f16b))
* **serena:** stop the Serena dashboard popup and make --no-serena
actually disable Serena
([#1003](#1003))
([919379a](919379a))
* support Copilot Business subscription auth
([#641](#641))
([0b4a4bd](0b4a4bd))
* wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy
entrypoint ([#943](#943))
([9b7b436](9b7b436))
* **wrap:** avoid duplicate top-level keys when injecting codex provider
([#884](#884))
([dd22cfd](dd22cfd))


### Code Refactoring

* DRY cache logic, add thread safety, fix Bash exclusion
([#704](#704))
([e36fccd](e36fccd))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Epicism pushed a commit to Epicism/headroom that referenced this pull request Jun 17, 2026
…lay (headroomlabs-ai#1000)

## Description

Image generation through the proxy fails. Driving Codex (`/v1/responses`
over
WebSocket) through Headroom, an image-generation turn never returns an
image —
the client retries (`Reconnecting… n/5`) and gives up, while the same
prompt
works when Codex talks to ChatGPT directly.

Root cause: two independent defects on the upstream
`websockets.connect()`, both
specific to how image generation behaves on the wire:

1. **Pong deadline kills the silent render.** An image turn emits a
single
`response.image_generation_call.generating` event and then goes silent
for
   20–60s while the model renders (no data frames). The hard-coded
`ping_timeout=20` treats that healthy-but-quiet connection as dead and
tears
   it down as `upstream_error` mid-render, before the image is ready.
2. **1 MiB frame cap drops the image.** The finished image comes back
inline as
a single base64 frame that exceeds the `websockets` default
`max_size=2**20`
   (1 MiB), raising `PayloadTooBig` exactly as the image lands.

They compound: with only ping fixed, the session survives the silent
phase
(observed ~20s → ~54s) but then dies on the oversized image frame.
Normal
text/tool turns stream tokens continuously and stay well under 1 MiB, so
neither
defect affects them — which is why this only ever bit image generation.

Closes: N/A (no tracking issue)

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/proxy/handlers/openai.py`: on the upstream `/v1/responses`
connect,
set `ping_timeout=None` (keep `ping_interval=20` for NAT keepalive) so a
long
silent render is not torn down on a missing pong, and `max_size=None` so
the
inline base64 image frame is accepted instead of raising
`PayloadTooBig`.
- `tests/test_openai_codex_ws_lifecycle.py`: add
`test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline`,
which
  captures the upstream connect kwargs and pins `ping_timeout is None` /
  `max_size is None`.

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ ruff check .
All checks passed!

$ mypy headroom --ignore-missing-imports
Success: no issues found in 358 source files

$ pytest tests/test_openai_codex_ws_lifecycle.py -q
collected 15 items
tests/test_openai_codex_ws_lifecycle.py ..............                   [100%]
============================== 15 passed in 0.72s ==============================

$ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q
collected 15 items / 14 deselected / 1 selected
tests/test_openai_codex_ws_lifecycle.py .                                [100%]
======================= 1 passed, 14 deselected in 0.35s =======================
```

End-to-end (managed Codex image generation through the running proxy):

```text
# BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only)
#   WS /v1/responses completed (cause=upstream_error,
#       last_upstream_type=response.image_generation_call.generating)
#   -> client "Reconnecting… n/5", no image produced

# AFTER fix:
[codex] Image ready; stopping the turn.
Saved image: /tmp/headroom-imagegen-test.png
$ file /tmp/headroom-imagegen-test.png
PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced   (908 KB)
# proxy session count +1 -> the turn DID traverse the proxy and completed.
```

## Real Behavior Proof

- Environment: macOS, headroom 0.23.0 running as the Codex
`model_provider`
  (proxy on `127.0.0.1:8787`), Codex CLI 0.139.0 driving a managed
  `/v1/responses` image-generation turn through the proxy.
- Exact command / steps: trigger a Codex image-generation turn
(gpt-image-2)
with the proxy in front; observe the upstream `/v1/responses` WS session
in
  `proxy.log` and whether a PNG is returned.
- Observed result: before the change the session dies with
`upstream_error`
while `last_upstream_type=response.image_generation_call.generating` and
no
image is produced; after the change a valid 1254×1254 PNG is returned
and the
  session traverses the proxy normally.
- Not tested: the full `pytest` suite was not run locally — this machine
has no
Rust toolchain to rebuild the matching `_core` extension, so the
complete
  suite (incl. the pyo3 tests) is left to CI. The affected
`test_openai_codex_ws_lifecycle.py` module was run against the installed
extension and passes 15/15; `ruff check .` and `mypy headroom` were run
in
  full and pass.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

- `ruff check .` (whole repo) and `mypy headroom
--ignore-missing-imports`
(358 source files) were run locally and pass. The only `pytest` not run
locally is the full suite, because the Rust `_core` cannot be rebuilt
here
without a toolchain; the directly affected lifecycle module passes 15/15
and
  CI runs the rest.
- Documentation / CHANGELOG left unchecked — this is a focused two-line
behavioral fix on the upstream WS connect; happy to add a CHANGELOG
entry if
  preferred.
- `ping_timeout=None` keeps `ping_interval` for NAT keepalive; if you'd
rather
bound it, a generous finite value (e.g. 300s) would also fix the render
case —
  happy to switch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
studyzy pushed a commit to studyzy/headroom that referenced this pull request Jun 24, 2026
…lay (headroomlabs-ai#1000)

## Description

Image generation through the proxy fails. Driving Codex (`/v1/responses`
over
WebSocket) through Headroom, an image-generation turn never returns an
image —
the client retries (`Reconnecting… n/5`) and gives up, while the same
prompt
works when Codex talks to ChatGPT directly.

Root cause: two independent defects on the upstream
`websockets.connect()`, both
specific to how image generation behaves on the wire:

1. **Pong deadline kills the silent render.** An image turn emits a
single
`response.image_generation_call.generating` event and then goes silent
for
   20–60s while the model renders (no data frames). The hard-coded
`ping_timeout=20` treats that healthy-but-quiet connection as dead and
tears
   it down as `upstream_error` mid-render, before the image is ready.
2. **1 MiB frame cap drops the image.** The finished image comes back
inline as
a single base64 frame that exceeds the `websockets` default
`max_size=2**20`
   (1 MiB), raising `PayloadTooBig` exactly as the image lands.

They compound: with only ping fixed, the session survives the silent
phase
(observed ~20s → ~54s) but then dies on the oversized image frame.
Normal
text/tool turns stream tokens continuously and stay well under 1 MiB, so
neither
defect affects them — which is why this only ever bit image generation.

Closes: N/A (no tracking issue)

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/proxy/handlers/openai.py`: on the upstream `/v1/responses`
connect,
set `ping_timeout=None` (keep `ping_interval=20` for NAT keepalive) so a
long
silent render is not torn down on a missing pong, and `max_size=None` so
the
inline base64 image frame is accepted instead of raising
`PayloadTooBig`.
- `tests/test_openai_codex_ws_lifecycle.py`: add
`test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline`,
which
  captures the upstream connect kwargs and pins `ping_timeout is None` /
  `max_size is None`.

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ ruff check .
All checks passed!

$ mypy headroom --ignore-missing-imports
Success: no issues found in 358 source files

$ pytest tests/test_openai_codex_ws_lifecycle.py -q
collected 15 items
tests/test_openai_codex_ws_lifecycle.py ..............                   [100%]
============================== 15 passed in 0.72s ==============================

$ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q
collected 15 items / 14 deselected / 1 selected
tests/test_openai_codex_ws_lifecycle.py .                                [100%]
======================= 1 passed, 14 deselected in 0.35s =======================
```

End-to-end (managed Codex image generation through the running proxy):

```text
# BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only)
#   WS /v1/responses completed (cause=upstream_error,
#       last_upstream_type=response.image_generation_call.generating)
#   -> client "Reconnecting… n/5", no image produced

# AFTER fix:
[codex] Image ready; stopping the turn.
Saved image: /tmp/headroom-imagegen-test.png
$ file /tmp/headroom-imagegen-test.png
PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced   (908 KB)
# proxy session count +1 -> the turn DID traverse the proxy and completed.
```

## Real Behavior Proof

- Environment: macOS, headroom 0.23.0 running as the Codex
`model_provider`
  (proxy on `127.0.0.1:8787`), Codex CLI 0.139.0 driving a managed
  `/v1/responses` image-generation turn through the proxy.
- Exact command / steps: trigger a Codex image-generation turn
(gpt-image-2)
with the proxy in front; observe the upstream `/v1/responses` WS session
in
  `proxy.log` and whether a PNG is returned.
- Observed result: before the change the session dies with
`upstream_error`
while `last_upstream_type=response.image_generation_call.generating` and
no
image is produced; after the change a valid 1254×1254 PNG is returned
and the
  session traverses the proxy normally.
- Not tested: the full `pytest` suite was not run locally — this machine
has no
Rust toolchain to rebuild the matching `_core` extension, so the
complete
  suite (incl. the pyo3 tests) is left to CI. The affected
`test_openai_codex_ws_lifecycle.py` module was run against the installed
extension and passes 15/15; `ruff check .` and `mypy headroom` were run
in
  full and pass.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

- `ruff check .` (whole repo) and `mypy headroom
--ignore-missing-imports`
(358 source files) were run locally and pass. The only `pytest` not run
locally is the full suite, because the Rust `_core` cannot be rebuilt
here
without a toolchain; the directly affected lifecycle module passes 15/15
and
  CI runs the rest.
- Documentation / CHANGELOG left unchecked — this is a focused two-line
behavioral fix on the upstream WS connect; happy to add a CHANGELOG
entry if
  preferred.
- `ping_timeout=None` keeps `ping_interval` for NAT keepalive; if you'd
rather
bound it, a generous finite value (e.g. 300s) would also fix the render
case —
  happy to switch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
studyzy pushed a commit to studyzy/headroom that referenced this pull request Jun 24, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.26.0</summary>

##
[0.26.0](headroomlabs-ai/headroom@v0.25.0...v0.26.0)
(2026-06-16)


### Features

* add Copilot BYOK provider wrapper utilities and CLI support
([headroomlabs-ai#1041](headroomlabs-ai#1041))
([e67ee2a](headroomlabs-ai@e67ee2a))
* add dashboard agent usage stats
([headroomlabs-ai#814](headroomlabs-ai#814))
([6d3f39f](headroomlabs-ai@6d3f39f))
* Add support for Mistral Vibe CLI
([headroomlabs-ai#935](headroomlabs-ai#935))
([0932b8b](headroomlabs-ai@0932b8b))
* attribute reread waste to over-compression via marker check
([headroomlabs-ai#901](headroomlabs-ai#901))
([f928576](headroomlabs-ai@f928576))
* **bedrock:** cross-region + Converse compression; bundle proxy binary
in images ([headroomlabs-ai#999](headroomlabs-ai#999))
([0dc2e1c](headroomlabs-ai@0dc2e1c))
* **dashboard:** surface compression-vs-cache net impact in Prefix Cache
panel ([headroomlabs-ai#913](headroomlabs-ai#913))
([2a4d300](headroomlabs-ai@2a4d300))
* **evals:** adversarial-input robustness grid for compressors
([headroomlabs-ai#918](headroomlabs-ai#918))
([5939004](headroomlabs-ai@5939004))
* **parser:** detect re-issued identical tool calls as reread waste
([headroomlabs-ai#909](headroomlabs-ai#909))
([7d4ae86](headroomlabs-ai@7d4ae86))
* **policy:** batch deep edits through one cache-bust
([headroomlabs-ai#856](headroomlabs-ai#856) P3a)
([headroomlabs-ai#1015](headroomlabs-ai#1015))
([c2e52fe](headroomlabs-ai@c2e52fe))
* **policy:** consume net-cost mutation gate in ContentRouter
([headroomlabs-ai#856](headroomlabs-ai#856) P2)
([headroomlabs-ai#905](headroomlabs-ai#905))
([553ade4](headroomlabs-ai@553ade4))
* **proxy:** compress AWS Bedrock InvokeModel requests via configurable
upstream ([headroomlabs-ai#720](headroomlabs-ai#720))
([7edb27a](headroomlabs-ai@7edb27a))


### Bug Fixes

* **anthropic:** strip styled Claude model ids
([headroomlabs-ai#651](headroomlabs-ai#651))
([0c5c89d](headroomlabs-ai@0c5c89d))
* **anyllm:** forward openai api_base/api_key to the any-llm backend
([headroomlabs-ai#942](headroomlabs-ai#942))
([headroomlabs-ai#954](headroomlabs-ai#954))
([a7ee8a6](headroomlabs-ai@a7ee8a6))
* **cache:** guard None exemplar embeddings in dynamic detector
([headroomlabs-ai#950](headroomlabs-ai#950))
([1ec9320](headroomlabs-ai@1ec9320))
* **cache:** name the missing piece in semantic detector guard
([headroomlabs-ai#1018](headroomlabs-ai#1018))
([3b0bcee](headroomlabs-ai@3b0bcee))
* **ci:** check out repo in PR Governance label job
([headroomlabs-ai#1021](headroomlabs-ai#1021))
([4558bc2](headroomlabs-ai@4558bc2))
* **ci:** make PR governance advisory
([headroomlabs-ai#1047](headroomlabs-ai#1047))
([74dff94](headroomlabs-ai@74dff94))
* **codex:** compute waste signals on the OpenAI Responses path
([headroomlabs-ai#898](headroomlabs-ai#898))
([b9e2761](headroomlabs-ai@b9e2761))
* **codex:** poll /wham/usage for subscription limits (handshake no
longer sends x-codex-* headers)
([headroomlabs-ai#924](headroomlabs-ai#924))
([8c00f71](headroomlabs-ai@8c00f71))
* **codex:** PR health label check state
([headroomlabs-ai#986](headroomlabs-ai#986))
([99c874d](headroomlabs-ai@99c874d))
* **codex:** retag thread providers so history menu stays whole across
the proxy boundary
([headroomlabs-ai#1034](headroomlabs-ai#1034))
([74ae781](headroomlabs-ai@74ae781))
* **codex:** write canonical hooks feature flag and migrate deprecated
codex_hooks ([headroomlabs-ai#743](headroomlabs-ai#743))
([dff6a19](headroomlabs-ai@dff6a19))
* **compression:** convert tree-sitter byte offsets to char offsets
([headroomlabs-ai#892](headroomlabs-ai#892))
([b1f700f](headroomlabs-ai@b1f700f))
* **compression:** correct JSON array item counting and entropy gate
([headroomlabs-ai#887](headroomlabs-ai#887))
([d6f0f0f](headroomlabs-ai@d6f0f0f))
* **compression:** keep container bodies compressible in code handler
([headroomlabs-ai#890](headroomlabs-ai#890))
([16ed73b](headroomlabs-ai@16ed73b))
* **compression:** measure short-value threshold on payload, not token
([headroomlabs-ai#889](headroomlabs-ai#889))
([65b0e8c](headroomlabs-ai@65b0e8c))
* **compression:** use thread-local tree-sitter parsers in code handler
([headroomlabs-ai#893](headroomlabs-ai#893))
([6cdb846](headroomlabs-ai@6cdb846))
* **gemini:** surface functionResponse payloads to waste-signal
detection ([headroomlabs-ai#897](headroomlabs-ai#897))
([9b0c840](headroomlabs-ai@9b0c840))
* **learn:** decode directory names with spaces in Windows project paths
([headroomlabs-ai#997](headroomlabs-ai#997))
([headroomlabs-ai#1027](headroomlabs-ai#1027))
([2d3701b](headroomlabs-ai@2d3701b))
* **learn:** scan subagent and workflow transcripts
([headroomlabs-ai#1045](headroomlabs-ai#1045))
([0ddd4ed](headroomlabs-ai@0ddd4ed))
* **openclaw:** declare headroom_retrieve tool contract
([headroomlabs-ai#947](headroomlabs-ai#947))
([7c8c909](headroomlabs-ai@7c8c909))
* **policy:** correct warm-cache penalty in net_mutation_gain to (S +
dT) ([headroomlabs-ai#903](headroomlabs-ai#903))
([0632eba](headroomlabs-ai@0632eba))
* **proxy:** add native Bedrock converse-stream route
([headroomlabs-ai#917](headroomlabs-ai#917))
([b08ec15](headroomlabs-ai@b08ec15))
* **proxy:** keep codex image-generation WS turns alive through the
relay ([headroomlabs-ai#1000](headroomlabs-ai#1000))
([7dbbb40](headroomlabs-ai@7dbbb40))
* **proxy:** make budget enforcement actually work
([headroomlabs-ai#885](headroomlabs-ai#885))
([a14ab45](headroomlabs-ai@a14ab45))
* **proxy:** read RTK gain stats globally by default
([headroomlabs-ai#957](headroomlabs-ai#957))
([b70fccb](headroomlabs-ai@b70fccb))
* route v1internal code assist requests to cloudcode-pa.googleapis…
([headroomlabs-ai#821](headroomlabs-ai#821))
([e20f16b](headroomlabs-ai@e20f16b))
* **serena:** stop the Serena dashboard popup and make --no-serena
actually disable Serena
([headroomlabs-ai#1003](headroomlabs-ai#1003))
([919379a](headroomlabs-ai@919379a))
* support Copilot Business subscription auth
([headroomlabs-ai#641](headroomlabs-ai#641))
([0b4a4bd](headroomlabs-ai@0b4a4bd))
* wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy
entrypoint ([headroomlabs-ai#943](headroomlabs-ai#943))
([9b7b436](headroomlabs-ai@9b7b436))
* **wrap:** avoid duplicate top-level keys when injecting codex provider
([headroomlabs-ai#884](headroomlabs-ai#884))
([dd22cfd](headroomlabs-ai@dd22cfd))


### Code Refactoring

* DRY cache logic, add thread safety, fix Bash exclusion
([headroomlabs-ai#704](headroomlabs-ai#704))
([e36fccd](headroomlabs-ai@e36fccd))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: ready for review Pull request body is complete and the author marked it ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants