diff --git a/agent/deadline.py b/agent/deadline.py index 83d7f3cf2c..a2c5cbfe05 100644 --- a/agent/deadline.py +++ b/agent/deadline.py @@ -648,32 +648,3 @@ def kill_process_tree(pid: int, *, sig: Optional[int] = None) -> bool: except Exception: continue return signalled - - -class SuspectableBackend: - """Protocol for backends whose connection state can be *poisoned* by a - race (teardown-vs-keepalive, auth-lock corruption) without the backend - itself being dead. - - The contract is **cheap-mark, lazy-verify**: noticing a poisoned state - must never do I/O — ``mark_suspect`` just latches a reason string. The - NEXT caller pays for verification once, via ``ensure_healthy``: a cheap - health probe that either clears the suspicion (backend was fine) or - forces a reconnect/recycle before the call proceeds. This is what keeps - a single race from permanently parking a connection (#81051/#77765/ - #84132): instead of parking on the ambiguous event, the backend is - marked suspect and recycled exactly once on next use. - """ - - def mark_suspect(self, reason: str) -> None: - """Latch a suspicion about this backend. Must be cheap (no I/O).""" - raise NotImplementedError - - async def ensure_healthy(self, timeout: float = 5.0) -> bool: - """Verify a suspect backend before reuse. - - Returns True when the backend is healthy (clearing the suspicion); - returns False after forcing a reconnect/recycle so the caller's - normal no-session path handles the rebuild. Must not raise. - """ - raise NotImplementedError diff --git a/tests/agent/test_deadline.py b/tests/agent/test_deadline.py index e66ac19b2e..c87252e506 100644 --- a/tests/agent/test_deadline.py +++ b/tests/agent/test_deadline.py @@ -545,6 +545,26 @@ class TestSequentialToolTimeoutResolver: # --------------------------------------------------------------------------- +def test_suspectable_backend_name_is_not_shadowed(): + """`agent.deadline.SuspectableBackend` must resolve to the Phase 3a + Protocol (sync `ensure_healthy(self) -> bool`), not a second, unrelated + `class SuspectableBackend` defined later in the module. A later Phase 3b + adopter independently redefined the same name as a concrete async class + (`ensure_healthy(self, timeout=5.0)`), which silently shadowed the + Protocol at module scope — nothing subclasses or imports either by name + today, so the collision caused no runtime breakage yet, but it would + hand the wrong (and differently-shaped) type to the next `from + agent.deadline import SuspectableBackend` consumer. + """ + import inspect + + from agent.deadline import SuspectableBackend + + assert getattr(SuspectableBackend, "_is_protocol", False) is True + assert not inspect.iscoroutinefunction(SuspectableBackend.ensure_healthy) + assert "timeout" not in inspect.signature(SuspectableBackend.ensure_healthy).parameters + + class _RecordingBackend: """Minimal SuspectableBackend: records mark_suspect calls."""