835a913ffd
Closes #75364. `_compress_context_via_codex_app_server` returns the transcript unchanged when the codex thread reports `interrupted` or `error`. The session is therefore still above threshold, and nothing records that the attempt failed — so the next turn retries immediately, and keeps retrying for as long as the condition persists. Every other compression path arms the shared failure cooldown, records an ineffective-compression strike, or both. This path records neither: * `_hygiene_compression_failure_cooldowns` is set only on `asyncio.TimeoutError`, or behind `_last_compress_aborted`, which is assigned exclusively in `context_compressor.py` on the Hermes summarizer path. * `compression_ineffective_count` lives in `ContextCompressor`, and this path returns before any compressor bookkeeping runs. `compress_context` already documents the rule this path was missing — "Every automatic entrypoint must honor compressor-owned cooldown and breaker state" — but the codex branch dispatches above that block and returns from inside it. `result.interrupted` needs no unusual configuration to occur: an ordinary user message arriving mid-compaction sets it (see `codex_app_server_session.py`, which produces the "compact turn interrupted" string). Observed in production on a Discord gateway session at ~315k tokens against a 258k window, where compaction was attempted on essentially every turn for ~70 minutes; the session's `compression_ineffective_count` was still 0 afterwards. This reuses the existing cooldown rather than adding a new mechanism: * arm `_record_compression_failure_cooldown` with the existing `_SUMMARY_FAILURE_COOLDOWN_SECONDS` when compaction returns interrupted/error; * honor an active cooldown on entry, matching the Hermes path. `force=True` bypasses both, so an explicit /compress is never braked by a failure it did not cause, and a successful compaction arms nothing.