diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index a178e68d..b79875b9 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -47,6 +47,7 @@ import {ImageViewReporter} from "./tool-calls/reporters/ImageViewReporter"; import {McpToolReporter} from "./tool-calls/reporters/McpToolReporter"; import {WebSearchReporter} from "./tool-calls/reporters/WebSearchReporter"; import {CodexPlanStream} from "./CodexPlanStream"; +import {isWorkspaceRoutingUnavailableMessage} from "./CodexThreadErrors"; import { createMessagePhaseMeta, createAgentTextMessageChunk, @@ -330,7 +331,7 @@ export class CodexEventHandler { return; } const failure = this.recordSessionFailure( - this.sessionFailureKind(notification.params.error.codexErrorInfo), + this.sessionFailureKind(notification.params.error), notification.params.turnId, "error", notification.params.error.message, @@ -1064,7 +1065,7 @@ export class CodexEventHandler { const failure = params.willRetry ? this.recordRetryWarning(params) : this.recordSessionFailure( - this.sessionFailureKind(params.error.codexErrorInfo), + this.sessionFailureKind(params.error), params.turnId, "error", params.error.message, @@ -1105,7 +1106,7 @@ export class CodexEventHandler { } private recordTypedSessionFailure(params: ErrorNotification): void { - const kind = this.sessionFailureKind(params.error.codexErrorInfo); + const kind = this.sessionFailureKind(params.error); this.recordSessionFailure(kind, params.turnId, "error", params.error.message); } @@ -1166,7 +1167,7 @@ export class CodexEventHandler { } private recordRetryWarning(params: ErrorNotification, attributeToTurn = true): SessionFailure { - const kind = this.sessionFailureKind(params.error.codexErrorInfo); + const kind = this.sessionFailureKind(params.error); return this.recordSessionFailure( kind, params.turnId, @@ -1227,8 +1228,9 @@ export class CodexEventHandler { }; } - private sessionFailureKind(error: CodexErrorInfo | null): CodexFailureKind { + private sessionFailureKind({message, codexErrorInfo: error}: ErrorNotification["error"]): CodexFailureKind { if (this.isAuthenticationRequiredError(error)) return "auth_required"; + if (isWorkspaceRoutingUnavailableMessage(message)) return "transport_lost"; if (this.getHttpStatusCode(error) === 429) return "rate_limited"; if (typeof error === "string") { return Object.hasOwn(STRING_CODEX_ERROR_CATEGORIES, error) diff --git a/src/CodexThreadErrors.ts b/src/CodexThreadErrors.ts index 69c721e3..de227254 100644 --- a/src/CodexThreadErrors.ts +++ b/src/CodexThreadErrors.ts @@ -195,7 +195,15 @@ function internalErrorText(err: unknown): string | null { */ export function isAccountReadUnavailableError(err: unknown): boolean { const text = internalErrorText(err); - return text !== null && ACCOUNT_READ_UNAVAILABLE_MESSAGES.has(text); + return text !== null && isWorkspaceRoutingUnavailableMessage(text); +} + +/** + * True for the whole text of a routing discovery that got no answer from the ChatGPT backend. A turn of a ChatGPT + * login runs the same discovery before each model request, and fails with this text as its error. + */ +export function isWorkspaceRoutingUnavailableMessage(text: string): boolean { + return ACCOUNT_READ_UNAVAILABLE_MESSAGES.has(text); } /** diff --git a/src/ServiceErrorMessage.ts b/src/ServiceErrorMessage.ts index 5f85e3a6..b83a0012 100644 --- a/src/ServiceErrorMessage.ts +++ b/src/ServiceErrorMessage.ts @@ -1,3 +1,5 @@ +import {isWorkspaceRoutingUnavailableMessage} from "./CodexThreadErrors"; + const SERVICE_ERROR_TYPES = new Set([ "invalid_request_error", "server_error", @@ -17,10 +19,21 @@ const INVALID_ENCRYPTED_CONTENT_MESSAGE = "The current provider can't read this + "which another provider or account encrypted. This happens after the provider changes in a thread. " + "Switch back to the previous provider or start a new thread."; +/** + * A ChatGPT login looks up its workspace on chatgpt.com before each model request. + * The app-server error has no cause. + */ +function workspaceRoutingUnavailableMessage(text: string): string { + return "Codex can't reach chatgpt.com to look up your ChatGPT workspace. " + + "Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. " + + `(${text})`; +} + /** * Reads the message of a known service error envelope, or adds a human-readable explanation. */ export function readableServiceErrorMessage(text: string): string { + if (isWorkspaceRoutingUnavailableMessage(text)) return workspaceRoutingUnavailableMessage(text); const value = parseJson(text); if (value === undefined) return text; if (isInvalidEncryptedContentError(value)) return INVALID_ENCRYPTED_CONTENT_MESSAGE; diff --git a/src/__tests__/CodexACPAgent/auth-error-events.test.ts b/src/__tests__/CodexACPAgent/auth-error-events.test.ts index 5fbaec90..8a5fae63 100644 --- a/src/__tests__/CodexACPAgent/auth-error-events.test.ts +++ b/src/__tests__/CodexACPAgent/auth-error-events.test.ts @@ -169,6 +169,20 @@ describe("CodexEventHandler - auth error events", () => { ); }); + it.each([ + {name: "legacy error text", capabilities: undefined}, + {name: "typed failure", capabilities: typedFailureCapabilities}, + ])("explains an unreachable ChatGPT backend as a connection failure in the $name", async ({capabilities}) => { + const {result, updates} = await runPromptWithError(createTestSessionState({ + sessionId: "workspace-routing-session", account: {type: "chatgpt", email: "user@example.com", planType: "plus"}, + }), { + message: "workspace routing discovery failed", codexErrorInfo: "other", additionalDetails: null, misalignment: null, + }, false, capabilities); + await expect(JSON.stringify({result, updates}, null, 2)).toMatchFileSnapshot( + `data/workspace-routing-failed-v1-${capabilities ? "typed-failure" : "legacy"}.json`, + ); + }); + it.each([ "Plain error", "", "{invalid JSON}", JSON.stringify({type: "error", status: 400, error: {type: "custom_error", message: "custom text"}}), diff --git a/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-air.json b/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-air.json new file mode 100644 index 00000000..6e12e392 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-air.json @@ -0,0 +1,31 @@ +{ + "error": { + "code": -32603, + "message": "Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed)" + }, + "stopReason": "error", + "_meta": { + "quota": { + "token_count": null, + "model_usage": [] + }, + "jetbrains": { + "air": { + "version": 1, + "sessionFailure": { + "id": "turn-1:error", + "revision": 1, + "category": "connection", + "severity": "error", + "title": "Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed)", + "actions": [ + "retry", + "new_session" + ] + } + } + } + }, + "state": "idle", + "sessionUpdate": "state_update" +} diff --git a/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-plain.json b/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-plain.json new file mode 100644 index 00000000..bfbcd032 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/structured-failure-v2-workspace-routing-plain.json @@ -0,0 +1,15 @@ +{ + "error": { + "code": -32603, + "message": "Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed)" + }, + "stopReason": "error", + "_meta": { + "quota": { + "token_count": null, + "model_usage": [] + } + }, + "state": "idle", + "sessionUpdate": "state_update" +} diff --git a/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-legacy.json b/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-legacy.json new file mode 100644 index 00000000..6c2deae5 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-legacy.json @@ -0,0 +1,21 @@ +{ + "result": { + "stopReason": "end_turn", + "usage": null, + "_meta": { + "quota": { + "token_count": null, + "model_usage": [] + } + } + }, + "updates": [ + { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed)\n\n" + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-typed-failure.json b/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-typed-failure.json new file mode 100644 index 00000000..78237145 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/workspace-routing-failed-v1-typed-failure.json @@ -0,0 +1,29 @@ +{ + "result": { + "stopReason": "end_turn", + "usage": null, + "_meta": { + "quota": { + "token_count": null, + "model_usage": [] + }, + "jetbrains": { + "air": { + "version": 1, + "sessionFailure": { + "id": "turn-id:error", + "revision": 1, + "category": "connection", + "severity": "error", + "title": "Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed)", + "actions": [ + "retry", + "new_session" + ] + } + } + } + } + }, + "updates": [] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/structured-failures-v2.test.ts b/src/__tests__/CodexACPAgent/structured-failures-v2.test.ts index 0fea4bc6..271256c5 100644 --- a/src/__tests__/CodexACPAgent/structured-failures-v2.test.ts +++ b/src/__tests__/CodexACPAgent/structured-failures-v2.test.ts @@ -87,6 +87,20 @@ describe("standard ACP v2 terminal failures", () => { expect(lastIdle(client)).toEqual({sessionUpdate: "state_update", state: "idle"}); }); + it.each([false, true])("explains an unreachable ChatGPT backend as a connection failure (%s)", async air => { + client = await connectSession(2, air ? { + clientCapabilities: {_meta: {jetbrains: {air: {version: 1, capabilities: ["sessionFailure"]}}}}, + } : {}); + await insert(client); + client.emit(errorNotification("workspace routing discovery failed", "other")); + client.emit(turnFinished("failed")); + await client.promptRunFinished(); + await settle(); + + await expect(dump(lastIdle(client))) + .toMatchFileSnapshot(`data/structured-failure-v2-workspace-routing-${air ? "air" : "plain"}.json`); + }); + it.each([false, true])("reports auth failure before insertion through both the request and idle/error (%s)", async air => { client = await connectSession(2, air ? { clientCapabilities: {_meta: {jetbrains: {air: {version: 1, capabilities: ["sessionFailure"]}}}}, diff --git a/src/__tests__/ServiceErrorMessage.test.ts b/src/__tests__/ServiceErrorMessage.test.ts index 3ded891c..c3aa2162 100644 --- a/src/__tests__/ServiceErrorMessage.test.ts +++ b/src/__tests__/ServiceErrorMessage.test.ts @@ -50,6 +50,21 @@ describe("service error messages", () => { }); }); + describe("an unreachable ChatGPT backend", () => { + it.each(["failed", "timed out"])("explains workspace routing discovery that %s", async outcome => { + await expect(readableServiceErrorMessage(`workspace routing discovery ${outcome}`)) + .toMatchFileSnapshot(`data/service-error-workspace-routing-${outcome.replace(" ", "-")}.txt`); + }); + + it.each([ + "workspace routing discovery unauthorized (401)", + "selected workspace missing from routing discovery", + "workspace routing discovery failed: details", + ])("keeps other routing errors verbatim: %s", text => { + expect(readableServiceErrorMessage(text)).toBe(text); + }); + }); + it.each([ "Plain error text", "", "{not json}", "null", "[]", '"json string"', JSON.stringify({error: {message}}), diff --git a/src/__tests__/data/service-error-workspace-routing-failed.txt b/src/__tests__/data/service-error-workspace-routing-failed.txt new file mode 100644 index 00000000..5c43f2a3 --- /dev/null +++ b/src/__tests__/data/service-error-workspace-routing-failed.txt @@ -0,0 +1 @@ +Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery failed) \ No newline at end of file diff --git a/src/__tests__/data/service-error-workspace-routing-timed-out.txt b/src/__tests__/data/service-error-workspace-routing-timed-out.txt new file mode 100644 index 00000000..c4b8323b --- /dev/null +++ b/src/__tests__/data/service-error-workspace-routing-timed-out.txt @@ -0,0 +1 @@ +Codex can't reach chatgpt.com to look up your ChatGPT workspace. Check your network connection and any proxy, VPN, or firewall that blocks chatgpt.com, then try again. (workspace routing discovery timed out) \ No newline at end of file