Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion src/CodexThreadErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/ServiceErrorMessage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isWorkspaceRoutingUnavailableMessage} from "./CodexThreadErrors";

const SERVICE_ERROR_TYPES = new Set([
"invalid_request_error",
"server_error",
Expand All @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/CodexACPAgent/auth-error-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -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": []
}
14 changes: 14 additions & 0 deletions src/__tests__/CodexACPAgent/structured-failures-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}}}},
Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/ServiceErrorMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Loading