Skip to content

Commit 3b8ac38

Browse files
authored
fix(codex): classify app-server auth refresh failures
Classify Codex native/app-server auth refresh logout failures and preserve app-server relogin detail in RPC errors.
1 parent 78644bc commit 3b8ac38

6 files changed

Lines changed: 93 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212
### Fixes
1313

1414
- iOS/chat: resize PhotosPicker image attachments to capped JPEGs before staging and sending, stripping source metadata and keeping oversized camera photos under the chat upload budget. Fixes #68524. Thanks @BunsDev.
15+
- Codex harness: classify native app-server token-refresh logout and relogin failures as authentication refresh errors, so users get re-authentication guidance instead of a raw runtime failure.
1516
- Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly.
1617
- Agents: preserve source-reply delivery metadata when merging tool-returned media into the final reply, keeping message-tool-only replies deliverable and mirrored. Thanks @pashpashpash and @vincentkoc.
1718
- macOS/companion: require system TLS trust before pinning a first-use direct `wss://` gateway certificate and honor `gateway.remote.tlsFingerprint` as the explicit pin for remote node-mode sessions, so fresh endpoints fail closed when macOS cannot trust the certificate unless configured out of band. Fixes #50642. Thanks @BunsDev.

extensions/codex/src/app-server/client.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,42 @@ describe("CodexAppServerClient", () => {
127127
await expect(request).rejects.toHaveProperty("message", "Method not found");
128128
});
129129

130+
it("surfaces relogin details from Codex app-server RPC errors", async () => {
131+
const harness = createClientHarness();
132+
clients.push(harness.client);
133+
134+
const request = harness.client.request("thread/start", {});
135+
const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
136+
harness.send({
137+
id: outbound.id,
138+
error: {
139+
code: -32602,
140+
message: "failed to load configuration",
141+
data: {
142+
reason: "cloudRequirements",
143+
errorCode: "Auth",
144+
action: "relogin",
145+
statusCode: 401,
146+
detail:
147+
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
148+
},
149+
},
150+
});
151+
152+
await expect(request).rejects.toHaveProperty(
153+
"message",
154+
"failed to load configuration: Your authentication session could not be refreshed automatically. Please log out and sign in again.",
155+
);
156+
await expect(request).rejects.toHaveProperty("data", {
157+
reason: "cloudRequirements",
158+
errorCode: "Auth",
159+
action: "relogin",
160+
statusCode: 401,
161+
detail:
162+
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
163+
});
164+
});
165+
130166
it("rejects timed-out requests and ignores late responses", async () => {
131167
vi.useFakeTimers();
132168
const harness = createClientHarness();

extensions/codex/src/app-server/client.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,39 @@ export class CodexAppServerRpcError extends Error {
4242
readonly data?: JsonValue;
4343

4444
constructor(error: { code?: number; message: string; data?: JsonValue }, method: string) {
45-
super(error.message || `${method} failed`);
45+
super(formatCodexAppServerRpcErrorMessage(error, method));
4646
this.name = "CodexAppServerRpcError";
4747
this.code = error.code;
4848
this.data = error.data;
4949
}
5050
}
5151

52+
function formatCodexAppServerRpcErrorMessage(
53+
error: { message: string; data?: JsonValue },
54+
method: string,
55+
): string {
56+
const message = error.message || `${method} failed`;
57+
const detail = readCodexAppServerRpcReloginDetail(error.data);
58+
return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
59+
}
60+
61+
function readCodexAppServerRpcReloginDetail(data: JsonValue | undefined): string | undefined {
62+
const record = isJsonObject(data) ? data : undefined;
63+
const nested = isJsonObject(record?.error) ? record.error : record;
64+
if (!nested) {
65+
return undefined;
66+
}
67+
const isRelogin =
68+
nested.action === "relogin" ||
69+
(nested.reason === "cloudRequirements" && nested.errorCode === "Auth");
70+
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
71+
return isRelogin && detail ? detail : undefined;
72+
}
73+
74+
function isJsonObject(value: unknown): value is { [key: string]: JsonValue } {
75+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
76+
}
77+
5278
export function isCodexAppServerConnectionClosedError(error: unknown): boolean {
5379
if (!(error instanceof Error)) {
5480
return false;

src/agents/auth-profiles/oauth-refresh-failure.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export type OAuthRefreshFailureReason =
1212
const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i;
1313
const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
1414

15+
function isOAuthRefreshFailureMessage(message: string): boolean {
16+
const lower = message.toLowerCase();
17+
return (
18+
lower.includes("oauth token refresh failed") ||
19+
lower.includes("access token could not be refreshed") ||
20+
lower.includes("authentication session could not be refreshed automatically")
21+
);
22+
}
23+
1524
function extractOAuthRefreshFailureProvider(message: string): string | null {
1625
const provider = message.match(OAUTH_REFRESH_FAILURE_PROVIDER_RE)?.[1]?.trim();
1726
return provider && provider.length > 0 ? provider : null;
@@ -49,7 +58,7 @@ export function classifyOAuthRefreshFailure(message: string): {
4958
provider: string | null;
5059
reason: OAuthRefreshFailureReason | null;
5160
} | null {
52-
if (!/oauth token refresh failed/i.test(message)) {
61+
if (!isOAuthRefreshFailureMessage(message)) {
5362
return null;
5463
}
5564
return {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ describe("formatAssistantErrorText", () => {
281281
);
282282
});
283283

284+
it("returns an explicit re-authentication message for Codex app-server refresh failures", () => {
285+
const msg = makeAssistantError(
286+
"Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.",
287+
);
288+
expect(formatAssistantErrorText(msg)).toBe(
289+
"Authentication refresh failed. Re-authenticate this provider and try again.",
290+
);
291+
});
292+
284293
it("returns a contention-specific message for OAuth refresh lock timeouts", () => {
285294
const msg = makeAssistantError("file lock timeout for /tmp/openclaw-oauth-refresh.lock");
286295
expect(formatAssistantErrorText(msg)).toBe(

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,16 @@ describe("classifyProviderRuntimeFailureKind", () => {
14471447
"OAuth token refresh failed for openai-codex: invalid_grant. Please try again or re-authenticate.",
14481448
),
14491449
).toBe("auth_refresh");
1450+
expect(
1451+
classifyProviderRuntimeFailureKind(
1452+
"Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.",
1453+
),
1454+
).toBe("auth_refresh");
1455+
expect(
1456+
classifyProviderRuntimeFailureKind(
1457+
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
1458+
),
1459+
).toBe("auth_refresh");
14501460
});
14511461

14521462
it("classifies OAuth refresh timeouts and lock contention distinctly", () => {

0 commit comments

Comments
 (0)