1818 * observed immediately after a `permission_denied` tool-result on a compound Bash command.
1919 * It is retried as a fresh run (not `--continue`, which is permanently disabled for the rest
2020 * of the driver invocation) since resuming would resend the same corrupted session state.
21- * - If the process produced no output (failed to start / auth error before any work), the
22- * driver does not retry because there is nothing to resume.
21+ * - Connection-refused failures before the first assistant response are retried as fresh
22+ * runs because there is no session state to resume.
23+ * - Other failures that produce no output use a separate bounded startup retry budget.
2324 * - On a `--continue` retry the initial prompt is omitted: Claude Code resumes the session
2425 * from its on-disk state rather than re-processing the original instructions.
2526 * - Retries use exponential backoff: 5s → 10s → 20s (capped at 60s) by default.
@@ -81,6 +82,7 @@ const RATE_LIMIT_ERROR_PATTERN = /rate_limit_error|429 Too Many Requests|"api_er
8182// run rather than --continue, since resuming would resend the same corrupted
8283// session state and reproduce the identical error.
8384const INVALID_JSON_BODY_ERROR_PATTERN = / r e q u e s t b o d y i s n o t v a l i d J S O N / i;
85+ const CONNECTION_REFUSED_ERROR_PATTERN = / c o n n e c t i o n r e f u s e d | E C O N N R E F U S E D / i;
8486
8587// Pattern to detect a clean max-turns exit from Claude Code.
8688// Claude Code emits a JSON result object with "subtype":"error_max_turns" when the
@@ -190,6 +192,25 @@ function isInvalidJsonBodyError(output) {
190192 return INVALID_JSON_BODY_ERROR_PATTERN . test ( output ) ;
191193}
192194
195+ /**
196+ * Determines if the collected output contains a refused network connection.
197+ * @param {string } output - Collected stdout+stderr from the process
198+ * @returns {boolean }
199+ */
200+ function isConnectionRefusedError ( output ) {
201+ return CONNECTION_REFUSED_ERROR_PATTERN . test ( output ) ;
202+ }
203+
204+ /**
205+ * Determines whether Claude produced an assistant response before failing.
206+ * System initialization and transport-error events do not represent resumable work.
207+ * @param {string } output - Collected stdout+stderr from the process
208+ * @returns {boolean }
209+ */
210+ function hasClaudeSessionProgress ( output ) {
211+ return output . split ( / \r ? \n / ) . some ( line => / " t y p e " \s * : \s * " a s s i s t a n t " / . test ( line ) ) ;
212+ }
213+
193214/**
194215 * Determines if the collected output contains a "no deferred tool marker" error.
195216 * This occurs when Claude Code is invoked with --continue but the session was never
@@ -425,6 +446,12 @@ async function main() {
425446 let useContinueOnRetry = false ;
426447 let continueDisabledPermanently = false ;
427448 let startupRetriesUsed = 0 ;
449+ // Tracks whether the *active session* (the run currently being resumed via --continue)
450+ // has ever produced an assistant response. This must persist across attempts — a later
451+ // --continue attempt can fail during its own startup (e.g. connection refused before it
452+ // emits anything) even though earlier attempts in the same session already made progress.
453+ // Reset only when a genuinely fresh run begins (see below), never on a --continue attempt.
454+ let sessionHasProgress = false ;
428455 const driverStartTime = Date . now ( ) ;
429456 // Soft-timeout guard: polled at the top of the retry loop and after each backoff sleep.
430457 // It does not preempt a running attempt — if a single invocation runs past the soft
@@ -447,6 +474,10 @@ async function main() {
447474 currentArgs = [ ...continueBaseArgs , "--continue" ] ;
448475 } else {
449476 currentArgs = attempt === 0 ? initialArgs : freshRetryArgs ;
477+ // This attempt starts a brand-new session (either attempt 0, or a fresh
478+ // retry that discards prior on-disk state) — no assistant progress can carry
479+ // forward from any earlier attempt, so reset the tracker.
480+ sessionHasProgress = false ;
450481 }
451482
452483 // Use redacted args for logging when the run carries the prompt text.
@@ -482,6 +513,11 @@ async function main() {
482513 const isNoDeferredMarker = isNoDeferredMarkerError ( result . output ) ;
483514 const isInvalidModel = isInvalidModelError ( result . output ) ;
484515 const isInvalidJsonBody = isInvalidJsonBodyError ( result . output ) ;
516+ const isConnectionRefused = isConnectionRefusedError ( result . output ) ;
517+ // Accumulate across attempts of the same session: once an assistant response has been
518+ // observed, it stays true for the remainder of this session's --continue attempts, even
519+ // if a later attempt's own output contains nothing but startup/transport errors.
520+ sessionHasProgress = sessionHasProgress || hasClaudeSessionProgress ( result . output ) ;
485521 const permissionDeniedCount = countPermissionDeniedIssues ( result . output ) ;
486522 const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues ( result . output ) ;
487523 log (
@@ -494,6 +530,8 @@ async function main() {
494530 ` isNoDeferredMarkerError=${ isNoDeferredMarker } ` +
495531 ` isInvalidModelError=${ isInvalidModel } ` +
496532 ` isInvalidJsonBodyError=${ isInvalidJsonBody } ` +
533+ ` isConnectionRefusedError=${ isConnectionRefused } ` +
534+ ` sessionHasProgress=${ sessionHasProgress } ` +
497535 ` permissionDeniedCount=${ permissionDeniedCount } ` +
498536 ` hasNumerousPermissionDenied=${ hasNumerousPermissionDenied } ` +
499537 ` hasOutput=${ result . hasOutput } ` +
@@ -598,6 +636,21 @@ async function main() {
598636 break ;
599637 }
600638
639+ // A refused connection before Claude produces an assistant response means the API
640+ // proxy path was unavailable during startup. There is no session state to resume, so
641+ // retry the original prompt as a fresh run with the normal exponential backoff.
642+ // sessionHasProgress reflects the whole session, not just this attempt's output, so a
643+ // later --continue attempt that fails during its own startup (no assistant line of its
644+ // own) is still correctly treated as mid-session rather than cold-start.
645+ if ( isConnectionRefused && ! sessionHasProgress && attempt < maxRetries ) {
646+ // Reset to fresh-run mode. No session state carries forward because Claude Code
647+ // never produced an assistant response for this session — the original prompt args
648+ // (initialArgs/freshRetryArgs) are reused unchanged on the next attempt.
649+ useContinueOnRetry = false ;
650+ log ( `attempt ${ attempt + 1 } : connection refused before first assistant response — retrying as fresh run with backoff (attempt ${ attempt + 2 } /${ maxRetries + 1 } )` ) ;
651+ continue ;
652+ }
653+
601654 // Retry when the session was partially executed (has output).
602655 // Use --continue so Claude Code can resume from its saved session state.
603656 if ( attempt < maxRetries && result . hasOutput ) {
@@ -664,6 +717,8 @@ if (typeof module !== "undefined" && module.exports) {
664717 isNoDeferredMarkerError,
665718 isInvalidModelError,
666719 isInvalidJsonBodyError,
720+ isConnectionRefusedError,
721+ hasClaudeSessionProgress,
667722 isSignalTerminationExitCode,
668723 shouldRetryWithContinue,
669724 countPermissionDeniedIssues,
0 commit comments