



















@@ -23,6 +23,7 @@ import {
2323} from "./model-fallback.js";
2424import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js";
2525import type { EmbeddedPiRunResult } from "./pi-embedded-runner/types.js";
26+import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js";
2627import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js";
27282829vi.mock("../infra/file-lock.js", () => ({
@@ -768,6 +769,106 @@ describe("runWithModelFallback", () => {
768769expect(run).toHaveBeenCalledTimes(1);
769770});
770771772+it("aborts the fallback chain on embedded session takeover instead of trying every model (#83510)", async () => {
773+const cfg = makeCfg({
774+agents: {
775+defaults: {
776+model: {
777+primary: "openai/gpt-5.4",
778+fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-4.1-mini"],
779+},
780+},
781+},
782+});
783+const takeoverError = new Error(
784+"session file changed while embedded prompt lock was released: /tmp/session.jsonl",
785+);
786+takeoverError.name = "EmbeddedAttemptSessionTakeoverError";
787+const run = vi.fn().mockRejectedValue(takeoverError);
788+789+await expect(
790+runWithModelFallback({
791+ cfg,
792+provider: "openai",
793+model: "gpt-5.4",
794+ run,
795+}),
796+).rejects.toBe(takeoverError);
797+expect(run).toHaveBeenCalledTimes(1);
798+});
799+800+it("aborts the fallback chain on session write-lock timeout instead of trying every model (#83510)", async () => {
801+const cfg = makeCfg({
802+agents: {
803+defaults: {
804+model: {
805+primary: "openai/gpt-5.4",
806+fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-4.1-mini"],
807+},
808+},
809+},
810+});
811+const lockError = new SessionWriteLockTimeoutError({
812+timeoutMs: 10_000,
813+owner: "pid=37121",
814+lockPath: "/tmp/openclaw/session.jsonl.lock",
815+});
816+const run = vi.fn().mockRejectedValue(lockError);
817+818+await expect(
819+runWithModelFallback({
820+ cfg,
821+provider: "openai",
822+model: "gpt-5.4",
823+ run,
824+}),
825+).rejects.toBe(lockError);
826+expect(run).toHaveBeenCalledTimes(1);
827+});
828+829+it("keeps provider failover metadata authoritative over nested session locks", async () => {
830+const cfg = makeCfg({
831+agents: {
832+defaults: {
833+model: {
834+primary: "openai/gpt-5.4",
835+fallbacks: ["anthropic/claude-sonnet-4-6"],
836+},
837+},
838+},
839+});
840+const lockError = new SessionWriteLockTimeoutError({
841+timeoutMs: 10_000,
842+owner: "pid=37121",
843+lockPath: "/tmp/openclaw/session.jsonl.lock",
844+});
845+const providerError = {
846+status: 429,
847+code: "RESOURCE_EXHAUSTED",
848+message: "upstream quota pressure",
849+cause: lockError,
850+};
851+const run = vi.fn().mockRejectedValueOnce(providerError).mockResolvedValueOnce("fallback ok");
852+853+const result = await runWithModelFallback({
854+ cfg,
855+provider: "openai",
856+model: "gpt-5.4",
857+ run,
858+});
859+860+expect(result.result).toBe("fallback ok");
861+expect(result.provider).toBe("anthropic");
862+expect(run).toHaveBeenCalledTimes(2);
863+expect(result.attempts[0]).toMatchObject({
864+provider: "openai",
865+model: "gpt-5.4",
866+reason: "rate_limit",
867+status: 429,
868+code: "RESOURCE_EXHAUSTED",
869+});
870+});
871+771872it("keeps raw provider schema errors in fallback summaries", async () => {
772873const cfg = makeCfg({
773874agents: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。