fix(context): fail closed when preflight compression stalls
This commit is contained in:
@@ -405,6 +405,21 @@ def compression_made_progress(
|
||||
_compression_made_progress = compression_made_progress
|
||||
|
||||
|
||||
class PreflightCompressionTimedOut(RuntimeError):
|
||||
"""Raised when an oversized turn cannot safely finish preflight."""
|
||||
|
||||
|
||||
def _fail_closed_after_preflight_timeout(agent, request_tokens: int) -> None:
|
||||
"""Stop an oversized turn instead of sending its unchanged provider payload."""
|
||||
if not getattr(agent, "_last_compression_timed_out", False):
|
||||
return
|
||||
raise PreflightCompressionTimedOut(
|
||||
"Context compression timed out before it could commit while the request "
|
||||
f"was still approximately {request_tokens:,} tokens. The provider call "
|
||||
"was not sent. Run /compress and wait for it to finish, then retry."
|
||||
)
|
||||
|
||||
|
||||
def _review_fork_first_request_pending(agent: Any) -> bool:
|
||||
"""Whether a detached review fork has yet to send its first provider request.
|
||||
|
||||
@@ -1169,6 +1184,7 @@ def build_turn_context(
|
||||
if not _compression_made_progress(
|
||||
_orig_len, len(messages), _orig_tokens, _preflight_tokens
|
||||
):
|
||||
_fail_closed_after_preflight_timeout(agent, _preflight_tokens)
|
||||
_preflight_compression_blocked = True
|
||||
break # Cannot compress further: neither rows nor tokens moved
|
||||
conversation_history = conversation_history_after_compression(
|
||||
|
||||
@@ -8068,6 +8068,11 @@ class AIAgent:
|
||||
auto-compress abort. Auto-compress callers use the default
|
||||
``force=False``.
|
||||
"""
|
||||
# Per-attempt signal consumed by turn-start preflight. A stalled
|
||||
# compression must not be mistaken for a structural no-op and followed
|
||||
# by the oversized provider request it was meant to prevent.
|
||||
self._last_compression_timed_out = False
|
||||
|
||||
from agent.conversation_compression import (
|
||||
CompressionCommitFence,
|
||||
compress_context,
|
||||
@@ -8194,6 +8199,7 @@ class AIAgent:
|
||||
timeout_cause["progress_observed"] = progress_observed
|
||||
|
||||
def _on_timeout(idle, waited, since_progress):
|
||||
self._last_compression_timed_out = True
|
||||
total_exhausted = timeout_cause["total_exhausted"]
|
||||
progress_observed = timeout_cause["progress_observed"]
|
||||
if total_exhausted:
|
||||
|
||||
@@ -467,6 +467,7 @@ class TestCompressContextForwarderOwnsTimeout:
|
||||
|
||||
assert out_msgs is original
|
||||
assert out_prompt == "sys"
|
||||
assert agent._last_compression_timed_out is True
|
||||
assert calls["n"] == 1
|
||||
agent._emit_warning.assert_called_once()
|
||||
assert agent.context_compressor._consecutive_timeout_failures == 1
|
||||
@@ -643,5 +644,6 @@ class TestCompressContextForwarderOwnsTimeout:
|
||||
commit_fence=fence,
|
||||
)
|
||||
assert seen["fence"] is fence
|
||||
assert agent._last_compression_timed_out is False
|
||||
assert prompt == "sys"
|
||||
assert msgs[0]["content"] == "ok"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Regression coverage for stalled preflight compression."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.turn_context import (
|
||||
PreflightCompressionTimedOut,
|
||||
_fail_closed_after_preflight_timeout,
|
||||
)
|
||||
|
||||
|
||||
def test_preflight_timeout_blocks_unchanged_provider_payload():
|
||||
agent = SimpleNamespace(_last_compression_timed_out=True)
|
||||
|
||||
with pytest.raises(PreflightCompressionTimedOut, match="provider call was not sent"):
|
||||
_fail_closed_after_preflight_timeout(agent, 190_035)
|
||||
|
||||
|
||||
def test_structural_noop_keeps_existing_preflight_behavior():
|
||||
agent = SimpleNamespace(_last_compression_timed_out=False)
|
||||
|
||||
_fail_closed_after_preflight_timeout(agent, 190_035)
|
||||
Reference in New Issue
Block a user