From d751148dd0c0f10cf04a955929010309db2321bc Mon Sep 17 00:00:00 2001 From: Qian-Cheng-nju Date: Fri, 9 Oct 2026 12:16:13 +0000 Subject: [PATCH] Honor Claude Code terminal error flags --- src/evaluator/termination.py | 7 ++--- tests/evaluator/test_termination.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/evaluator/termination.py b/src/evaluator/termination.py index 7bea49c2..99dca5c8 100644 --- a/src/evaluator/termination.py +++ b/src/evaluator/termination.py @@ -240,8 +240,9 @@ def claude_code_result_error(ctx: TerminationContext) -> str | None: Background work can produce several ``result`` events. The final result must follow the last conversation activity; an earlier success cannot finalize a truncated follow-up turn. Malformed streams and execution-error - results are INFRA_ERROR. ``error_max_turns`` remains a turn-budget LIMIT, - not infrastructure, when it closes the stream. + results are INFRA_ERROR, including ``is_error`` on a ``success`` subtype. + ``error_max_turns`` remains a turn-budget LIMIT, not infrastructure, when + it closes the stream. """ if ctx.backend != "claude_code": return None @@ -261,7 +262,7 @@ def claude_code_result_error(ctx: TerminationContext) -> str | None: # No result closes the latest activity, including background turns. return TerminationReason.INFRA_ERROR subtype = last_result.get("subtype", "") - if subtype.startswith("error") and subtype != "error_max_turns": + if subtype != "error_max_turns" and (last_result.get("is_error") is True or subtype.startswith("error")): return TerminationReason.INFRA_ERROR return None diff --git a/tests/evaluator/test_termination.py b/tests/evaluator/test_termination.py index 392ebb4f..0cd3272c 100644 --- a/tests/evaluator/test_termination.py +++ b/tests/evaluator/test_termination.py @@ -17,6 +17,7 @@ from evaluator.backends import get_backend from evaluator.backends.agentic import AgenticBackend +from evaluator.score import is_non_genuine from evaluator.termination import ( INFRA_RULES, TerminationContext, @@ -1019,6 +1020,47 @@ def test_cc_exec_error_is_infra(tmp_path): assert classify(_ctx(p, backend="claude_code")) == TerminationReason.INFRA_ERROR +@pytest.mark.parametrize("agent_exit", [0, 1]) +def test_cc_success_subtype_with_error_flag_is_infra(tmp_path, agent_exit): + events = [ + *CC_SUCCESS, + {"type": "system", "subtype": "task_notification"}, + {"type": "rate_limit_event", "rate_limit_info": {"status": "rejected", "rateLimitType": "five_hour"}}, + { + "type": "assistant", + "error": "rate_limit", + "message": {"content": [{"type": "text", "text": "You've hit your session limit"}]}, + }, + {"type": "result", "subtype": "success", "is_error": True, "stop_reason": "stop_sequence"}, + ] + p = _write_jsonl(tmp_path / "quota-after-background.jsonl", events) + reason = classify(_ctx(p, backend="claude_code", agent_exit=agent_exit)) + assert reason == TerminationReason.INFRA_ERROR + assert is_non_genuine({"termination_reason": reason, "check_verdict": "TIMEOUT"}) + + +def test_cc_error_flag_does_not_require_quota_text(tmp_path): + p = _write_jsonl(tmp_path / "flagged-error.jsonl", [{"type": "result", "subtype": "success", "is_error": True}]) + assert classify(_ctx(p, backend="claude_code")) == TerminationReason.INFRA_ERROR + + +def test_cc_error_flag_recovered_by_later_success_is_ok(tmp_path): + events = [ + {"type": "result", "subtype": "success", "is_error": True}, + *CC_SUCCESS, + ] + p = _write_jsonl(tmp_path / "recovered-error-flag.jsonl", events) + assert classify(_ctx(p, backend="claude_code")) == TerminationReason.OK + + +def test_cc_runner_timeout_precedes_result_error_flag(tmp_path): + p = _write_jsonl( + tmp_path / "timeout-error-flag.jsonl", [{"type": "result", "subtype": "success", "is_error": True}] + ) + ctx = _ctx(p, backend="claude_code", agent_exit=-1, error="claude_code timeout after 7200s") + assert classify(ctx) == TerminationReason.TIMEOUT + + def test_cc_max_turns_is_not_infra(tmp_path): # error_max_turns is a turn-budget LIMIT, not infrastructure — must NOT flag. p = _write_jsonl(tmp_path / "maxturns.jsonl", CC_MAX_TURNS)