refactor(gateway): drop the write-only one-shot generation stamp; /stop and eviction share _drop_turn_slot

- The `one_turn_restore["run_generation"]` stamp had no reader once settlement
  moved to the invalidate chokepoint (the finalizer guards on its own generation
  via _is_session_run_current); delete it and the test lines that set it.
- release-slot-then-evict-cached-agent (#44212 rationale) was duplicated in
  /stop and eviction; one _drop_turn_slot owns it.
- Best-effort interrupt uses the repo's _log_suppressed seam like run_shutdown.
- turn_lease.rebind resolves the lease via token.lease like release does
  (identity, not a session_id lookup); _held_turn_lease hands back the token
  map so release/rebind stop re-peeking session state.
- Test trims: vacuous isinstance, registry-internals asserts.
This commit is contained in:
kshitijk4poor
2026-09-11 12:46:43 +05:30
committed by kshitij
parent 90970d85f2
commit 789eec791b
6 changed files with 30 additions and 42 deletions
+24 -20
View File
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
from agent.interrupt_compat import _accepts_keyword
from gateway.config import Platform
from gateway.session import SessionSource, build_session_context_prompt
from gateway.run_shutdown import _log_suppressed
from hermes_cli.config import cfg_get
if TYPE_CHECKING: # string annotations only; never imported at runtime (cycle)
@@ -255,14 +256,24 @@ class GatewayAgentCacheMixin:
self._persist_active_agents()
return True
def _drop_turn_slot(self, session_key: str) -> None:
"""Release the running-agent slot and evict the cached instance (/stop, eviction, reaper).
``_interrupt_requested`` is cleared only by the turn finalizer, so on a hung/still-draining
run the flag would survive and silently kill the session's NEXT message (interrupted=True,
api_calls=0, empty response); the next message rebuilds from history while the old agent
keeps its flag so a hung drain still dies (#44212)."""
self._release_running_agent_state(session_key)
self._evict_cached_agent(session_key)
def _held_turn_lease(self, session_key: str, run_generation: int):
"""Return ``(registry, token)`` when ``session_key`` holds a lease token for ``run_generation``, else None."""
"""Return ``(registry, lease_tokens)`` when ``session_key`` holds a lease token for
``run_generation``, else None. Callers ``get``/``pop`` the token from the map themselves."""
registry = getattr(self, "_turn_leases", None)
state = self._peek_session_state(session_key) if session_key and registry is not None else None
token = state.turn.lease_tokens.get(run_generation) if state is not None else None
if token is None:
tokens = state.turn.lease_tokens if state is not None else None
if tokens is None or run_generation not in tokens:
return None
return registry, token
return registry, tokens
def _release_turn_lease(self, session_key: str, run_generation: int) -> bool:
"""Release the turn lease acquired by (``session_key``, ``run_generation``). Keyed by (routing
@@ -271,8 +282,8 @@ class GatewayAgentCacheMixin:
held = self._held_turn_lease(session_key, run_generation)
if held is None:
return False
registry, token = held
self._peek_session_state(session_key).turn.lease_tokens.pop(run_generation, None)
registry, tokens = held
token = tokens.pop(run_generation)
try:
return registry.release(token)
except Exception:
@@ -286,9 +297,9 @@ class GatewayAgentCacheMixin:
held = self._held_turn_lease(session_key, run_generation) if new_session_id else None
if held is None:
return False
registry, token = held
registry, tokens = held
try:
return registry.rebind(token, new_session_id)
return registry.rebind(tokens[run_generation], new_session_id)
except Exception:
logger.debug("Failed to rebind turn lease", exc_info=True)
return False
@@ -392,12 +403,11 @@ class GatewayAgentCacheMixin:
running_agent = state.turn.agent if state else None
_process_task_id, _process_baseline = "", None
if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
try:
# A raising interrupt implementation must not leave the slot unroutable: the generation
# bump and release below are the cleanup that matters.
with _log_suppressed(logging.WARNING, "Failed to interrupt running agent for %s; continuing",
session_key, exc_info=True):
request_hard_interrupt(running_agent, interrupt_reason)
except Exception:
# A raising interrupt implementation must not leave the slot unroutable: the
# generation bump and release below are the cleanup that matters.
logger.warning("Failed to interrupt running agent for %s; continuing", session_key, exc_info=True)
_process_task_id = getattr(running_agent, "_gateway_turn_process_task_id", "")
_process_baseline = getattr(running_agent, "_gateway_turn_process_baseline", None)
# Bump the generation BEFORE scheduling the reap thread and capture the post-bump value:
@@ -442,13 +452,7 @@ class GatewayAgentCacheMixin:
if state is not None:
state.persistent.pending_command_text = None
if release_running_state:
self._release_running_agent_state(session_key)
# Evict the cached agent: ``_interrupt_requested`` is only cleared by the turn finalizer,
# so on a hung/still-draining run the flag survives and silently kills the session's NEXT
# message (interrupted=True, api_calls=0, empty response). Like /new and /model, the next
# message rebuilds from history; the old agent keeps its flag so a hung drain still dies.
# See #44212.
self._evict_cached_agent(session_key)
self._drop_turn_slot(session_key)
async def _refresh_agent_cache_message_count(self, session_key: str, session_id: Optional[str]) -> None:
"""Re-baseline a cached agent's stored message_count after THIS turn — the coherence guard