From 1db307f5f502219653b917bc2ff11286f8ffaa05 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 31 Aug 2026 12:24:31 -0700 Subject: [PATCH] [eric] agents: the exhausted-respawn 400 cards the router restart honestly instead of blaming the model --- .../agents/manager/run/handle_run_error.py | 27 +++++++++++++++++++ .../tests/test_stale_tool_schema_respawn.py | 9 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/apps/agents/manager/run/handle_run_error.py b/backend/apps/agents/manager/run/handle_run_error.py index 9e895cb7..af1566b3 100644 --- a/backend/apps/agents/manager/run/handle_run_error.py +++ b/backend/apps/agents/manager/run/handle_run_error.py @@ -172,6 +172,33 @@ async def handle_run_error(e: Exception, session: AgentSession, session_id: str, ) return logger.warning(f"Agent {session_id}: deferred-tool 400 again after a respawn; carding it") + # The generic card below says "switch to another model", which for THIS failure blames the + # wrong thing: the model was fine, our router was restarting. Say what happened and what + # actually works (a plain resend once it settles; proven live on the same transcript). + friendly_msg = ( + "The app's model router was restarting when this chat connected, and the automatic " + "retry hit the same window. It settles within a minute; send your message again and " + "the chat picks up where it left off." + ) + error_msg = Message(role="system", content=friendly_msg, branch_id=session.active_branch_id) + absorb_repeat_card(session, error_msg) + await ws_manager.send_to_session(session_id, "agent:message", { + "session_id": session_id, + "message": error_msg.model_dump(mode="json"), + }) + try: + from backend.apps.service.client import submit_diagnostic + submit_diagnostic({ + "kind": "model_error", + "subkind": "stale_tool_schema_respawn_exhausted", + "flight": flight_recorder.build_envelope(session_id, "model_error", "stale_tool_schema", session.model, "stream" if turn.current_turn_emitted else "spawn", -1), + "session_id": session_id, + "model": session.model, + "error_preview": redact_for_telemetry(str(e), limit=400), + }) + except Exception: + logger.debug("submit_diagnostic stale_tool_schema failed", exc_info=True) + return if is_context_overflow_error(e, extra_text=p_stderr_tail): p_tier_gate = is_long_context_error(e, extra_text=p_stderr_tail) diff --git a/backend/tests/test_stale_tool_schema_respawn.py b/backend/tests/test_stale_tool_schema_respawn.py index c6c4cab8..99fdde85 100644 --- a/backend/tests/test_stale_tool_schema_respawn.py +++ b/backend/tests/test_stale_tool_schema_respawn.py @@ -156,4 +156,11 @@ async def test_the_second_identical_400_does_card(monkeypatch): s.stale_tool_schema_retry_used = True # the one-shot is already spent await mod.handle_run_error(RuntimeError(REAL), s, s.id, TurnState(), []) - assert [m for m in s.messages if m.role == "system"], "a spent budget must produce an honest card" + cards = [m for m in s.messages if m.role == "system"] + assert cards, "a spent budget must produce an honest card" + # The card must blame the actual culprit (our router restarting), never the model, and must + # promise the thing that is proven to work (a plain resend on the same chat). + text = cards[-1].content + assert "router" in text.lower(), text + assert "send your message again" in text.lower(), text + assert "switch" not in text.lower(), "the generic switch-models advice blames the wrong thing"