fix(compression): attempt-generation ownership for overlapping stall-fallback attempts
Follow-up to #96634 (stall-fallback retry, #78981) addressing donovan-yohan's post-merge adversarial review. The stall path detaches a timed-out primary worker (fence cancel wins; future stays on the pool) and immediately runs the fallback against the SAME ContextCompressor, creating two verified races: 1. Late-primary snapshot restore: the detached primary's unwind called _restore_compressor_attempt_state with the PRIMARY's pre-attempt snapshot. Landing after the fallback's commit it rolled _previous_summary/cooldown/provenance/telemetry back to pre-primary values, silently discarding fallback-owned state. 2. Shared _compression_cancelled_check: the late primary's `finally` cleared the callback the fallback had just installed, so the fallback's F4 cancellation consult read None. Fix: a monotonic per-compressor attempt generation claimed under one module lock (_claim_compressor_attempt). Snapshot restores carry their claiming generation and no-op when stale; the cancelled-check set/clear moves into owner-stamped helpers (_install_compression_cancelled_check / _clear_compression_cancelled_check_if_owner) so only the installing attempt can clear it. The commit fence keeps owning COMMIT admission; the generation owns compressor-ATTRIBUTE writes — two boundaries. Legacy callers (attempt_generation=None) and slotted third-party compressors (generation 0) keep the historical unconditional behavior. Secondary review items: - Lean chunk digests during a stall-fallback retry now follow the summary onto the pinned healthy route: take_pinned_summary_route() echoes the consumed route into a context-local _SUMMARY_ROUTE_CONSUMED, and _build_chunk_digests passes attempt_summary_route_kwargs() (non-consuming) to call_llm. The pin's single-use contract for the SUMMARY call is unchanged — the main-model retry still never re-issues the pinned route. - Worker re-run repeating pre-compression callbacks: documented as an accepted limitation on _retry_compression_on_fallback_chain (built-ins idempotent; resuming mid-pipeline would couple the retry to host callback ordering). Tests (tests/agent/test_compression_attempt_ownership.py, 10 cases): deterministic interleavings for both races (late-primary restore no-ops + preserves fallback state; stale finally cannot clear the fallback's callback), legacy/slotted compatibility, digest route follow + context-locality of the consumed echo. Mutation-checked: reverting only the two prod files to origin/main fails the suite; restored stack green (21 passed incl. the original #78981 suite). The one red in the wider sweep (test_silence_cannot_approach_double_idle_timeout) is pre-existing on clean origin/main — verified independently.
This commit is contained in:
@@ -77,15 +77,23 @@ def _safe_int(value: Any) -> int | None:
|
||||
# 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.
|
||||
# ``_build_chunk_digests``, which issues its own ``call_llm`` calls directly.
|
||||
# Those digests consult ``attempt_summary_route_kwargs()`` (non-consuming):
|
||||
# during a stall-fallback retry they follow the summary onto the healthy
|
||||
# fallback backend instead of returning to the stalled primary. The consumed
|
||||
# echo below preserves the pin's single-use contract for the SUMMARY call —
|
||||
# the main-model retry still never re-issues the pinned route.
|
||||
_SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[Dict[str, Any]]] = (
|
||||
contextvars.ContextVar("hermes_summary_route_pin", default=None)
|
||||
)
|
||||
|
||||
# Echo of the route the summary call consumed, for SIBLING aux calls of the
|
||||
# same attempt (lean digests). Context-local like the pin itself, so it can
|
||||
# never leak across threads or into an unrelated compression attempt.
|
||||
_SUMMARY_ROUTE_CONSUMED: contextvars.ContextVar[Optional[Dict[str, Any]]] = (
|
||||
contextvars.ContextVar("hermes_summary_route_consumed", default=None)
|
||||
)
|
||||
|
||||
# call_llm kwargs a pinned route may set. ``timeout`` lets a fallback entry
|
||||
# keep its own deadline instead of inheriting one the primary already burned
|
||||
# (same per-entry semantics the aux client applies to chain candidates).
|
||||
@@ -120,14 +128,38 @@ def take_pinned_summary_route() -> Optional[Dict[str, Any]]:
|
||||
Single use by design. ``_generate_summary`` retries itself on the main
|
||||
model when the summary route fails; re-issuing the pinned route there
|
||||
would spend a second full deadline on the backend that just failed.
|
||||
|
||||
The consumed route is echoed into ``_SUMMARY_ROUTE_CONSUMED`` so that
|
||||
SIBLING auxiliary calls in the same attempt (the lean chunk digests,
|
||||
which run after the summary) can keep addressing the healthy fallback
|
||||
backend instead of silently returning to the stalled task route
|
||||
(#96634 post-merge review, secondary item).
|
||||
"""
|
||||
route = _SUMMARY_ROUTE_PIN.get()
|
||||
if route is None:
|
||||
return None
|
||||
_SUMMARY_ROUTE_PIN.set(None)
|
||||
_SUMMARY_ROUTE_CONSUMED.set(route)
|
||||
return route
|
||||
|
||||
|
||||
def attempt_summary_route_kwargs() -> Dict[str, Any]:
|
||||
"""Route kwargs for sibling aux calls of the CURRENT summary attempt.
|
||||
|
||||
Non-consuming. Prefers a still-pending pin (digest paths that run before
|
||||
the summary), else the route the summary call just consumed. Empty when
|
||||
no stall-fallback pin is active — normal task routing applies.
|
||||
"""
|
||||
route = _SUMMARY_ROUTE_PIN.get() or _SUMMARY_ROUTE_CONSUMED.get()
|
||||
if not route:
|
||||
return {}
|
||||
return {
|
||||
field: route[field]
|
||||
for field in _PINNED_ROUTE_FIELDS
|
||||
if route.get(field) not in (None, "")
|
||||
}
|
||||
|
||||
|
||||
def _pinned_summary_call_kwargs() -> Dict[str, Any]:
|
||||
"""Consume the pinned route as explicit ``call_llm`` keyword arguments."""
|
||||
route = take_pinned_summary_route()
|
||||
@@ -4646,6 +4678,9 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
|
||||
try:
|
||||
from agent.auxiliary_client import call_llm
|
||||
|
||||
# During a stall-fallback retry, follow the summary onto the
|
||||
# pinned healthy route (non-consuming read) instead of
|
||||
# re-addressing the stalled task backend (#96634 follow-up).
|
||||
resp = call_llm(
|
||||
messages=[{
|
||||
"role": "user",
|
||||
@@ -4653,6 +4688,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb
|
||||
}],
|
||||
task="compression",
|
||||
max_tokens=_LEAN_DIGEST_MAX_TOKENS,
|
||||
**attempt_summary_route_kwargs(),
|
||||
)
|
||||
body = (
|
||||
resp.choices[0].message.content
|
||||
|
||||
@@ -329,14 +329,121 @@ def _snapshot_compressor_attempt_state(compressor: Any) -> dict[str, Any]:
|
||||
return copy.deepcopy(selected)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Attempt ownership (#96634 follow-up).
|
||||
#
|
||||
# The stall-fallback path deliberately DETACHES a timed-out primary worker
|
||||
# (fence cancel wins; the future stays on the shared pool) and immediately
|
||||
# starts a fallback attempt against the SAME ContextCompressor. Two races
|
||||
# follow from that overlap:
|
||||
#
|
||||
# 1. The late primary's unwind still calls _restore_compressor_attempt_state
|
||||
# with the PRIMARY's pre-attempt snapshot. Landing after the fallback's
|
||||
# commit, it rolls _previous_summary / cooldown / provenance / telemetry
|
||||
# back to pre-primary values — silently discarding fallback-owned state.
|
||||
# 2. _compression_cancelled_check is one shared attribute: the late
|
||||
# primary's ``finally`` clears the callback the fallback just installed,
|
||||
# so the fallback's F4 cancellation consult reads None.
|
||||
#
|
||||
# Both are fixed with a monotonic per-compressor attempt generation, claimed
|
||||
# under one module lock. Restores and callback set/clear are keyed to the
|
||||
# claiming generation and no-op when a newer attempt owns the compressor.
|
||||
# The commit fence still owns COMMIT admission; the generation owns
|
||||
# compressor-ATTRIBUTE writes — two different boundaries.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMPRESSOR_ATTEMPT_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _claim_compressor_attempt(compressor: Any) -> int:
|
||||
"""Claim the compressor for a new attempt; returns its generation id.
|
||||
|
||||
Monotonic per compressor instance. Any restore or cancelled-check
|
||||
mutation stamped with an OLDER generation becomes a no-op, so a
|
||||
detached, late-unwinding attempt cannot clobber its successor's state.
|
||||
"""
|
||||
with _COMPRESSOR_ATTEMPT_LOCK:
|
||||
generation = int(getattr(compressor, "_compression_attempt_generation", 0) or 0) + 1
|
||||
try:
|
||||
compressor._compression_attempt_generation = generation
|
||||
except Exception:
|
||||
# Slotted/frozen third-party compressor: ownership tracking is
|
||||
# unavailable; generation 0 disables the guard (legacy behavior).
|
||||
return 0
|
||||
return generation
|
||||
|
||||
|
||||
def _compressor_attempt_is_current(compressor: Any, generation: int) -> bool:
|
||||
"""True when *generation* still owns the compressor (or guard disabled)."""
|
||||
if not generation:
|
||||
return True
|
||||
with _COMPRESSOR_ATTEMPT_LOCK:
|
||||
return (
|
||||
int(getattr(compressor, "_compression_attempt_generation", 0) or 0)
|
||||
== generation
|
||||
)
|
||||
|
||||
|
||||
def _install_compression_cancelled_check(
|
||||
compressor: Any, check: Any, generation: int
|
||||
) -> None:
|
||||
"""Install the F4 cancellation consult, stamped with its owner attempt."""
|
||||
with _COMPRESSOR_ATTEMPT_LOCK:
|
||||
try:
|
||||
compressor._compression_cancelled_check = check
|
||||
compressor._compression_cancelled_check_owner = generation
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _clear_compression_cancelled_check_if_owner(
|
||||
compressor: Any, generation: int
|
||||
) -> bool:
|
||||
"""Clear the cancellation consult only when *generation* installed it.
|
||||
|
||||
A detached late primary's ``finally`` must not tear down the callback a
|
||||
newer fallback attempt just installed. Returns True when cleared.
|
||||
"""
|
||||
with _COMPRESSOR_ATTEMPT_LOCK:
|
||||
owner = getattr(compressor, "_compression_cancelled_check_owner", None)
|
||||
if owner is not None and generation and owner != generation:
|
||||
return False
|
||||
try:
|
||||
compressor._compression_cancelled_check = None
|
||||
compressor._compression_cancelled_check_owner = None
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def _restore_compressor_attempt_state(
|
||||
compressor: Any,
|
||||
snapshot: dict[str, Any],
|
||||
*,
|
||||
durable_cooldown_authoritative: Optional[bool] = None,
|
||||
durable_cooldown_state: Optional[dict[str, Any]] = None,
|
||||
attempt_generation: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Restore the safe per-attempt snapshot after a pre-commit hard cancel."""
|
||||
"""Restore the safe per-attempt snapshot after a pre-commit hard cancel.
|
||||
|
||||
``attempt_generation`` (when provided) is the claim the calling attempt
|
||||
took via :func:`_claim_compressor_attempt`. A restore stamped with a
|
||||
stale generation no-ops: the stall-fallback path detaches a timed-out
|
||||
primary and hands the compressor to a fallback attempt, and the late
|
||||
primary's unwind must not roll fallback-owned state back to the
|
||||
primary's pre-attempt snapshot (#96634 post-merge review, claim 1).
|
||||
"""
|
||||
if attempt_generation is not None and not _compressor_attempt_is_current(
|
||||
compressor, attempt_generation
|
||||
):
|
||||
logger.warning(
|
||||
"Skipping stale compressor attempt-state restore: attempt "
|
||||
"generation %s no longer owns the compressor (current: %s). A "
|
||||
"newer (stall-fallback) attempt's state is preserved.",
|
||||
attempt_generation,
|
||||
getattr(compressor, "_compression_attempt_generation", None),
|
||||
)
|
||||
return
|
||||
# A successful summary clears the durable cooldown before the outer commit
|
||||
# boundary. Recreate (or clear) that row before restoring exact in-memory
|
||||
# values, otherwise the next refresh would overwrite this rollback. Unknown
|
||||
@@ -930,6 +1037,13 @@ def _retry_compression_on_fallback_chain(
|
||||
entry's own ``timeout`` (when declared) sets that idle window, so a
|
||||
fallback tuned for a slower-but-healthy backend is not held to a deadline
|
||||
the stalled primary defined (#62452 semantics, applied to the stall path).
|
||||
|
||||
Known limitation (accepted, #96634 review): the retry re-runs the COMPLETE
|
||||
worker, which repeats memory/plugin pre-compression callbacks. Built-in
|
||||
callbacks are idempotent (re-reads and overwrites of attempt-scoped
|
||||
state); third-party plugin callbacks are advised to be. Splitting the
|
||||
worker to resume mid-pipeline would couple this path to every host's
|
||||
callback ordering — deliberately out of scope.
|
||||
"""
|
||||
# An explicit stop is not a stalled route. The retry worker would abort on
|
||||
# the same event anyway, but starting one at all makes /stop look ignored.
|
||||
@@ -2580,6 +2694,10 @@ def compress_context(
|
||||
_compressor_attempt_snapshot = _snapshot_compressor_attempt_state(
|
||||
agent.context_compressor
|
||||
)
|
||||
# Claim attempt ownership: a detached, late-unwinding sibling attempt
|
||||
# (stall-fallback overlap) must not restore its snapshot over ours or
|
||||
# clear our cancellation consult (#96634 post-merge review).
|
||||
_attempt_generation = _claim_compressor_attempt(agent.context_compressor)
|
||||
_durable_cooldown_authoritative: Optional[bool] = None
|
||||
_durable_cooldown_state: Optional[dict[str, Any]] = None
|
||||
if (
|
||||
@@ -2643,7 +2761,8 @@ def compress_context(
|
||||
)
|
||||
if not _codex_fence_entered:
|
||||
_restore_compressor_attempt_state(
|
||||
agent.context_compressor, _compressor_attempt_snapshot
|
||||
agent.context_compressor, _compressor_attempt_snapshot,
|
||||
attempt_generation=_attempt_generation,
|
||||
)
|
||||
existing_prompt = getattr(agent, "_cached_system_prompt", None)
|
||||
if not existing_prompt:
|
||||
@@ -3381,12 +3500,11 @@ def compress_context(
|
||||
# removed in the finally below so it cannot leak into later attempts
|
||||
# (e.g. a manual /compress force-clear).
|
||||
if commit_fence is not None:
|
||||
try:
|
||||
agent.context_compressor._compression_cancelled_check = (
|
||||
lambda: commit_fence.is_cancelled
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_install_compression_cancelled_check(
|
||||
agent.context_compressor,
|
||||
lambda: commit_fence.is_cancelled,
|
||||
_attempt_generation,
|
||||
)
|
||||
# Incoming-message interrupts and active-turn redirects must not tear an
|
||||
# atomic summary in half (#23975). Explicit stop surfaces set a separate
|
||||
# Event atomically; never infer cause from the racy message fields.
|
||||
@@ -3416,10 +3534,9 @@ def compress_context(
|
||||
raise AuxiliaryExplicitCancellation()
|
||||
finally:
|
||||
if commit_fence is not None:
|
||||
try:
|
||||
agent.context_compressor._compression_cancelled_check = None
|
||||
except Exception:
|
||||
pass
|
||||
_clear_compression_cancelled_check_if_owner(
|
||||
agent.context_compressor, _attempt_generation
|
||||
)
|
||||
except AuxiliaryExplicitCancellation:
|
||||
try:
|
||||
_restore_compressor_attempt_state(
|
||||
@@ -3427,6 +3544,7 @@ def compress_context(
|
||||
_compressor_attempt_snapshot,
|
||||
durable_cooldown_authoritative=_durable_cooldown_authoritative,
|
||||
durable_cooldown_state=_durable_cooldown_state,
|
||||
attempt_generation=_attempt_generation,
|
||||
)
|
||||
except BaseException as _rollback_exc:
|
||||
# Compensation failure must surface, but it must not strand the
|
||||
@@ -3594,6 +3712,7 @@ def compress_context(
|
||||
_compressor_attempt_snapshot,
|
||||
durable_cooldown_authoritative=_durable_cooldown_authoritative,
|
||||
durable_cooldown_state=_durable_cooldown_state,
|
||||
attempt_generation=_attempt_generation,
|
||||
)
|
||||
if (
|
||||
messages_before_compression is not None
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Attempt-ownership guards for overlapping compression attempts (#96634).
|
||||
|
||||
The stall-fallback path (#78981, PR #96634) deliberately DETACHES a
|
||||
timed-out primary compression worker — the fence cancel wins and the
|
||||
future stays on the shared pool — then immediately runs a fallback attempt
|
||||
against the SAME ``ContextCompressor``. donovan-yohan's post-merge
|
||||
adversarial review identified two interleavings where the still-unwinding
|
||||
primary clobbers fallback-owned state:
|
||||
|
||||
1. The late primary's unwind calls ``_restore_compressor_attempt_state``
|
||||
with the PRIMARY's pre-attempt snapshot. Landing after the fallback's
|
||||
commit, it rolls ``_previous_summary`` / cooldown / provenance /
|
||||
telemetry back to pre-primary values.
|
||||
2. ``_compression_cancelled_check`` is one shared attribute: the late
|
||||
primary's ``finally`` clears the callback the fallback just installed.
|
||||
|
||||
Both are now guarded by a monotonic per-compressor attempt generation
|
||||
(``_claim_compressor_attempt``): restores and callback set/clear are keyed
|
||||
to the claiming generation and no-op when a newer attempt owns the
|
||||
compressor. These tests drive both interleavings deterministically —
|
||||
no timing, no threads.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from agent.conversation_compression import (
|
||||
_claim_compressor_attempt,
|
||||
_clear_compression_cancelled_check_if_owner,
|
||||
_compressor_attempt_is_current,
|
||||
_install_compression_cancelled_check,
|
||||
_restore_compressor_attempt_state,
|
||||
_snapshot_compressor_attempt_state,
|
||||
)
|
||||
|
||||
|
||||
def _compressor(**overrides):
|
||||
"""Bare compressor stand-in carrying only attempt-state fields."""
|
||||
base = {
|
||||
"_previous_summary": "primary-era summary",
|
||||
"_summary_failure_cooldown_until": 0.0,
|
||||
"_last_summary_error": None,
|
||||
"_cooldown_persist_failed": False,
|
||||
}
|
||||
base.update(overrides)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
class TestLatePrimaryRestoreAfterFallbackCommit:
|
||||
"""Claim 1: a stale attempt's snapshot restore must no-op."""
|
||||
|
||||
def test_stale_restore_noops_and_preserves_fallback_state(self):
|
||||
compressor = _compressor()
|
||||
|
||||
# Primary attempt starts: snapshot + claim.
|
||||
primary_snapshot = _snapshot_compressor_attempt_state(compressor)
|
||||
primary_gen = _claim_compressor_attempt(compressor)
|
||||
|
||||
# Primary "runs" and mutates state, then stalls; the host detaches
|
||||
# it and starts the fallback attempt, which claims a NEWER generation
|
||||
# and commits its own state.
|
||||
compressor._previous_summary = "primary partial work"
|
||||
fallback_gen = _claim_compressor_attempt(compressor)
|
||||
assert fallback_gen > primary_gen
|
||||
compressor._previous_summary = "FALLBACK COMMITTED SUMMARY"
|
||||
compressor._summary_failure_cooldown_until = 123.0
|
||||
|
||||
# The detached primary finally unwinds and tries to restore its
|
||||
# pre-attempt snapshot. Stale generation → must no-op.
|
||||
_restore_compressor_attempt_state(
|
||||
compressor, primary_snapshot, attempt_generation=primary_gen
|
||||
)
|
||||
|
||||
assert compressor._previous_summary == "FALLBACK COMMITTED SUMMARY"
|
||||
assert compressor._summary_failure_cooldown_until == 123.0
|
||||
|
||||
def test_current_attempt_restore_still_works(self):
|
||||
"""The guard must not break the legitimate same-attempt rollback."""
|
||||
compressor = _compressor()
|
||||
snapshot = _snapshot_compressor_attempt_state(compressor)
|
||||
gen = _claim_compressor_attempt(compressor)
|
||||
|
||||
compressor._previous_summary = "attempt scribbles"
|
||||
_restore_compressor_attempt_state(
|
||||
compressor, snapshot, attempt_generation=gen
|
||||
)
|
||||
|
||||
assert compressor._previous_summary == "primary-era summary"
|
||||
|
||||
def test_legacy_callers_without_generation_are_unchanged(self):
|
||||
"""attempt_generation=None preserves the historical always-restore."""
|
||||
compressor = _compressor()
|
||||
snapshot = _snapshot_compressor_attempt_state(compressor)
|
||||
_claim_compressor_attempt(compressor) # someone else claims
|
||||
|
||||
compressor._previous_summary = "scribbles"
|
||||
_restore_compressor_attempt_state(compressor, snapshot)
|
||||
|
||||
assert compressor._previous_summary == "primary-era summary"
|
||||
|
||||
def test_slotted_compressor_disables_guard_gracefully(self):
|
||||
"""A compressor that rejects attribute writes yields generation 0
|
||||
(guard off) rather than raising into the compression path."""
|
||||
|
||||
class Frozen:
|
||||
__slots__ = ()
|
||||
|
||||
gen = _claim_compressor_attempt(Frozen())
|
||||
assert gen == 0
|
||||
# Generation 0 always reports current — legacy behavior.
|
||||
assert _compressor_attempt_is_current(Frozen(), 0) is True
|
||||
|
||||
|
||||
class TestCancelledCheckOwnership:
|
||||
"""Claim 2: only the installing attempt may clear the shared callback."""
|
||||
|
||||
def test_stale_primary_finally_cannot_clear_fallback_callback(self):
|
||||
compressor = _compressor()
|
||||
|
||||
primary_gen = _claim_compressor_attempt(compressor)
|
||||
_install_compression_cancelled_check(
|
||||
compressor, lambda: "primary", primary_gen
|
||||
)
|
||||
|
||||
# Fallback claims and installs ITS callback while the primary is
|
||||
# still unwinding.
|
||||
fallback_gen = _claim_compressor_attempt(compressor)
|
||||
fallback_check = lambda: "fallback" # noqa: E731
|
||||
_install_compression_cancelled_check(
|
||||
compressor, fallback_check, fallback_gen
|
||||
)
|
||||
|
||||
# Detached primary's ``finally`` fires late — must be refused.
|
||||
cleared = _clear_compression_cancelled_check_if_owner(
|
||||
compressor, primary_gen
|
||||
)
|
||||
|
||||
assert cleared is False
|
||||
assert compressor._compression_cancelled_check is fallback_check
|
||||
|
||||
# The owner can still clear its own callback afterwards.
|
||||
assert _clear_compression_cancelled_check_if_owner(
|
||||
compressor, fallback_gen
|
||||
) is True
|
||||
assert compressor._compression_cancelled_check is None
|
||||
|
||||
def test_owner_clear_roundtrip(self):
|
||||
compressor = _compressor()
|
||||
gen = _claim_compressor_attempt(compressor)
|
||||
check = lambda: True # noqa: E731
|
||||
_install_compression_cancelled_check(compressor, check, gen)
|
||||
assert compressor._compression_cancelled_check is check
|
||||
|
||||
assert _clear_compression_cancelled_check_if_owner(compressor, gen)
|
||||
assert compressor._compression_cancelled_check is None
|
||||
assert compressor._compression_cancelled_check_owner is None
|
||||
|
||||
def test_generation_zero_clear_is_unconditional(self):
|
||||
"""Guard-disabled (legacy/slotted) attempts keep the old clear."""
|
||||
compressor = _compressor()
|
||||
gen = _claim_compressor_attempt(compressor)
|
||||
_install_compression_cancelled_check(compressor, lambda: True, gen)
|
||||
|
||||
# generation 0 = guard disabled → clears like the historical code.
|
||||
assert _clear_compression_cancelled_check_if_owner(compressor, 0)
|
||||
assert compressor._compression_cancelled_check is None
|
||||
|
||||
|
||||
class TestDigestCallsFollowThePinnedRoute:
|
||||
"""Secondary item: lean digests must not return to the stalled route."""
|
||||
|
||||
def test_consumed_pin_still_routes_sibling_digest_calls(self):
|
||||
import contextvars
|
||||
|
||||
def _probe():
|
||||
from agent.context_compressor import (
|
||||
attempt_summary_route_kwargs,
|
||||
pin_summary_route,
|
||||
take_pinned_summary_route,
|
||||
)
|
||||
|
||||
route = {"provider": "fallback-prov", "model": "fallback-model"}
|
||||
with pin_summary_route(route):
|
||||
# Summary call consumes the pin (single-use preserved)...
|
||||
consumed = take_pinned_summary_route()
|
||||
assert consumed == route
|
||||
assert take_pinned_summary_route() is None
|
||||
# ...but sibling digest calls still see the attempt route.
|
||||
kwargs = attempt_summary_route_kwargs()
|
||||
assert kwargs.get("provider") == "fallback-prov"
|
||||
assert kwargs.get("model") == "fallback-model"
|
||||
return True
|
||||
|
||||
# Fresh context per test: the consumed echo must not leak in from
|
||||
# any other test that touched the contextvars.
|
||||
assert contextvars.copy_context().run(_probe) is True
|
||||
|
||||
def test_no_pin_means_no_route_override(self):
|
||||
import contextvars
|
||||
|
||||
def _probe():
|
||||
from agent.context_compressor import attempt_summary_route_kwargs
|
||||
|
||||
return attempt_summary_route_kwargs()
|
||||
|
||||
assert contextvars.copy_context().run(_probe) == {}
|
||||
|
||||
def test_consumed_echo_is_context_local(self):
|
||||
"""The echo cannot leak into an unrelated attempt's context."""
|
||||
import contextvars
|
||||
|
||||
def _consume_in_isolated_context():
|
||||
from agent.context_compressor import (
|
||||
pin_summary_route,
|
||||
take_pinned_summary_route,
|
||||
)
|
||||
|
||||
with pin_summary_route({"provider": "p", "model": "m"}):
|
||||
take_pinned_summary_route()
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
ctx.run(_consume_in_isolated_context)
|
||||
|
||||
# Outer context never saw the pin or its echo.
|
||||
from agent.context_compressor import attempt_summary_route_kwargs
|
||||
|
||||
assert attempt_summary_route_kwargs() == {}
|
||||
Reference in New Issue
Block a user