diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index ffac5e20fa..de6305a6fa 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -350,9 +350,8 @@ def _replay_reasoning_items( """Replay persisted encrypted reasoning/compaction items for one assistant turn. Skips duplicate ids, ``compaction`` checkpoints unless THIS request carries ``context_management`` (else a persisted checkpoint erases pre-checkpoint history on a model that cannot decrypt it), and items stamped by - another issuer or model (HTTP 400). Endpoint-stamped legacy items without model provenance drop - (fail closed) when the current model is known; fully unstamped items pass. ``id`` (store=False - lookups 404) and the Hermes provenance fields are stripped.""" + another issuer or model (HTTP 400). Items without a model stamp (legacy or unstamped) replay on a + matching issuer. ``id`` (store=False lookups 404) and the Hermes provenance fields are stripped.""" global _CROSS_ISSUER_WARN_EMITTED replayed: List[Dict[str, Any]] = [] for ri in _as_list(msg.get("codex_reasoning_items")): @@ -364,9 +363,11 @@ def _replay_reasoning_items( item_issuer = ri.get("_issuer_kind") item_model = ri.get("_issuer_model") foreign_issuer = current_issuer_kind is not None and item_issuer is not None and item_issuer != current_issuer_kind - foreign_model = current_issuer_model is not None and ( - (item_model is not None and item_model != current_issuer_model) - or (item_model is None and item_issuer is not None) + # No model stamp → trust the endpoint stamp. Native compaction checkpoints and reasoning persisted + # before model stamping exist carry none; dropping them would erase every existing session's + # context once. A wrong guess is caught by the invalid_encrypted_content 400 classifier. + foreign_model = ( + current_issuer_model is not None and item_model is not None and item_model != current_issuer_model ) if foreign_issuer or foreign_model: if not _CROSS_ISSUER_WARN_EMITTED: diff --git a/tests/agent/test_codex_responses_adapter.py b/tests/agent/test_codex_responses_adapter.py index 61894dc517..ec382875af 100644 --- a/tests/agent/test_codex_responses_adapter.py +++ b/tests/agent/test_codex_responses_adapter.py @@ -593,16 +593,21 @@ def test_reasoning_replay_requires_matching_issuer_model_on_same_endpoint(): assert not any(i.get("type") == "reasoning" for i in other) -def test_reasoning_replay_drops_endpoint_stamped_legacy_item_without_model(): - # Fail closed: an endpoint-only stamp cannot prove the minting model once the request model is known. +def test_legacy_endpoint_stamped_item_without_model_replays_on_same_issuer(): + # WHY: native compaction checkpoints and reasoning persisted before model stamping carry only the + # endpoint stamp; dropping them would erase every existing session's context once after upgrade. issuer = "other:https://responses.example.com/v1" legacy = {"type": "reasoning", "encrypted_content": "legacy-blob", "_issuer_kind": issuer} items = _chat_messages_to_responses_input( _reasoning_history(legacy), current_issuer_kind=issuer, current_issuer_model="gpt-5.6-sol" ) - assert not any(i.get("type") == "reasoning" for i in items) - # Ordinary assistant text stays replayable; only the sealed blob is withheld. - assert any(i.get("role") == "assistant" for i in items) + replayed = [i for i in items if i.get("type") == "reasoning"] + assert [i["encrypted_content"] for i in replayed] == ["legacy-blob"] + # A different endpoint stamp still drops. + foreign = _chat_messages_to_responses_input( + _reasoning_history(legacy), current_issuer_kind="codex_backend", current_issuer_model="gpt-5.6-sol" + ) + assert not any(i.get("type") == "reasoning" for i in foreign) def test_preflight_codex_api_kwargs_drops_oversized_message_id_end_to_end():