Skip to content

Commit f95dc0a

Browse files
fix: keep session takeover out of model fallback
1 parent 395bd57 commit f95dc0a

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

src/agents/model-fallback.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,29 @@ describe("runWithModelFallback", () => {
897897
expect(errorCall.error).toBe(unknownError);
898898
});
899899

900+
it("rethrows embedded session takeover errors instead of walking model fallbacks", async () => {
901+
const cfg = makeCfg();
902+
const takeoverError = Object.assign(
903+
new Error("session file changed while embedded prompt lock was released: /tmp/session.jsonl"),
904+
{ name: "EmbeddedAttemptSessionTakeoverError" },
905+
);
906+
const run = vi.fn().mockRejectedValue(takeoverError);
907+
const onError = vi.fn();
908+
909+
await expect(
910+
runWithModelFallback({
911+
cfg,
912+
provider: "openai",
913+
model: "gpt-4.1-mini",
914+
run,
915+
onError,
916+
}),
917+
).rejects.toBe(takeoverError);
918+
919+
expect(run).toHaveBeenCalledTimes(1);
920+
expect(onError).not.toHaveBeenCalled();
921+
});
922+
900923
it("throws unrecognized error on last candidate", async () => {
901924
const cfg = makeCfg();
902925
const run = vi.fn().mockRejectedValueOnce(new Error("something weird"));

src/agents/model-fallback.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
import { isLikelyContextOverflowError } from "./pi-embedded-helpers/errors.js";
4949
import type { FailoverReason } from "./pi-embedded-helpers/types.js";
5050
import { resolveSessionSuspensionReason, suspendSession } from "./session-suspension.js";
51+
import { isSessionWriteLockTimeoutError } from "./session-write-lock-error.js";
5152

5253
const log = createSubsystemLogger("model-fallback");
5354

@@ -116,6 +117,23 @@ function shouldRethrowAbort(err: unknown): boolean {
116117
return isFallbackAbortError(err) && !isTimeoutError(err);
117118
}
118119

120+
function isEmbeddedAttemptSessionTakeoverError(err: unknown): boolean {
121+
if (!err || typeof err !== "object") {
122+
return false;
123+
}
124+
const name = "name" in err ? String(err.name) : "";
125+
if (name === "EmbeddedAttemptSessionTakeoverError") {
126+
return true;
127+
}
128+
return formatErrorMessage(err).includes(
129+
"session file changed while embedded prompt lock was released",
130+
);
131+
}
132+
133+
function shouldRethrowSessionOwnershipError(err: unknown): boolean {
134+
return isEmbeddedAttemptSessionTakeoverError(err) || isSessionWriteLockTimeoutError(err);
135+
}
136+
119137
function createModelCandidateCollector(allowlist: Set<string> | null | undefined): {
120138
candidates: ModelCandidate[];
121139
addExplicitCandidate: (candidate: ModelCandidate) => void;
@@ -231,6 +249,9 @@ async function runFallbackCandidate<T>(params: {
231249
if (isCommandLaneTaskTimeoutError(err)) {
232250
throw err;
233251
}
252+
if (shouldRethrowSessionOwnershipError(err)) {
253+
throw err;
254+
}
234255
// Normalize abort-wrapped rate-limit errors (e.g. Google Vertex RESOURCE_EXHAUSTED)
235256
// so they become FailoverErrors and continue the fallback loop instead of aborting.
236257
const normalizedFailover = coerceToFailoverError(err, {

0 commit comments

Comments
 (0)