From 6151e59d655bc88b6ed2a1eb01b069b95b9cecdc Mon Sep 17 00:00:00 2001 From: Shaun Eccles Date: Thu, 27 Aug 2026 07:59:27 +1000 Subject: [PATCH] fix(compression): surface an unpublished stall-fallback fence at WARNING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up for #95433. When the host's fence factory is absent or raises, the retry runs on a private CompressionCommitFence() that hard-interrupt admission never reads — /stop would serialize against the aborted attempt's fence instead of the retry's commit boundary. Promote the factory-failure swallow from debug to warning and warn when the retry has no published fence, so the control-plane degradation is visible rather than silent. Also documents the pin's coverage (single _generate_summary call; the lean-mode chunk digests are a separate unpinned call path). --- agent/context_compressor.py | 10 ++++++++++ agent/conversation_compression.py | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index b9cca84c76..e98fbb6a08 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -72,6 +72,16 @@ def _safe_int(value: Any) -> int | None: # with it. Context is copied per worker (``propagate_context_to_thread``), so # the pin reaches the retry's whole synchronous call chain and cannot leak # into the stalled attempt or any unrelated auxiliary call. +# +# Coverage is the single ``_generate_summary`` LLM call only. That is one call +# per compression run (its only non-recursive call site is the compress path; +# the two recursive calls are the deliberate main-model retry that must NOT +# re-issue the pin). Lean ``tail_mode`` additionally runs +# ``_build_chunk_digests``, which issues its own ``call_llm`` calls directly +# and never consults the pin — during a stall-fallback retry those digests +# still target the stalled primary and degrade to per-segment placeholders. +# Deliberate: the digest path is a best-effort augmentation, not the summary, +# and pinning it would require weakening the single-use contract below. _SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[Dict[str, Any]]] = ( contextvars.ContextVar("hermes_summary_route_pin", default=None) ) diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index e7c032d752..05fed80da8 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -953,11 +953,18 @@ def _retry_compression_on_fallback_chain( try: retry_fence = new_fence() except Exception: - logger.debug( - "compression stall-fallback fence factory failed", + logger.warning( + "compression stall-fallback fence factory failed; the retry " + "will run on an unpublished fence (a /stop mid-retry cannot " + "serialize against its commit boundary)", exc_info=True, ) if not isinstance(retry_fence, CompressionCommitFence): + logger.warning( + "compression stall-fallback retry running on an unpublished fence; " + "hard-interrupt admission will read the aborted attempt's fence " + "rather than the retry's commit boundary", + ) retry_fence = CompressionCommitFence() idle = float(route.get("timeout") or idle_timeout_seconds) ceiling = max(float(total_ceiling_seconds), idle)