@@ -1138,35 +1138,13 @@ async function consumeDaemonRun({
11381138 const data = event . data as SseErrorPayload ;
11391139 const structuredError = daemonSseError ( data ) ;
11401140 pendingStructuredError = structuredError ;
1141- // The daemon emits this error frame from the child-close handler
1142- // BEFORE `finishWithRetryDecision()` runs, so a transient failure it
1143- // can recover via a same-run retry is reported here first and only
1144- // resolved later. `run.resumable` is also computed at that same
1145- // finalize step. Read the run status ONCE to classify, and let the
1146- // SSE `end` frame (always emitted on terminal) resolve in-flight
1147- // runs — this has no timeout, so even a slow retry is handled:
1148- // - failed / canceled -> surface the error now, with the
1149- // finalized `resumable` bit (set just before status flips to
1150- // failed, so a `failed` read already has it);
1151- // - status unreachable -> surface the structured error (safe
1152- // default; never drop a real failure);
1153- // - succeeded (recovered) or still running/queued (retry in
1154- // flight) -> do NOT surface; keep consuming so the stream's
1155- // `end` frame resolves it (succeeded -> onDone; failed ->
1156- // the failure path below, carrying `end`'s resumable bit).
1157- const status = await fetchChatRunStatus ( runId ) . catch ( ( ) => null ) ;
1158- if ( status && ( status . status === 'failed' || status . status === 'canceled' ) ) {
1159- onRunStatus ?.( 'failed' ) ;
1160- handlers . onError (
1161- markErrorResumable ( structuredError , status . resumable === true ) ,
1162- ) ;
1163- return ;
1164- }
1165- if ( ! status ) {
1166- onRunStatus ?.( 'failed' ) ;
1167- handlers . onError ( structuredError ) ;
1168- return ;
1169- }
1141+ // Error frames can be emitted for a failed first attempt before the
1142+ // same run's retry has completed. Do not classify the run from a
1143+ // point-in-time status probe here: that can catch a transient
1144+ // failed state, surface a stale error, and disconnect before the
1145+ // later successful retry frames arrive. Cache the structured error
1146+ // and let the terminal `end` event or the post-stream status
1147+ // fallback below decide whether it should be surfaced.
11701148 continue ;
11711149 }
11721150
@@ -1184,7 +1162,30 @@ async function consumeDaemonRun({
11841162 }
11851163 }
11861164 }
1187- reconnects = sawStreamProgress ? 0 : reconnects + 1 ;
1165+ let shouldResetReconnects = sawStreamProgress ;
1166+ if ( pendingStructuredError && endStatus === null ) {
1167+ const status = await fetchChatRunStatus ( runId ) . catch ( ( ) => null ) ;
1168+ if ( status && isChatRunStatus ( status . status ) && status . status !== 'queued' && status . status !== 'running' ) {
1169+ endStatus = status . status ;
1170+ exitCode = status . exitCode ?? null ;
1171+ exitSignal = status . signal ?? null ;
1172+ serverDeclaredSuccess = status . status === 'succeeded' ;
1173+ if ( status . resumable === true ) endResumable = true ;
1174+ onRunStatus ?.( endStatus ) ;
1175+ break ;
1176+ }
1177+ if ( ! status ) {
1178+ onRunStatus ?.( 'failed' ) ;
1179+ handlers . onError ( pendingStructuredError ) ;
1180+ return ;
1181+ }
1182+ // The connection closed after an error frame but before a terminal
1183+ // frame. If the run is still active, retry the SSE stream, but count
1184+ // this as a reconnect attempt instead of letting the error frame reset
1185+ // the budget forever.
1186+ shouldResetReconnects = false ;
1187+ }
1188+ reconnects = shouldResetReconnects ? 0 : reconnects + 1 ;
11881189 }
11891190
11901191 if ( endStatus === null ) {
0 commit comments