fix(transforms): bound native content detection with a Windows watchdog (#575)#1563
Conversation
…og (headroomlabs-ai#575) On Windows the first call into `headroom._core.detect_content_type` can park forever in an ort/`Once` init (`WaitOnAddress`) at 0% CPU. A wedged native call cannot be cancelled from Python, so it deadlocks the caller and — in the proxy — permanently consumes a compression-executor worker, eventually saturating the pool and stalling every request for the full compression timeout. The Rust backend is already off by default on Windows (`_resolve_detect_backend` returns "python" there), but an explicit `HEADROOM_DETECT_BACKEND=rust`, or any regression of that default, re-exposes the hang with no escape hatch. Add a Windows-only watchdog around the native detect call: run it on a daemon thread and bound the caller's wait (`HEADROOM_DETECT_TIMEOUT_SECS`, default 5s). On timeout it raises `TimeoutError`, which the existing handler degrades to the pure-Python regex detector — so detection always returns instead of deadlocking. The native call releases the GIL while parked, so the watchdog thread runs. Other platforms keep the direct call with no per-call thread overhead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR governanceThis PR follows the template and is marked ready for human review. |
JerrettDavis
left a comment
There was a problem hiding this comment.
The Windows watchdog is the right mitigation for the immediate deadlock, but it needs one more operational guard before I’d be comfortable merging it. If the native detector hangs once, subsequent calls will still try the same native path, wait the full watchdog budget, and leave another daemon thread blocked in the native call. Under proxy traffic that turns the fix into repeated 5s latency plus unbounded stuck-thread growth. Please add a process-level circuit breaker after the first watchdog timeout (for example mark native detection unhealthy and route future detections directly to the Python fallback), and cover that with a test that two Windows detections after a simulated hang only create/enter the native detector once.
…eout (headroomlabs-ai#575) The Windows watchdog frees the caller but not the native path: if the native detector hangs once, every later call still enters it, waits the full watchdog budget (5s each), and strands another daemon thread in the wedged native call. Under proxy traffic that is repeated latency plus unbounded stuck-thread growth. Add a process-level circuit breaker: the first watchdog TimeoutError marks native detection unhealthy, and all later detections route straight to the pure-Python fallback without re-entering native. Only TimeoutError trips it (the only thing the watchdog raises); panics/bad tags keep their existing transient degrade since they are fast failures, not stuck threads. Test: two Windows detections after a simulated hang enter the native detector exactly once. Added an autouse fixture resetting the module-level detect flags so the process-wide breaker does not leak across tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JerrettDavis
left a comment
There was a problem hiding this comment.
Reviewed the latest circuit-breaker update. This addresses the blocker I raised.
The watchdog is now Windows-scoped, the first timeout disables the native detector process-wide, and subsequent calls go straight to the pure-Python detector instead of spending the full timeout and leaving another stuck daemon thread behind. The tests cover env parsing, fast native success, native exception propagation, timeout degrade, and the circuit breaker skip path.
I attempted a local targeted run of python -m pytest tests/test_transforms_content_router.py -q, but this review worktree cannot import headroom._core locally. The full GitHub CI matrix is green for this PR.
🤖 I have created a release *beep* *boop* --- <details><summary>0.29.0</summary> ## [0.29.0](v0.28.0...v0.29.0) (2026-07-03) ### Features * **proxy:** add --lossless no-CCR mode with format-native compaction ([#1721](#1721)) ([c75ebde](c75ebde)) * **stats:** surface Codex WS compression counters in /stats summary ([#1680](#1680)) ([2fe19c3](2fe19c3)) * **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) ([#1726](#1726)) ([eea667a](eea667a)) ### Bug Fixes * **bedrock:** fail fast when session-token auth lacks botocore ([#1553](#1553)) ([54cfa36](54cfa36)) * **bedrock:** route ARNs via converse, named AWS profiles, and au. re… ([#1456](#1456)) ([7d87aa2](7d87aa2)) * **ccr:** honor workspace dir for sqlite store ([#1564](#1564)) ([96e1dfe](96e1dfe)) * **claude:** surface Remote Control proxy incompatibility ([#1610](#1610)) ([4bf7f92](4bf7f92)) * **cli:** stop advertising unwired compression tuning env vars in banner ([#1634](#1634)) ([d5bf98d](d5bf98d)) * **codex:** avoid duplicate headroom provider config ([#1431](#1431)) ([ddd4adf](ddd4adf)) * **compression:** reject lossy unmarked tool output in unit router path ([#1479](#1479)) ([de24cd5](de24cd5)) * **cortex-code:** migrate to current Cortex REST API endpoints + add e2e benchmarks ([#1474](#1474)) ([f00ace6](f00ace6)) * **dashboard:** align token savings headline denominator ([#1653](#1653)) ([646e705](646e705)) * **dashboard:** derive per-project setup URL from live origin ([#1511](#1511)) ([e035aef](e035aef)) * **detection:** contain unidiff panic on orphaned +++ target line ([#1548](#1548)) ([e386c09](e386c09)) * **evals:** CJK-aware F1 tokenization + token estimation ([#1527](#1527)) ([99a8540](99a8540)) * **install:** close parent log fd in start_detached_agent ([#1576](#1576)) ([816cb85](816cb85)) * **install:** use Windows-safe PID liveness probe in runtime_status ([#1544](#1544)) ([#1560](#1560)) ([6b227b9](6b227b9)) * **learn:** aggregate verbosity baselines across projects instead of overwriting ([#1288](#1288)) ([27a5468](27a5468)) * **mcp:** show lifetime totals and label rolling session scope in headroom_stats ([#1428](#1428)) ([1c0e152](1c0e152)) * **memory:** cap local embedder CPU thread oversubscription ([#198](#198)) ([#1559](#1559)) ([b84afbf](b84afbf)) * **memory:** singleflight LocalBackend init to stop cold-start races ([#1691](#1691)) ([bec47a1](bec47a1)) * **openclaw:** detect uv-installed headroom binary in ~/.local/bin ([#1459](#1459)) ([adaeb88](adaeb88)) * **opencode:** preserve custom OpenAI gateway paths ([#1596](#1596)) ([c19347c](c19347c)) * **opencode:** route native providers + load transport plugin, fix Serena context ([#1573](#1573)) ([ad0034f](ad0034f)) * preserve anthropic passthrough tool order ([#1427](#1427)) ([a932247](a932247)) * **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat) ([#1672](#1672)) ([8cddf9b](8cddf9b)) * **proxy:** expose persistent savings metrics ([#1647](#1647)) ([5fe4e7b](5fe4e7b)) * **proxy:** fail open when kompress saturation would exhaust pre-upstream budget ([#1430](#1430)) ([15ac650](15ac650)) * **proxy:** handle streaming CCR retrieval ([#1451](#1451)) ([d337e3b](d337e3b)) * **proxy:** include system/tools/sampling in cache key ([#1473](#1473)) ([312129a](312129a)) * **proxy:** preserve Responses passthrough bytes ([#1598](#1598)) ([2a34a82](2a34a82)) * **proxy:** strip Codex lite header on the HTTP /responses path ([#1663](#1663)) ([9fbd47b](9fbd47b)) * **proxy:** wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS ([#1632](#1632)) ([814ffa3](814ffa3)) * **savings:** count cache-read tokens in input cost estimate ([#1429](#1429)) ([72ade37](72ade37)) * skip Magika backend on x86 CPUs without AVX2 ([#1162](#1162)) ([64783d8](64783d8)) * **transforms/content-router:** route grep/log output away from HTML extractor ([#1719](#1719)) ([0d18ef2](0d18ef2)) * **transforms:** bound native content detection with a Windows watchdog ([#575](#575)) ([#1563](#1563)) ([95abca3](95abca3)) * Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL ([#1393](#1393)) ([cff7247](cff7247)) * **wrap:** detach the shared proxy on Windows so it survives an ungraceful agent close ([#1464](#1464)) ([6cba441](6cba441)) * **wrap:** preserve custom Vertex base URL ([#1477](#1477)) ([75427bb](75427bb)) * **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap ([#1604](#1604)) ([c9d717c](c9d717c)) </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>
…og (headroomlabs-ai#575) (headroomlabs-ai#1563) ## Description On Windows, the first call into the native `headroom._core.detect_content_type` can park forever in an ort/`Once` initialization (`WaitOnAddress`) at 0% CPU. A wedged native call cannot be cancelled from Python, so it deadlocks the caller. In the proxy it is worse: each affected request permanently consumes a compression-executor worker, eventually saturating the pool (`running == max_workers`, `leaked_threads_total == 0` because the worker never finishes) and stalling every subsequent request for the full `COMPRESSION_TIMEOUT_SECONDS` before passthrough. The Rust backend is already off by default on Windows — `_resolve_detect_backend()` returns `"python"` there — but an explicit `HEADROOM_DETECT_BACKEND=rust`, or any future regression of that default, re-exposes the hang with no escape hatch. This implements the issue's third ask: a timeout/watchdog so a hung native init degrades gracefully instead of deadlocking the agent / MCP server / proxy. Closes headroomlabs-ai#575 ## 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 - Added a Windows-only watchdog around the native detect call in `transforms/content_router.py`. `_rust_detect_watchdogged()` runs `detect_content_type` on a daemon thread and bounds the caller's wait; on timeout it raises `TimeoutError`, which the existing `except BaseException` handler degrades to the pure-Python regex detector. Detection therefore always returns instead of deadlocking (and, in the proxy, instead of permanently consuming a compression-executor worker). - Added `_detect_timeout_secs()` reading `HEADROOM_DETECT_TIMEOUT_SECS` (default 5s; blank / non-numeric / non-positive values fall back to the default). - Gated the watchdog to `sys.platform == "win32"` — the only platform where the hang is observed. Other platforms keep the direct native call with no per-call thread overhead (the trusted hot path is unchanged). - Added regression tests for the watchdog, env parsing, error relay, the Windows degrade-on-hang path, and the Windows happy path. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ ruff check headroom/transforms/content_router.py tests/test_transforms_content_router.py All checks passed! $ ruff format --check headroom/transforms/content_router.py tests/test_transforms_content_router.py 2 files already formatted $ mypy headroom --ignore-missing-imports (exit 0) $ pytest tests/test_transforms_content_router.py -q 33 passed ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, ruff 0.15.17 / mypy 1.20.2 / pytest 9.1.0, `headroom._core` built locally, branch `fix/575-native-detect-watchdog`. - Exact command / steps: ran the four checks above. `test_detect_content_watchdog_degrades_on_windows_hang` forces `HEADROOM_DETECT_BACKEND=rust`, patches `sys.platform` to `"win32"`, sets `HEADROOM_DETECT_TIMEOUT_SECS=0.1`, and injects a native `detect_content_type` that blocks on an `Event` (simulating the `WaitOnAddress` park, GIL released) — then asserts detection still returns. The companion tests cover env parsing, error relay through the watchdog, and the fast-native Windows path. - Observed result: with a hung native detector, `_detect_content('[{"id": 1}]')` returns `ContentType.JSON_ARRAY` (the pure-Python degrade path) within the 0.1s budget instead of hanging; with a fast native detector on Windows it returns the native result unchanged; non-Windows behavior (direct call) is untouched and the existing rust-delegation test still passes. All 33 tests in the file pass; ruff / format / mypy clean. - Not tested: the live `from headroom._core import detect_content_type; detect_content_type("hello world")` deadlock on an affected Windows 11 24H2 machine was not reproduced end to end (it requires the specific System32 ONNX Runtime build). The fix is instead covered by the deterministic hung-detector injection test, which exercises the exact degrade path the watchdog adds. This PR does not attempt the Rust-side fix for the underlying first-call init deadlock (asks headroomlabs-ai#1) — it is the Python-side watchdog (ask headroomlabs-ai#3); the existing `HEADROOM_DETECT_BACKEND` flag already covers ask headroomlabs-ai#2. ## 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 ## Additional Notes - The watchdog cannot cancel a wedged native call (no portable way to kill a thread blocked in C). It frees the *caller* and leaves the stuck daemon thread to die with the process; this is marked with a `ponytail:` comment naming the upgrade path (the Rust-side non-blocking first-call init). For the saturation scenario this is still a strict improvement: callers no longer block indefinitely, so the executor drains instead of wedging permanently. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- <details><summary>0.29.0</summary> ## [0.29.0](headroomlabs-ai/headroom@v0.28.0...v0.29.0) (2026-07-03) ### Features * **proxy:** add --lossless no-CCR mode with format-native compaction ([headroomlabs-ai#1721](headroomlabs-ai#1721)) ([c75ebde](headroomlabs-ai@c75ebde)) * **stats:** surface Codex WS compression counters in /stats summary ([headroomlabs-ai#1680](headroomlabs-ai#1680)) ([2fe19c3](headroomlabs-ai@2fe19c3)) * **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) ([headroomlabs-ai#1726](headroomlabs-ai#1726)) ([eea667a](headroomlabs-ai@eea667a)) ### Bug Fixes * **bedrock:** fail fast when session-token auth lacks botocore ([headroomlabs-ai#1553](headroomlabs-ai#1553)) ([54cfa36](headroomlabs-ai@54cfa36)) * **bedrock:** route ARNs via converse, named AWS profiles, and au. re… ([headroomlabs-ai#1456](headroomlabs-ai#1456)) ([7d87aa2](headroomlabs-ai@7d87aa2)) * **ccr:** honor workspace dir for sqlite store ([headroomlabs-ai#1564](headroomlabs-ai#1564)) ([96e1dfe](headroomlabs-ai@96e1dfe)) * **claude:** surface Remote Control proxy incompatibility ([headroomlabs-ai#1610](headroomlabs-ai#1610)) ([4bf7f92](headroomlabs-ai@4bf7f92)) * **cli:** stop advertising unwired compression tuning env vars in banner ([headroomlabs-ai#1634](headroomlabs-ai#1634)) ([d5bf98d](headroomlabs-ai@d5bf98d)) * **codex:** avoid duplicate headroom provider config ([headroomlabs-ai#1431](headroomlabs-ai#1431)) ([ddd4adf](headroomlabs-ai@ddd4adf)) * **compression:** reject lossy unmarked tool output in unit router path ([headroomlabs-ai#1479](headroomlabs-ai#1479)) ([de24cd5](headroomlabs-ai@de24cd5)) * **cortex-code:** migrate to current Cortex REST API endpoints + add e2e benchmarks ([headroomlabs-ai#1474](headroomlabs-ai#1474)) ([f00ace6](headroomlabs-ai@f00ace6)) * **dashboard:** align token savings headline denominator ([headroomlabs-ai#1653](headroomlabs-ai#1653)) ([646e705](headroomlabs-ai@646e705)) * **dashboard:** derive per-project setup URL from live origin ([headroomlabs-ai#1511](headroomlabs-ai#1511)) ([e035aef](headroomlabs-ai@e035aef)) * **detection:** contain unidiff panic on orphaned +++ target line ([headroomlabs-ai#1548](headroomlabs-ai#1548)) ([e386c09](headroomlabs-ai@e386c09)) * **evals:** CJK-aware F1 tokenization + token estimation ([headroomlabs-ai#1527](headroomlabs-ai#1527)) ([99a8540](headroomlabs-ai@99a8540)) * **install:** close parent log fd in start_detached_agent ([headroomlabs-ai#1576](headroomlabs-ai#1576)) ([816cb85](headroomlabs-ai@816cb85)) * **install:** use Windows-safe PID liveness probe in runtime_status ([headroomlabs-ai#1544](headroomlabs-ai#1544)) ([headroomlabs-ai#1560](headroomlabs-ai#1560)) ([6b227b9](headroomlabs-ai@6b227b9)) * **learn:** aggregate verbosity baselines across projects instead of overwriting ([headroomlabs-ai#1288](headroomlabs-ai#1288)) ([27a5468](headroomlabs-ai@27a5468)) * **mcp:** show lifetime totals and label rolling session scope in headroom_stats ([headroomlabs-ai#1428](headroomlabs-ai#1428)) ([1c0e152](headroomlabs-ai@1c0e152)) * **memory:** cap local embedder CPU thread oversubscription ([headroomlabs-ai#198](headroomlabs-ai#198)) ([headroomlabs-ai#1559](headroomlabs-ai#1559)) ([b84afbf](headroomlabs-ai@b84afbf)) * **memory:** singleflight LocalBackend init to stop cold-start races ([headroomlabs-ai#1691](headroomlabs-ai#1691)) ([bec47a1](headroomlabs-ai@bec47a1)) * **openclaw:** detect uv-installed headroom binary in ~/.local/bin ([headroomlabs-ai#1459](headroomlabs-ai#1459)) ([adaeb88](headroomlabs-ai@adaeb88)) * **opencode:** preserve custom OpenAI gateway paths ([headroomlabs-ai#1596](headroomlabs-ai#1596)) ([c19347c](headroomlabs-ai@c19347c)) * **opencode:** route native providers + load transport plugin, fix Serena context ([headroomlabs-ai#1573](headroomlabs-ai#1573)) ([ad0034f](headroomlabs-ai@ad0034f)) * preserve anthropic passthrough tool order ([headroomlabs-ai#1427](headroomlabs-ai#1427)) ([a932247](headroomlabs-ai@a932247)) * **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat) ([headroomlabs-ai#1672](headroomlabs-ai#1672)) ([8cddf9b](headroomlabs-ai@8cddf9b)) * **proxy:** expose persistent savings metrics ([headroomlabs-ai#1647](headroomlabs-ai#1647)) ([5fe4e7b](headroomlabs-ai@5fe4e7b)) * **proxy:** fail open when kompress saturation would exhaust pre-upstream budget ([headroomlabs-ai#1430](headroomlabs-ai#1430)) ([15ac650](headroomlabs-ai@15ac650)) * **proxy:** handle streaming CCR retrieval ([headroomlabs-ai#1451](headroomlabs-ai#1451)) ([d337e3b](headroomlabs-ai@d337e3b)) * **proxy:** include system/tools/sampling in cache key ([headroomlabs-ai#1473](headroomlabs-ai#1473)) ([312129a](headroomlabs-ai@312129a)) * **proxy:** preserve Responses passthrough bytes ([headroomlabs-ai#1598](headroomlabs-ai#1598)) ([2a34a82](headroomlabs-ai@2a34a82)) * **proxy:** strip Codex lite header on the HTTP /responses path ([headroomlabs-ai#1663](headroomlabs-ai#1663)) ([9fbd47b](headroomlabs-ai@9fbd47b)) * **proxy:** wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS ([headroomlabs-ai#1632](headroomlabs-ai#1632)) ([814ffa3](headroomlabs-ai@814ffa3)) * **savings:** count cache-read tokens in input cost estimate ([headroomlabs-ai#1429](headroomlabs-ai#1429)) ([72ade37](headroomlabs-ai@72ade37)) * skip Magika backend on x86 CPUs without AVX2 ([headroomlabs-ai#1162](headroomlabs-ai#1162)) ([64783d8](headroomlabs-ai@64783d8)) * **transforms/content-router:** route grep/log output away from HTML extractor ([headroomlabs-ai#1719](headroomlabs-ai#1719)) ([0d18ef2](headroomlabs-ai@0d18ef2)) * **transforms:** bound native content detection with a Windows watchdog ([headroomlabs-ai#575](headroomlabs-ai#575)) ([headroomlabs-ai#1563](headroomlabs-ai#1563)) ([95abca3](headroomlabs-ai@95abca3)) * Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL ([headroomlabs-ai#1393](headroomlabs-ai#1393)) ([cff7247](headroomlabs-ai@cff7247)) * **wrap:** detach the shared proxy on Windows so it survives an ungraceful agent close ([headroomlabs-ai#1464](headroomlabs-ai#1464)) ([6cba441](headroomlabs-ai@6cba441)) * **wrap:** preserve custom Vertex base URL ([headroomlabs-ai#1477](headroomlabs-ai#1477)) ([75427bb](headroomlabs-ai@75427bb)) * **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap ([headroomlabs-ai#1604](headroomlabs-ai#1604)) ([c9d717c](headroomlabs-ai@c9d717c)) </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>
Description
On Windows, the first call into the native
headroom._core.detect_content_typecan park forever in an ort/Onceinitialization (WaitOnAddress) at 0% CPU. A wedged native call cannot be cancelled from Python, so it deadlocks the caller. In the proxy it is worse: each affected request permanently consumes a compression-executor worker, eventually saturating the pool (running == max_workers,leaked_threads_total == 0because the worker never finishes) and stalling every subsequent request for the fullCOMPRESSION_TIMEOUT_SECONDSbefore passthrough.The Rust backend is already off by default on Windows —
_resolve_detect_backend()returns"python"there — but an explicitHEADROOM_DETECT_BACKEND=rust, or any future regression of that default, re-exposes the hang with no escape hatch. This implements the issue's third ask: a timeout/watchdog so a hung native init degrades gracefully instead of deadlocking the agent / MCP server / proxy.Closes #575
Type of Change
Changes Made
transforms/content_router.py._rust_detect_watchdogged()runsdetect_content_typeon a daemon thread and bounds the caller's wait; on timeout it raisesTimeoutError, which the existingexcept BaseExceptionhandler degrades to the pure-Python regex detector. Detection therefore always returns instead of deadlocking (and, in the proxy, instead of permanently consuming a compression-executor worker)._detect_timeout_secs()readingHEADROOM_DETECT_TIMEOUT_SECS(default 5s; blank / non-numeric / non-positive values fall back to the default).sys.platform == "win32"— the only platform where the hang is observed. Other platforms keep the direct native call with no per-call thread overhead (the trusted hot path is unchanged).Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
headroom._corebuilt locally, branchfix/575-native-detect-watchdog.test_detect_content_watchdog_degrades_on_windows_hangforcesHEADROOM_DETECT_BACKEND=rust, patchessys.platformto"win32", setsHEADROOM_DETECT_TIMEOUT_SECS=0.1, and injects a nativedetect_content_typethat blocks on anEvent(simulating theWaitOnAddresspark, GIL released) — then asserts detection still returns. The companion tests cover env parsing, error relay through the watchdog, and the fast-native Windows path._detect_content('[{"id": 1}]')returnsContentType.JSON_ARRAY(the pure-Python degrade path) within the 0.1s budget instead of hanging; with a fast native detector on Windows it returns the native result unchanged; non-Windows behavior (direct call) is untouched and the existing rust-delegation test still passes. All 33 tests in the file pass; ruff / format / mypy clean.from headroom._core import detect_content_type; detect_content_type("hello world")deadlock on an affected Windows 11 24H2 machine was not reproduced end to end (it requires the specific System32 ONNX Runtime build). The fix is instead covered by the deterministic hung-detector injection test, which exercises the exact degrade path the watchdog adds. This PR does not attempt the Rust-side fix for the underlying first-call init deadlock (asks Add Claude Opus 4.5 and Claude 4 model family to context limits #1) — it is the Python-side watchdog (ask Fix ZlibError by removing compression headers after httpx decompression #3); the existingHEADROOM_DETECT_BACKENDflag already covers ask [BUG] Decompression error: ZlibError #2.Review Readiness
Checklist
Additional Notes
ponytail:comment naming the upgrade path (the Rust-side non-blocking first-call init). For the saturation scenario this is still a strict improvement: callers no longer block indefinitely, so the executor drains instead of wedging permanently.