fix(cache): source-qualify the peer identity and gate the declared bind
Both blockers from @andrexibiza's review of 28a2d7f0ee. 1. The generation lookup was not in the same identity domain as recovery. latest_conversation_boundary() selected on session_key alone, while _declared_conversation_session() is qualified by (source, session_key). X-Hermes-Session-Key accepts any authenticated caller-supplied string, so an API conversation may legally carry the same key as a Telegram row in one database -- a /new over there rotated this conversation's gwk_ generation while recovery correctly refused to cross the same line, moving the affinity identity out from under a physical identity that had not moved. The boundary read now takes (session_key, source), and the carrier is 'source|key|generation' rather than 'key|generation' -- keying on the string alone would also collapse two same-key conversations from different sources onto one routing key, since this value leaves the process verbatim as OpenRouter's sticky session_id and xAI's x-grok-conv-id. The source comes from the agent's own session row, falling back to the platform the row will be created with before it lands. 2. The declared key's stated lower precedence did not survive settlement. Both handlers let stored_session_id / an explicit body session_id win, then called _bind_declared_conversation() unconditionally. record_gateway_session_peer() does SET session_key = ? across compression ancestors, so a request carrying conversation A's chain plus header key B silently rebound A to B: A could no longer be recovered by its own key, and B recovered A's session. Recording is now gated on the declared key having actually selected or minted the session, on both paths. Behind that gate the bind itself refuses to overwrite a row already bound to a different key, so a future caller cannot reintroduce the same defect by opting in wrongly. test_declaration_outranks_the_lineage_root asserted the pre-qualification contract by comparing a DB-backed agent against a DB-less one; it now makes the stronger statement it was written for -- one declared conversation reached through two different physical ids on the same peer. Refs #96811 Found in review by @andrexibiza, whose analysis located each of these defects and specified what a correct fix had to prove. Co-Authored-By: Andrex Ibiza, MBA <andrexibiza@gmail.com>
This commit is contained in:
@@ -2356,6 +2356,21 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
if db is None:
|
||||
return
|
||||
try:
|
||||
# Defence in depth behind the callers' precedence gate: never
|
||||
# rewrite a row that already belongs to a different conversation.
|
||||
# record_gateway_session_peer does SET session_key = ?, so a
|
||||
# mistaken bind would strand the original conversation and hand its
|
||||
# session to this request's key.
|
||||
existing = db.get_session(sid) or {}
|
||||
current = str(existing.get("session_key") or "").strip()
|
||||
if current and current != key:
|
||||
logger.debug(
|
||||
"[%s] refusing to rebind session %s from a different "
|
||||
"declared conversation",
|
||||
self.name,
|
||||
sid,
|
||||
)
|
||||
return
|
||||
db.record_gateway_session_peer(
|
||||
sid,
|
||||
source=self._SESSION_SOURCE,
|
||||
@@ -6423,6 +6438,12 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
# the conversation it declared via ``X-Hermes-Session-Key`` before
|
||||
# minting a throwaway id — otherwise every reply is a new conversation
|
||||
# to every affinity surface (#96811).
|
||||
# The response chain still outranks the declared key. Recording is
|
||||
# gated on that same precedence: binding a session the chain selected
|
||||
# would rewrite ITS routing key to this request's header
|
||||
# (record_gateway_session_peer does SET session_key = ?), stranding the
|
||||
# original conversation and letting the header key recover it instead.
|
||||
_declared_selected = not stored_session_id and bool(gateway_session_key)
|
||||
session_id = (
|
||||
stored_session_id
|
||||
or self._declared_conversation_session(gateway_session_key)
|
||||
@@ -6498,7 +6519,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
tool_complete_callback=_on_tool_complete,
|
||||
agent_ref=agent_ref,
|
||||
gateway_session_key=gateway_session_key,
|
||||
bind_declared_conversation=True,
|
||||
bind_declared_conversation=_declared_selected,
|
||||
**agent_overrides,
|
||||
route=route,
|
||||
))
|
||||
@@ -6534,7 +6555,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
ephemeral_system_prompt=instructions,
|
||||
session_id=session_id,
|
||||
gateway_session_key=gateway_session_key,
|
||||
bind_declared_conversation=True,
|
||||
bind_declared_conversation=_declared_selected,
|
||||
**agent_overrides,
|
||||
route=route,
|
||||
)
|
||||
@@ -7599,10 +7620,11 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
# (/v1/responses, /v1/runs) record one, so no other
|
||||
# caller's rows change shape.
|
||||
if bind_declared_conversation:
|
||||
self._bind_declared_conversation(
|
||||
getattr(agent, "session_id", None) or session_id,
|
||||
gateway_session_key,
|
||||
)
|
||||
if _declared_selected:
|
||||
self._bind_declared_conversation(
|
||||
getattr(agent, "session_id", None) or session_id,
|
||||
gateway_session_key,
|
||||
)
|
||||
clear_session_vars(tokens)
|
||||
|
||||
self._activate_admitted_request()
|
||||
|
||||
Reference in New Issue
Block a user