245 lines
9.7 KiB
Python
245 lines
9.7 KiB
Python
"""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 TestSummaryRoutePinSingleUse:
|
|
"""The pin is single-use: consumed by the one summary call per attempt."""
|
|
|
|
def test_pin_is_consumed_once(self):
|
|
import contextvars
|
|
|
|
def _probe():
|
|
from agent.context_compressor import (
|
|
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
|
|
# ...and the main-model retry never re-issues it.
|
|
assert take_pinned_summary_route() is None
|
|
return True
|
|
|
|
# Fresh context per test: state must not leak in from any other
|
|
# test that touched the contextvars.
|
|
assert contextvars.copy_context().run(_probe) is True
|
|
|
|
def test_consume_is_context_local(self):
|
|
"""A consume in one context cannot leak into an unrelated attempt's."""
|
|
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.
|
|
from agent.context_compressor import take_pinned_summary_route
|
|
|
|
assert take_pinned_summary_route() is None
|
|
|
|
|
|
class TestMidRestoreClaimRace:
|
|
"""TOCTOU: a claim landing between entry check and write must void the restore."""
|
|
|
|
def test_claim_during_restore_body_voids_the_write(self, monkeypatch):
|
|
import agent.conversation_compression as cc
|
|
|
|
compressor = _compressor()
|
|
snapshot = _snapshot_compressor_attempt_state(compressor)
|
|
primary_gen = _claim_compressor_attempt(compressor)
|
|
compressor._previous_summary = "FALLBACK STATE"
|
|
|
|
# Interleave deterministically: the fallback claims the compressor
|
|
# while the primary is inside the restore body (during deepcopy of
|
|
# the snapshot, i.e. after the entry check passed).
|
|
real_deepcopy = cc.copy.deepcopy
|
|
state = {"claimed": False}
|
|
|
|
def claiming_deepcopy(obj, *a, **kw):
|
|
if not state["claimed"] and obj is snapshot:
|
|
state["claimed"] = True
|
|
_claim_compressor_attempt(compressor) # fallback arrives NOW
|
|
return real_deepcopy(obj, *a, **kw)
|
|
|
|
monkeypatch.setattr(cc.copy, "deepcopy", claiming_deepcopy)
|
|
_restore_compressor_attempt_state(
|
|
compressor, snapshot, attempt_generation=primary_gen
|
|
)
|
|
|
|
# The write-time re-check must have refused the stale restore.
|
|
assert compressor._previous_summary == "FALLBACK STATE"
|