Revert "[eric] agents: the policy ratchet narrows the declared recap mode, not whatever the turn happened to render"

This reverts commit 729dc968bc.
This commit is contained in:
ciregenz
2026-08-31 22:46:48 -07:00
parent 729dc968bc
commit ae44920cfe
2 changed files with 3 additions and 97 deletions
@@ -31,7 +31,6 @@ from backend.apps.agents.core.error_classify import (
from backend.apps.agents.core.is_router_unavailable_error import is_router_unavailable_error
from backend.apps.agents.core.extract_reset_hint import extract_reset_hint
from backend.apps.agents.core.redact_for_telemetry import redact_for_telemetry
from backend.apps.agents.manager.run.RunOptions import PREFIX_NARROWNESS
from backend.apps.agents.core import flight_recorder
from backend.apps.agents.manager.run.empty_finish import count_tool_calls
from backend.apps.agents.session_credential import api_key_twin_model
@@ -377,23 +376,14 @@ async def handle_run_error(e: Exception, session: AgentSession, session_id: str,
# The provider's abuse classifier declined the REQUEST, and on the subscription lane what it reads as "duplicating model outputs" is OUR recap of the chat on a fresh CLI session (192 blocks in 14 days, 0 on API keys); deterministic, so the one retry that can pass carries no history at all, the session stays that way, and every block is reported with the shape it sent.
p_sent = session.history_prefix_sent
p_report_model_error(f"policy_block:{p_sent}", session_id, session, turn, e, p_stderr_tail)
# Ratchet on the DECLARED mode, never on what happened to be rendered. `history_prefix_sent`
# defaults to "none" on every turn and is only overwritten when a fresh-session rebuild
# actually attaches a recap, so on a RESUMED turn (the common case) it reads "none" while the
# mode is still "minimal". Keying the ladder on it meant a block on a resumed turn skipped the
# ratchet entirely, went straight to "nothing left to strip", and left the mode untouched, so
# the next rebuild sent a recap again and blocked again. Live: one chat blocked 3x with
# sent going none -> minimal -> none, which ENG-399 says can never happen (2026-08-30).
p_mode = session.history_prefix_mode
if p_mode != "none":
p_next = PREFIX_NARROWNESS[PREFIX_NARROWNESS.index(p_mode) + 1]
session.history_prefix_mode = p_next
if p_sent != "none":
session.history_prefix_mode = "none"
session.needs_fresh_session = True
session.pending_continuation = True
session.pending_continuation_prompt = (
"Continue the task exactly where you left off; the session summary was reduced "
"this turn, rely on the visible conversation.")
logger.warning(f"Agent {session_id}: provider content-policy block (sent={p_sent}); narrowing the recap {p_mode} -> {session.history_prefix_mode} and retrying")
logger.warning(f"Agent {session_id}: provider content-policy block on a turn carrying a {p_sent} history prefix; retrying with {session.history_prefix_mode}")
return
# The subscription lane declined and nothing is left to strip; fleet data says the same request passes on an API key (0 of 328 vs 4.4%), so a user who connected their own Anthropic key continues there, told in one line, instead of losing the ask (ENG-383).
p_twin = api_key_twin_model(session.model or "", load_settings())
@@ -1,84 +0,0 @@
"""A blocked chat retried, blocked, retried, blocked (Ken, 2026-08-31; envelopes 2026-08-30).
The ratchet keyed on `history_prefix_sent`, which is an OUTCOME: RunOptions sets it to "none" at the
top of every turn and only overwrites it when a fresh-session rebuild actually attaches a recap. On a
RESUMED turn nothing is attached, so it reads "none" while the declared mode is still "minimal".
The handler read that "none" as "nothing left to strip", skipped the ladder, and went straight to the
terminal card WITHOUT narrowing anything. The next rebuild therefore sent a recap again and blocked
again. The fleet shows the impossible sequence that proves it: one session, three blocks, sent going
none -> minimal -> none, when ENG-399 says a session ratcheted to none is never widened back.
Rule this restores: never key a guard on an incidental fact; use the DECLARED signal.
"""
import pytest
from backend.apps.agents.core.models import AgentSession
from backend.apps.agents.manager.run.RunOptions import PREFIX_NARROWNESS
def p_session(mode, sent):
s = AgentSession(name="t", model="sonnet", dashboard_id="d")
s.history_prefix_mode = mode
s.history_prefix_sent = sent
return s
def test_sent_is_an_outcome_not_the_state():
"""The field the old code trusted defaults to 'none' on a turn that simply resumed."""
import inspect
from backend.apps.agents.manager.run import RunOptions
src = inspect.getsource(RunOptions)
assert 'session.history_prefix_sent = "none"' in src, (
"if this default ever goes away, the bug's premise changes and this file should be re-read"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mode,expected", [("minimal", "summary"), ("summary", "none")])
async def test_a_block_on_a_RESUMED_turn_still_narrows(monkeypatch, mode, expected):
"""sent='none' (nothing attached) while the mode is wider: the ladder MUST still step."""
from backend.apps.agents.manager.run import handle_run_error as mod
from backend.apps.agents.manager.streaming.state import TurnState
async def p_send(*a, **k):
return None
monkeypatch.setattr(mod.ws_manager, "send_to_session", p_send)
monkeypatch.setattr(mod, "p_report_model_error", lambda *a, **k: None)
s = p_session(mode, "none")
err = RuntimeError(
'API Error: 400 {"error":{"message":"Output blocked as it seems to violate our '
'Acceptable Use Policy (legal/aup): reverse engineering or duplicating model outputs"}}'
)
await mod.handle_run_error(err, s, s.id, TurnState(), [])
assert s.history_prefix_mode == expected, "the declared mode must narrow one step"
assert s.pending_continuation is True, "and the narrowed retry must actually be armed"
assert [m for m in s.messages if m.role == "system"] == [], "no terminal card while the ladder has room"
@pytest.mark.asyncio
async def test_only_an_EXHAUSTED_ladder_reaches_the_terminal_card(monkeypatch):
from backend.apps.agents.manager.run import handle_run_error as mod
from backend.apps.agents.manager.streaming.state import TurnState
async def p_send(*a, **k):
return None
monkeypatch.setattr(mod.ws_manager, "send_to_session", p_send)
monkeypatch.setattr(mod, "p_report_model_error", lambda *a, **k: None)
monkeypatch.setattr(mod, "api_key_twin_model", lambda *a, **k: None) # no own key, like the field
s = p_session("none", "none")
await mod.handle_run_error(RuntimeError("blocked ... legal/aup ... duplicating model outputs"),
s, s.id, TurnState(), [])
cards = [m for m in s.messages if m.role == "system"]
assert cards, "with nothing left to strip the user is owed the honest card"
assert "start a fresh chat" in cards[-1].content.lower()
assert s.pending_continuation is False, "and it must NOT keep retrying a request that cannot pass"
def test_the_ladder_only_ever_narrows():
assert PREFIX_NARROWNESS == ("minimal", "summary", "none")
for i, mode in enumerate(PREFIX_NARROWNESS[:-1]):
assert PREFIX_NARROWNESS.index(PREFIX_NARROWNESS[i + 1]) > PREFIX_NARROWNESS.index(mode)