Skip to content

fix(runner): retry only transport failures, and only in one layer - #75

Merged
TMHSDigital merged 1 commit into
mainfrom
fix/retry-only-transport-failures
Sep 23, 2026
Merged

TMHSDigital merged 1 commit into
mainfrom
fix/retry-only-transport-failures

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Fixes #26.

The problem. The runner retried every error an adapter raised, except a refusal, up to three attempts. That included:

  • a WireContractError, whose own docstring says no retry fixes it;
  • a CheckpointMismatchError;
  • the missing optional dependency error;
  • a ValueError;
  • a 401.

Both SDKs also retried underneath (TypeSafe's default policy, Anthropic's max_retries=2), so one case could make nine billed calls, and a retried answer could be scored.

What changed

  • is_transient() in runner/execute.py decides whether an error is worth another attempt. It goes by the error's shape, so the runner imports no SDK:
    • anything plumbline raises on purpose (PlumblineError) is permanent;
    • an error with a status code is transient only for 408, 409, 425, 429 and 5xx;
    • TimeoutError, ConnectionError, and classes named *ConnectionError or *TimeoutError are transient. Anthropic's connection errors subclass neither builtin, hence the name check;
    • anything else is permanent. A recorded failure is visible, and a retry costs money.
  • Retry-After is waited for, up to 60 s.
  • A permanent failure is recorded after its first attempt, with the number of attempts it actually made.
  • SDK retries off: both adapters build their SDK client with retries disabled (RetryPolicy(max_retries=0) for TypeSafe, max_retries=0 for Anthropic). The runner is now the only retrying layer, so the artifact's attempts is the number of calls made.
  • METHODOLOGY now says what is retried and what is not.

Checked

  • New tests failed on main and pass now:
    • six permanent errors are each attempted once: WireContractError, CheckpointMismatchError, a plain PlumblineError, ValueError, 401 and 400;
    • 429, 503, ConnectionError, and an APIConnectionError-named class are retried;
    • Retry-After: 2 waits 2 s, and 600 is capped at 60;
    • both adapters build their client with retries off.
  • The review's repro now shows one call, not three, for both a WireContractError and a 401.
  • ruff, mypy --strict, and the full pytest suite pass.

🤖 Generated with Claude Code

The runner retried every error an adapter raised except a refusal, up to
three attempts: a WireContractError (whose docstring says no retry fixes
it), a CheckpointMismatchError, the missing-extra error, a ValueError,
and a 401. Both SDKs also retried underneath (TypeSafe's default policy,
Anthropic's max_retries=2), so one case could make nine billed calls,
and a retried answer could be scored.

is_transient() now decides, by the error's shape so the runner imports
no SDK: anything plumbline raises on purpose is permanent; a status code
is transient only for 408, 409, 425, 429 and 5xx; TimeoutError,
ConnectionError, and classes named *ConnectionError or *TimeoutError
(Anthropic's do not subclass the builtins) are transient; anything else
is permanent, since a recorded failure is visible and a retry costs
money. A Retry-After header is waited for, up to 60 seconds. A permanent
failure is recorded after its first attempt, with the attempts it made.

Both adapters build their SDK client with retries off, so the runner is
the only layer and the artifact's attempts is the call count. METHODOLOGY
says what is and is not retried.

Fixes #26.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@TMHSDigital
TMHSDigital merged commit 0709ae9 into main Sep 23, 2026
17 checks passed
@TMHSDigital
TMHSDigital deleted the fix/retry-only-transport-failures branch September 23, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The runner retries errors that no retry can fix, and the SDK retries again underneath

1 participant