fix(codex): canonicalise persisted issuer stamps via the route-identity owner

`_classify_responses_issuer` reimplemented endpoint canonicalisation with
its own urlsplit/urlunsplit pass. The repo already owns that logic in
`hermes_cli/route_identity.py::normalize_route_base_url` (stdlib-only,
used by agent/backend_identity.py), so delegate to it.

Reasoning items persisted before canonicalisation were stamped with the
raw `other:<agent.base_url>` (trailing slash, host case). Comparing them
verbatim against the now-canonical `current_issuer_kind` marked them
foreign and dropped them on the very endpoint that minted them. Run the
persisted stamp through the same canonicaliser (`_canonical_issuer_kind`,
non-`other:` kinds untouched) before comparing.

Also fixes the `_chat_messages_to_responses_input` docstring, which still
stated the pre-stack rule that legacy endpoint-stamped items drop when the
current model is known; the stack replays them on a matching issuer.
This commit is contained in:
kshitijk4poor
2026-09-15 01:04:43 +05:30
committed by kshitij
parent 426ddfeabc
commit 00f0d92be0
2 changed files with 29 additions and 8 deletions

View File

@@ -11,10 +11,10 @@ import unicodedata
import uuid
from types import SimpleNamespace
from typing import Any, Callable, Dict, Iterator, List, NamedTuple, Optional, TypeGuard
from urllib.parse import urlsplit, urlunsplit
from agent.message_sanitization import deterministic_call_id
from agent.prompt_builder import DEFAULT_AGENT_IDENTITY
from hermes_cli.route_identity import normalize_route_base_url
logger = logging.getLogger(__name__)
@@ -33,8 +33,15 @@ def _classify_responses_issuer(
# The openai SDK appends a trailing slash to ``client.base_url`` and hosts are case-insensitive, so the
# aux adapter and the main transport must canonicalise the same endpoint to one kind or aux calls drop
# every main-minted blob.
parts = urlsplit(str(base_url).strip().rstrip("/"))
return f"other:{urlunsplit((parts.scheme.lower(), parts.netloc.lower(), parts.path, parts.query, parts.fragment))}"
return f"other:{normalize_route_base_url(str(base_url).strip())}"
def _canonical_issuer_kind(kind: Any) -> Any:
"""Canonicalise a persisted ``other:<url>`` issuer stamp. Items stamped before canonicalisation carry the raw
``agent.base_url`` (trailing slash / host case) and must still replay on the same endpoint."""
if isinstance(kind, str) and kind.startswith("other:"):
return _classify_responses_issuer(base_url=kind[len("other:"):])
return kind
# Per-process throttle for the cross-issuer skip warning.
@@ -367,12 +374,12 @@ def _replay_reasoning_items(
item_id = ri.get("id")
if (item_id and item_id in seen_item_ids) or (ri.get("type") == "compaction" and not native_compaction_eligible):
continue
item_issuer = ri.get("_issuer_kind")
item_issuer = _canonical_issuer_kind(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
# 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.
# before model stamping 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
)
@@ -460,8 +467,8 @@ def _chat_messages_to_responses_input(
``replay_encrypted_reasoning``: per-session kill switch, threaded False by
``AIAgent._disable_codex_reasoning_replay`` after an ``invalid_encrypted_content`` 400.
``is_github_responses``: drops ``id`` from replayed message items (Copilot 401s on stale ids).
``current_issuer_kind`` / ``current_issuer_model``: provenance guard; foreign-stamped items drop, as do
endpoint-stamped legacy items without model provenance when the current model is known.
``current_issuer_kind`` / ``current_issuer_model``: provenance guard; items stamped by another issuer or
model drop. Legacy items carrying only an endpoint stamp replay on a matching issuer.
``native_compaction_eligible``: THIS request carries ``context_management``; gates both replaying ``compaction``
checkpoints and ``prune_pre_checkpoint_items``. Checkpoints persist across model swaps / compression flips / resume,
so without the gate one checkpoint would erase pre-checkpoint history on a model that cannot decrypt it (lossless:

View File

@@ -620,6 +620,20 @@ def test_issuer_kind_is_canonical_across_trailing_slash_and_host_case():
assert _classify_responses_issuer(base_url="https://other/v1") != canonical
def test_legacy_raw_endpoint_stamp_replays_on_canonical_issuer():
# WHY: items persisted before issuer canonicalisation carry the raw ``agent.base_url`` (trailing slash,
# host case); they must still replay on the same endpoint instead of being dropped as foreign.
legacy = {"type": "reasoning", "encrypted_content": "legacy-blob", "_issuer_kind": "other:https://H/v1/"}
items = _chat_messages_to_responses_input(
_reasoning_history(legacy), current_issuer_kind="other:https://h/v1", current_issuer_model="gpt-5.6-sol"
)
assert [i["encrypted_content"] for i in items if i.get("type") == "reasoning"] == ["legacy-blob"]
foreign = _chat_messages_to_responses_input(
_reasoning_history(legacy), current_issuer_kind="other:https://other/v1", 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():
kwargs = _preflight_codex_api_kwargs(
{