[eric] agents: an unclassified turn failure arms a fresh CLI session, so a chat bricked on "hit a snag" heals on the user's own next send instead of needing a manual branch (ENG-258)

This commit is contained in:
ciregenz
2026-08-11 19:43:54 -07:00
parent 54c336f5bc
commit a28268bab9
2 changed files with 51 additions and 0 deletions
@@ -304,6 +304,14 @@ async def handle_run_error(e: Exception, session: AgentSession, session_id: str,
else:
# Track unclassified agent failures too so we stop flying blind on them.
p_report_model_error("unclassified", session_id, session, turn, e, p_stderr_tail)
# Every classified branch above is an EXTERNAL fact (auth, credits, capacity, certs) that a
# respawn cannot fix. Landing here instead means the failure may live in this session's own
# CLI state, and that state is replayed verbatim on every retry, which is how one chat bricks
# forever on "hit a snag" while its siblings are fine (ENG-258). Arm the proven fresh-session
# rebuild so the next ordinary send drops the resume transcript and respawns the client; the
# user's own retry becomes the cure instead of needing a manual branch.
session.needs_fresh_session = True
logger.info(f"Agent {session_id}: unclassified failure, next turn rebuilds on a fresh CLI session")
# The SDK's ProcessError masks the cause behind "Check stderr output for details"; append the scrubbed stderr tail so the card (and its analytics copy) names what actually broke instead of shipping a dead end.
p_card_text = f"Error: {str(e)}"
p_cause = redact_for_telemetry(p_stderr_tail, limit=400).strip()
+43
View File
@@ -83,3 +83,46 @@ def test_informative_error_does_not_get_stderr_appended(monkeypatch):
session, _ = p_drive_error(monkeypatch, exc, stderr=["irrelevant tail"])
card = [m for m in session.messages if m.role == "system"][-1].content
assert "Runtime log tail" not in card
# ENG-258: one session bricking on "hit a snag" forever while its siblings run fine. Every classified
# branch is an external fact a respawn can't fix; the unclassified bucket is the one that can be this
# session's own poisoned CLI state, replayed identically on every retry. Assert BOTH directions, or
# "it arms" would pass just as well on a version that arms unconditionally.
def test_unclassified_failure_arms_a_fresh_session_so_the_next_send_self_heals(monkeypatch):
session, _ = p_drive_error(
monkeypatch, Exception("API Error: 400 invalid_request_error: messages.3: unexpected block")
)
assert session.needs_fresh_session is True
def test_a_dead_resume_transcript_does_not_stay_sticky(monkeypatch):
# The real shape of a session bricked by its own CLI state: the resume id no longer resolves, so
# every retry replays the same doomed resume until something drops it.
session, _ = p_drive_error(
monkeypatch, Exception("No conversation found with session ID: 9f3c1a2b-dead-4f00-bbbb-000000000000")
)
assert session.needs_fresh_session is True
def test_out_of_credits_does_not_respawn_the_cli(monkeypatch):
session, _ = p_drive_error(
monkeypatch, Exception("Your credit balance is too low to run this request")
)
assert session.needs_fresh_session is False
def test_auth_failure_does_not_respawn_the_cli(monkeypatch):
session, _ = p_drive_error(monkeypatch, Exception("401 invalid authentication credentials"))
assert session.needs_fresh_session is False
def test_rate_limit_does_not_respawn_the_cli(monkeypatch):
session, _ = p_drive_error(monkeypatch, Exception("429 rate_limit_error: overloaded"))
assert session.needs_fresh_session is False
def test_missing_cli_binary_does_not_respawn_the_cli(monkeypatch):
session, _ = p_drive_error(monkeypatch, Exception(P_FIELD_CLI_MISSING))
assert session.needs_fresh_session is False