Skip to content

fix(openai): recompress cached Responses units whose CCR entries expired - #4146

Open
stephan-nb wants to merge 3 commits into
headroomlabs-ai:mainfrom
stephan-nb:fix/responses-unit-cache-dangling-ccr
Open

stephan-nb wants to merge 3 commits into
headroomlabs-ai:mainfrom
stephan-nb:fix/responses-unit-cache-dangling-ccr

Conversation

@stephan-nb

@stephan-nb stephan-nb commented Oct 10, 2026 •

Copy link
Copy Markdown

Description

The Responses unit-result cache has no TTL, but the CCR entries behind the markers in its results expire after the CCR TTL and are written only when a unit is compressed. After expiry, a cache hit reuses a result whose marker no retrieval can redeem (Content not found). The result is silent lossy compression on long-running proxies.

_get_openai_responses_cached_unit now checks, on a hit, that every CCR hash in the cached compressed text still exists() in the compression store. Hashes come from the existing _CCR_HASH_RE, which matches <<ccr:...>>, Retrieve more: hash= and Retrieve original: hash=. If one is missing, the entry is evicted and the lookup returns None. The unit then goes through _compress_and_store, which writes the CCR entry again. Results without markers and an unreachable store keep the old behaviour.

Fixes #4145.

Type of Change

  • Bug fix

Changes Made

  • headroom/proxy/handlers/openai.py: _openai_responses_cached_ccr_markers_live() added; _get_openai_responses_cached_unit() evicts and misses when it returns False.
  • tests/test_openai_responses_compression_units.py: two tests.
    • ..._recompresses_when_ccr_entry_expired: the entry is deleted after the first request; the second request compresses again (2 router calls), produces the same output and restores the entry.
    • ..._reuses_result_while_ccr_entry_live: a live entry still gives a cache hit (1 router call).

Testing

  • pytest tests/test_openai_responses_compression_units.py: 46 passed (44 before plus the 2 new).
  • pytest tests/test_openai_codex_routing.py tests/test_openai_responses_compression_units.py: 87 passed.
  • Red check: with the handler change reverted, ..._recompresses_when_ccr_entry_expired fails (assert 1 == 2); the live-entry test passes either way.
  • ruff check and ruff format --check clean on both files.
  • Built with maturin from main at 976aa714 (Python 3.14, macOS arm64).

Real Behavior Proof

  • Environment: headroom-ai 0.39.1 proxy (cache mode), Codex CLI 0.161.0 over the ChatGPT subscription WebSocket route, macOS.
  • Exact command / steps: run the same cat of three prose files (38.7 KB) through Codex on the same model, more than 1800 s apart, without restarting the proxy. Then call headroom_retrieve on the shipped marker hash.
  • Observed result (unpatched): the second run receives the same marker hashes as the first, 0a5f6643447032a6ec5a2528 and 9498590711d246862a70f1cf. headroom_retrieve returns Content not found and ccr_entries has no row. A model with no cache entry recompresses and the rows reappear.
  • Not tested: the patched proxy end to end. The fix is covered by the unit tests above only.

Runtime Rollout Safety

  • Rollout-managed feature(s): none.
  • Minimum rollout channel: n/a.
  • Stable/default behavior changed: only on unit-cache hits whose CCR entry has expired. Those now recompress instead of shipping an unredeemable marker.
  • Kill switch / disable path: none added; revert the commit.
  • Unsafe override required: no.
  • Qualification impact: none expected; recompression cost on expired hits only.
  • Rollback path: revert.

Notes for review

  • The check runs outside the cache lock. Eviction re-checks identity (cache.get(key) is result), so a concurrent re-store is not dropped.
  • Only the Responses unit cache is touched. If other paths reuse compressed text across requests, the same question applies there; not audited.
  • Marker hashes are the store keys by design (store(..., explicit_hash=...) for producers with their own hash). If any producer emits a marker hash that is not its exact store key, this check would turn its hits into misses. That is a performance cost, not a correctness one.

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 (no user-facing behaviour or setting changed)
  • 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 did not edit CHANGELOG.md

Devin Review

@github-actions

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Check at least one verification item in Testing.
  • Paste real command output or artifact links in Testing → Test Output.
  • Fill in Real Behavior Proof → Observed result.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added the status: needs author action Pull request body or readiness checklist still needs author updates label Oct 10, 2026

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 2 potential issues.

Devin Review

Comment thread headroom/proxy/handlers/openai.py Outdated
Comment on lines +500 to +529
def test_openai_responses_unit_cache_recompresses_when_ccr_entry_expired():
# The unit cache has no TTL; the CCR entry behind its marker does. Reusing the
# cached result after the entry expired would ship a marker no retrieval can
# redeem, so the unit must be compressed (and its entry stored) again.
from headroom.cache.compression_store import (
CompressionStore,
clear_request_compression_store,
set_request_compression_store,
)

store = CompressionStore(max_entries=100, default_ttl=60)
set_request_compression_store(store)
try:
handler, calls = _marker_compressing_handler(store)
long_text = " ".join(f"word{i}" for i in range(180))

first = _compress_shell_output(handler, long_text, "req_expire_one")
hash_key = first.rsplit("hash=", 1)[1].rstrip("]")
assert store.exists(hash_key)

store._backend.delete(hash_key) # what TTL expiry does to the entry
assert not store.exists(hash_key)

second = _compress_shell_output(handler, long_text, "req_expire_two")

assert calls["count"] == 2
assert second == first
assert store.exists(hash_key)
finally:
clear_request_compression_store()

@devin-ai-integration devin-ai-integration Bot Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 After-fix runtime proof is missing

The PR's runtime evidence comes from the unpatched proxy; it explicitly says the patched proxy was not tested end to end. These mocked tests do not supply the after-fix evidence required for Real Behavior Proof.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Devin Review

Comment thread headroom/proxy/handlers/openai.py Outdated
Comment on lines +728 to +732
original = getattr(result, "original", "")
if isinstance(original, str):
hashes.difference_update(
match.group(1).lower() for match in _CCR_HASH_RE.finditer(original)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Expired generated marker survives cache hits

When a unit generates a marker already present in its input, hashes.difference_update excludes that generated hash. After its CCR entry expires, cached output keeps an unredeemable marker instead of recompressing.

Learn more

A unit can contain a CCR marker while also compressing marker-free text beside it. The marker-preserving compressor routes that text separately, and its newly stored entry can have the same hash as the inherited marker. Removing every hash found in original also removes that newly generated hash. After the entry expires, cache hits skip the liveness check for it and ship an unredeemable generated marker.

Example: A previous output carries marker H for text T. A later tool output contains T followed by that marker. Compressing T again stores H and emits another H; after H expires, this unit stays cached and sends both expired markers instead of restoring the entry.

Recommended fix: Track hashes newly stored during compression rather than inferring their origin from set subtraction. If that is unavailable, preserve liveness checks for hashes shared by the input and generated text, with a test covering the duplicate-hash case.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Responses unit-result cache reuses CCR markers after their entries expire

1 participant