

















@@ -20,6 +20,8 @@ import { isSessionWriteLockAcquireError } from "./session-write-lock-error.js";
20202121const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
2222const MAX_FAILOVER_CAUSE_DEPTH = 25;
23+const MISSING_TOOL_RESULT_REASON = "missing_tool_result";
24+const MISSING_TOOL_RESULT_TEXT_RE = /native Codex tool\.call without a matching tool\.result/i;
23252426/** 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}
346348349+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+) {
356408return false;
357409}
358410if (isFailoverError(err)) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。