Skip to content

Replace a model attempt whose connection dropped mid-response (ADR 0008 amendment) - #76

Merged
wilke merged 1 commit into
chian:mainfrom
wilke:fix/replace-retryable-failed-attempt
Oct 8, 2026
Merged

wilke merged 1 commit into
chian:mainfrom
wilke:fix/replace-retryable-failed-attempt

Conversation

@wilke

@wilke wilke commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Failure

This was the third live refiner build on Argo (2026-10-08, build_request_e25c9514…). The Materialization Implementer's streamed call failed after 164 s:

APIError … provider_error.type = RemoteProtocolError, http_status = None, retryable = True
"peer closed connection without sending complete message body (incomplete chunked read)"

The route's frozen recovery policy was retry_on_healthy_probe with max_replacements: 1. However, ADR 0008 replacement only covers attempts that go silent. An attempt that raises went straight to physical_attempt_failed → ModelCallFailed ("no alternate route was selected"), and the build ended interrupted 3 minutes after starting.

The same Argo drop also hit the chat coding adapter earlier today (fixed there in #75). Argo drops long streamed responses intermittently, so one replacement turns a lost build into a short delay.

Change

  • agent/model_call_recovery.py: when the primary attempt raises, the supervisor calls a new replace_failed_primary(). It replaces the attempt only if all of the following hold:

    • the policy mode is retry_on_healthy_probe;
    • the failure is a dropped connection with no provider verdict: retryable, http_status is None, and the transport error type is in {RemoteProtocolError, ReadError, WriteError, ConnectError, ReadTimeout, APIConnectionError};
    • the same max_replacements budget used for silent calls isn't exhausted;
    • the call isn't cancelled.

    A replacement closes the failed attempt, backs off (5 s × attempt, capped by recovery_grace_seconds), and starts a new physical attempt on the same route. It's recorded as recovery_replacement_proposed with basis retryable_transport_failure and duplicate_server_work_possible: true. Only one result is ever admitted, as before.

  • Unchanged:

    • provider error responses (an HTTP status such as 503 "overloaded", or an error event in the stream) still fail with no retry, which test_episode_model_failure_reporting requires ("Reporting must not introduce a retry");
    • preserve/disabled modes;
    • routes without a recovery record (legacy).
  • ADR 0008: adds a dated amendment describing this case.

Tests

tests/agent/test_model_call_failed_attempt_replacement.py, 7 tests:

  • a dropped stream is replaced and the result admitted;
  • the budget is respected;
  • preserve and disabled keep failing;
  • non-retryable errors aren't replaced;
  • a 503 provider verdict isn't retried;
  • legacy routes keep their behaviour.

Unchanged and passing: test_episode_model_failure_reporting.py (5) and test_launch_transport_streams.py (4).

Note

The chat coding adapter (#75) has its own small retry loop, because Implementer coding sessions don't go through this supervisor. ADR 0008 prefers a single shared policy, so a follow-up could route coding-model calls through supervise_model_call.

🤖 Generated with Claude Code

…08 amendment)

Third live refiner build on Argo (2026-10-08, build e25c9514): the
Materialization Implementer's streamed call failed after 164 s with
"peer closed connection without sending complete message body (incomplete
chunked read)" (RemoteProtocolError, http_status None, classified
retryable). Its route's recovery policy was retry_on_healthy_probe with
max_replacements 1, but ADR 0008 replacement covered only silent calls, so
the supervisor raised and the whole build stopped.

The supervisor now also replaces a physical attempt that failed because the
connection dropped with no provider verdict (RemoteProtocolError, read/write/
connect errors, no HTTP status), under retry_on_healthy_probe and within the
same max_replacements budget, after a short backoff, recorded as
recovery_replacement_proposed with basis retryable_transport_failure.
Provider error responses (HTTP status such as 503, or a stream error event)
still fail without a retry, as test_episode_model_failure_reporting requires;
preserve/disabled modes and legacy routes are unchanged. ADR 0008 amended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wilke
wilke merged commit 9666de3 into chian:main Oct 8, 2026
33 of 52 checks passed
wilke added a commit that referenced this pull request Oct 8, 2026
…er, refiner status (#77)

A refiner build on Argo stalled for hours (2026-10-08): the host left the ANL
VPN mid-request, the provider socket became half-open, and the client had no
read timeout. Every ADR 0008 health probe then timed out before any response,
which the policy treats as "preserve the original" — indefinitely — and
/build status showed model_wait: null because refiner calls bypass the
Builder's in-process wait tracker.

- agent/provider_http.py: provider clients get TCP keepalive (~2 min to declare
  a vanished peer) and a 30 s connect timeout; reads stay unbounded so long
  silent generations remain legal. Used by the launch transport and the chat
  coding adapter. A dead connection now fails as a read error, which the
  dropped-transport replacement (#76) handles.
- model_call_recovery: a silent call records call_waiting every 120 s (idle
  seconds, phase, probe state), so a stall is visible and a stopped supervisor
  is distinguishable from a waiting one. Three consecutive health probes that
  get no response at all (connect failure, or a timeout before response
  headers) end the call with provider_unreachable instead of preserving it
  forever; a probe that received headers resets the count.
- /build status: while refining, model_wait describes the build's newest
  model call from the ledger (episode, phase, idle seconds, attempt,
  endpoint, response_seen), via a new DuetStore.events_of_type cross-Duet read
  of model_launch_call events carrying owner_duet_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant