fix(tui): retry transport-class body/decode failures for sub-agent requests
A scout died at 141 seconds because a DeepSeek response stream failed to decode — the transient classifier's needle list covered headers, timeouts, resets, and 5xx/429 wording but not body/decode failures, so the child was classified Fatal on attempt one with zero retries and the parent had to re-plan from a runtime event (morning-report issue #7). Body-decode and truncated-response wording now classifies as transient: 'error decoding response body', the Chat API read/parse contexts, and unexpected-EOF/incomplete-message phrasing. The existing bounded policy applies unchanged — at most two same-prompt retries with 250ms backoff — and auth-class failures stay fatal, pinned by the new classifier test. Verified: subagent suite green; cargo fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8535,6 +8535,15 @@ fn is_transient_subagent_provider_error(error: &anyhow::Error) -> bool {
|
||||
"502",
|
||||
"503",
|
||||
"504",
|
||||
// Body/decode failures are transport-class: the provider accepted the
|
||||
// request and died mid-response (a DeepSeek stream decode error killed
|
||||
// a 141s scout with zero retries — morning-report issue #7). One
|
||||
// same-prompt retry is cheap next to re-planning the child.
|
||||
"error decoding response body",
|
||||
"failed to read chat api response body",
|
||||
"failed to parse chat api json",
|
||||
"unexpected end of file",
|
||||
"incomplete message",
|
||||
]
|
||||
.iter()
|
||||
.any(|needle| message.contains(needle))
|
||||
|
||||
@@ -5587,6 +5587,27 @@ fn transient_provider_classifier_matches_sse_header_timeout() {
|
||||
assert!(is_transient_subagent_provider_error(&err));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_provider_classifier_matches_body_decode_failures() {
|
||||
// A provider that accepts the request and dies mid-response is a
|
||||
// transport failure, not a fatal child error: one same-prompt retry is
|
||||
// cheap next to re-planning 141 seconds of scout work (morning-report
|
||||
// issue #7, DeepSeek stream decode).
|
||||
let decode = anyhow::anyhow!("error decoding response body")
|
||||
.context("Failed to read Chat API response body");
|
||||
assert!(is_transient_subagent_provider_error(&decode));
|
||||
|
||||
let parse = anyhow::anyhow!("expected value at line 1 column 1")
|
||||
.context("Failed to parse Chat API JSON");
|
||||
assert!(is_transient_subagent_provider_error(&parse));
|
||||
|
||||
let auth = anyhow::anyhow!("401 unauthorized: invalid api key");
|
||||
assert!(
|
||||
!is_transient_subagent_provider_error(&auth),
|
||||
"auth failures stay fatal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_provider_classifier_matches_structured_rate_limit() {
|
||||
let err = anyhow::Error::new(crate::llm_client::LlmError::RateLimited {
|
||||
|
||||
Reference in New Issue
Block a user