@@ -20,6 +20,8 @@ import { isSessionWriteLockAcquireError } from "./session-write-lock-error.js";
2020
2121const ABORT_TIMEOUT_RE = / r e q u e s t w a s a b o r t e d | r e q u e s t a b o r t e d / i;
2222const MAX_FAILOVER_CAUSE_DEPTH = 25 ;
23+ const MISSING_TOOL_RESULT_REASON = "missing_tool_result" ;
24+ const MISSING_TOOL_RESULT_TEXT_RE = / n a t i v e C o d e x t o o l \. c a l l w i t h o u t a m a t c h i n g t o o l \. r e s u l t / i;
2325
2426/** Structured error used to carry model fallback/failover metadata across layers. */
2527export class FailoverError extends Error {
@@ -344,15 +346,65 @@ function hasEmbeddedAttemptSessionTakeover(err: unknown, seen: Set<object> = new
344346 ) ;
345347}
346348
349+ function readField ( value : unknown , key : string ) : unknown {
350+ if ( ! value || typeof value !== "object" ) {
351+ return undefined ;
352+ }
353+ return ( value as Record < string , unknown > ) [ key ] ;
354+ }
355+
356+ function readStringField ( value : unknown , key : string ) : string | undefined {
357+ const field = readField ( value , key ) ;
358+ return typeof field === "string" ? field : undefined ;
359+ }
360+
361+ function isMissingToolResultMessage ( value : string ) : boolean {
362+ return MISSING_TOOL_RESULT_TEXT_RE . test ( value ) ;
363+ }
364+
365+ function isMissingToolResultMarker ( value : string ) : boolean {
366+ return value . trim ( ) === MISSING_TOOL_RESULT_REASON ;
367+ }
368+
369+ function readMissingToolResultMarker ( err : unknown ) : true | undefined {
370+ const message = readDirectErrorMessage ( err ) ;
371+ if ( message && isMissingToolResultMessage ( message ) ) {
372+ return true ;
373+ }
374+ for ( const key of [ "code" , "reason" , "status" ] as const ) {
375+ const value = readStringField ( err , key ) ;
376+ if ( value && isMissingToolResultMarker ( value ) ) {
377+ return true ;
378+ }
379+ }
380+ const output = readStringField ( err , "output" ) ;
381+ if ( output && isMissingToolResultMessage ( output ) ) {
382+ return true ;
383+ }
384+ const resultReason = readStringField ( readField ( err , "result" ) , "reason" ) ;
385+ const detailReason = readStringField ( readField ( err , "detail" ) , "reason" ) ;
386+ if ( resultReason === MISSING_TOOL_RESULT_REASON || detailReason === MISSING_TOOL_RESULT_REASON ) {
387+ return true ;
388+ }
389+ return undefined ;
390+ }
391+
392+ function hasMissingToolResultFailure ( err : unknown ) : boolean {
393+ return findErrorProperty ( err , readMissingToolResultMarker ) === true ;
394+ }
395+
347396/**
348- * True when the error is a local runtime coordination error (session write-lock
349- * timeout or embedded attempt session takeover) rather than a provider/model
350- * failure. The model fallback chain must abort on these instead of consuming
351- * candidate slots — retrying any model would hit the same local condition.
352- * See #83510.
397+ * True when the error is a local runtime coordination/tool-execution error
398+ * rather than a provider/model failure. The model fallback chain must abort on
399+ * these instead of consuming candidate slots — retrying any model would hit the
400+ * same local condition. See #83510 and #95474.
353401 */
354402export function isNonProviderRuntimeCoordinationError ( err : unknown ) : boolean {
355- if ( ! hasSessionWriteLockContention ( err ) && ! hasEmbeddedAttemptSessionTakeover ( err ) ) {
403+ if (
404+ ! hasSessionWriteLockContention ( err ) &&
405+ ! hasEmbeddedAttemptSessionTakeover ( err ) &&
406+ ! hasMissingToolResultFailure ( err )
407+ ) {
356408 return false ;
357409 }
358410 if ( isFailoverError ( err ) ) {
0 commit comments