From 3aa770fa848bf3a9701484d4aa23bcdbeb508a79 Mon Sep 17 00:00:00 2001 From: Altay Date: Thu, 21 May 2026 21:48:29 +0300 Subject: [PATCH] fix: classify codex deactivated workspace codes --- CHANGELOG.md | 2 +- src/agents/failover-error.test.ts | 22 ++++++++++++++++++++++ src/agents/failover-error.ts | 5 +++++ src/agents/pi-embedded-helpers/errors.ts | 7 ++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1861982280e..068e38eb607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - 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. - 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. - 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. +- 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. ## 2026.5.20 @@ -858,7 +859,6 @@ Docs: https://docs.openclaw.ai - CLI: route `plugins list --json` through the parsed command fast path and cover it in response budgets so plugin JSON inventory avoids full CLI registration work. - Control UI/Overview: render recent session rows through the shared session display resolver so label/displayName priority, key-equivalent labels, and channel fallbacks stay consistent with the chat selector. (#50696) Thanks @Maple778 and @BunsDev. - Gateway/network: keep OpenClaw-installed undici dispatchers on HTTP/1.1 and treat destroyed HTTP/2 session errors as recoverable network teardown, preventing `ERR_HTTP2_INVALID_SESSION` from crashing active gateway turns. Fixes #81627. (#81838) Thanks @joshavant. -- Codex/failover: classify `deactivated_workspace` as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. - Memory/daily-files: widen the daily-memory file matcher used by Dreaming, rem-backfill, rem-harness, the doctor sweep, and short-term promotion so `memory/YYYY-MM-DD-.md` files written by the bundled session-memory hook (and any future slugged variants) are discovered alongside the date-only `memory/YYYY-MM-DD.md` shape. Date extraction still uses the leading `YYYY-MM-DD` capture group, so per-day ingestion/promotion semantics are unchanged for existing date-only files; slugged files now flow through the same paths instead of being silently skipped. Fixes #69536. Thanks @jack-stormentswe. - macOS/Gateway: fail managed LaunchAgent stop and restart when the configured gateway port remains busy after cleanup instead of reporting success while a listener survives. Fixes #73132. Thanks @BunsDev. - Telegram: reuse the sticky IPv4 Bot API transport for periodic getMe health checks, so IPv4-working hosts with broken IPv6 egress stop logging repeated probe timeouts. Fixes #76852. (#76856) Thanks @SymbolStar. diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index 256302d96cb..4a875808ad8 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -950,6 +950,28 @@ describe("failover-error", () => { expect(resolveFailoverReasonFromError({ message: "deactivated workspace" })).toBe( "auth_permanent", ); + expect(resolveFailoverReasonFromError({ code: "deactivated_workspace" })).toBe( + "auth_permanent", + ); + expect( + resolveFailoverReasonFromError({ + detail: { code: "deactivated_workspace" }, + }), + ).toBe("auth_permanent"); + expect( + resolveFailoverReasonFromError({ + status: 403, + message: "Forbidden", + detail: { code: "deactivated_workspace" }, + }), + ).toBe("auth_permanent"); + expect( + resolveFailoverReasonFromError({ + status: 400, + message: "Bad request", + detail: { code: "deactivated_workspace" }, + }), + ).toBe("auth_permanent"); }); it("403 OpenRouter 'Key limit exceeded' returns billing (model fallback trigger)", () => { diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index 4bf920fa347..2c057c5070b 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -151,6 +151,11 @@ function readDirectErrorCode(err: unknown): string | undefined { const trimmed = directCode.trim(); return trimmed ? trimmed : undefined; } + const detailCode = (err as { detail?: { code?: unknown } }).detail?.code; + if (typeof detailCode === "string") { + const trimmed = detailCode.trim(); + return trimmed ? trimmed : undefined; + } const status = (err as { status?: unknown }).status; if (typeof status !== "string" || /^\d+$/.test(status)) { return undefined; diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 9a5b38d3076..97873af38d1 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -758,6 +758,8 @@ function classifyFailoverReasonFromCode(raw: string | undefined): FailoverReason case "THROTTLINGEXCEPTION": case "THROTTLING_EXCEPTION": return "rate_limit"; + case "DEACTIVATED_WORKSPACE": + return "auth_permanent"; case "OVERLOADED": case "OVERLOADED_ERROR": return "overloaded"; @@ -919,6 +921,10 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi const messageClassification = signal.message ? classifyFailoverClassificationFromMessage(signal.message, signal.provider) : null; + const codeReason = classifyFailoverReasonFromCode(signal.code); + if (codeReason === "auth_permanent") { + return toReasonClassification(codeReason); + } const statusClassification = classifyFailoverClassificationFromHttpStatus( inferredStatus, signal.message, @@ -929,7 +935,6 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi if (statusClassification) { return statusClassification; } - const codeReason = classifyFailoverReasonFromCode(signal.code); if (codeReason) { return toReasonClassification(codeReason); }