Skip to content

Commit 016c34f

Browse files
litang9altaywtf
andauthored
Fix/codex deactivated workspace failover (#55893)
Merged via squash. Prepared head SHA: 3aa770f Co-authored-by: litang9 <141409885+litang9@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf
1 parent 1d5b5db commit 016c34f

6 files changed

Lines changed: 46 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
5151
- CLI/perf: serve `doctor`, `gateway`, `models`, and `plugins` parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.
5252
- Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.
5353
- Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.
54+
- Codex/failover: classify `deactivated_workspace` as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.
5455

5556
## 2026.5.20
5657

src/agents/failover-error.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,37 @@ describe("failover-error", () => {
943943
);
944944
});
945945

946+
it("Codex deactivated workspace marker returns auth_permanent", () => {
947+
expect(resolveFailoverReasonFromError({ message: "deactivated_workspace" })).toBe(
948+
"auth_permanent",
949+
);
950+
expect(resolveFailoverReasonFromError({ message: "deactivated workspace" })).toBe(
951+
"auth_permanent",
952+
);
953+
expect(resolveFailoverReasonFromError({ code: "deactivated_workspace" })).toBe(
954+
"auth_permanent",
955+
);
956+
expect(
957+
resolveFailoverReasonFromError({
958+
detail: { code: "deactivated_workspace" },
959+
}),
960+
).toBe("auth_permanent");
961+
expect(
962+
resolveFailoverReasonFromError({
963+
status: 403,
964+
message: "Forbidden",
965+
detail: { code: "deactivated_workspace" },
966+
}),
967+
).toBe("auth_permanent");
968+
expect(
969+
resolveFailoverReasonFromError({
970+
status: 400,
971+
message: "Bad request",
972+
detail: { code: "deactivated_workspace" },
973+
}),
974+
).toBe("auth_permanent");
975+
});
976+
946977
it("403 OpenRouter 'Key limit exceeded' returns billing (model fallback trigger)", () => {
947978
// GitHub: openclaw/openclaw#53849 — OpenRouter returns 403 with "Key limit exceeded"
948979
// when the monthly key spending limit is reached. This must trigger billing failover

src/agents/failover-error.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ function readDirectErrorCode(err: unknown): string | undefined {
151151
const trimmed = directCode.trim();
152152
return trimmed ? trimmed : undefined;
153153
}
154+
const detailCode = (err as { detail?: { code?: unknown } }).detail?.code;
155+
if (typeof detailCode === "string") {
156+
const trimmed = detailCode.trim();
157+
return trimmed ? trimmed : undefined;
158+
}
154159
const status = (err as { status?: unknown }).status;
155160
if (typeof status !== "string" || /^\d+$/.test(status)) {
156161
return undefined;

src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ describe("isAuthPermanentErrorMessage", () => {
8888
"OAuth authentication is currently not allowed for this organization",
8989
"API_KEY_REVOKED",
9090
"api_key_deleted",
91+
"deactivated_workspace",
92+
"deactivated workspace",
9193
],
9294
expected: true,
9395
},

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,8 @@ function classifyFailoverReasonFromCode(raw: string | undefined): FailoverReason
758758
case "THROTTLINGEXCEPTION":
759759
case "THROTTLING_EXCEPTION":
760760
return "rate_limit";
761+
case "DEACTIVATED_WORKSPACE":
762+
return "auth_permanent";
761763
case "OVERLOADED":
762764
case "OVERLOADED_ERROR":
763765
return "overloaded";
@@ -919,6 +921,10 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi
919921
const messageClassification = signal.message
920922
? classifyFailoverClassificationFromMessage(signal.message, signal.provider)
921923
: null;
924+
const codeReason = classifyFailoverReasonFromCode(signal.code);
925+
if (codeReason === "auth_permanent") {
926+
return toReasonClassification(codeReason);
927+
}
922928
const statusClassification = classifyFailoverClassificationFromHttpStatus(
923929
inferredStatus,
924930
signal.message,
@@ -929,7 +935,6 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi
929935
if (statusClassification) {
930936
return statusClassification;
931937
}
932-
const codeReason = classifyFailoverReasonFromCode(signal.code);
933938
if (codeReason) {
934939
return toReasonClassification(codeReason);
935940
}

src/agents/pi-embedded-helpers/failover-matches.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const PERIODIC_USAGE_LIMIT_RE =
77

88
const HIGH_CONFIDENCE_AUTH_PERMANENT_PATTERNS = [
99
/api[_ ]?key[_ ]?(?:revoked|deactivated|deleted)/i,
10+
/deactivated[_ ]workspace/i,
1011
"key has been disabled",
1112
"key has been revoked",
1213
"account has been deactivated",

0 commit comments

Comments
 (0)