fix: classify codex deactivated workspace codes

This commit is contained in:
Altay
2026-05-21 21:48:29 +03:00
parent a488cbe695
commit 3aa770fa84
4 changed files with 34 additions and 2 deletions

View File

@@ -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-<slug>.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.

View File

@@ -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)", () => {

View File

@@ -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;

View File

@@ -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);
}