Skip to content

Commit 2843258

Browse files
authored
fix(billing): carry the codex denial message across the ACP boundary
A plain Error serializes to a bare -32603 "Internal error" at the ACP boundary, so the gateway text never reached the host's classifier — the denial read as a fatal session error and triggered a kill/respawn/resend loop instead of the upgrade modal. Reject with RequestError.internalError(undefined, message) like the Claude adapter, which lands client-side as "Internal error: …needs a paid PostHog plan…" and classifies correctly. The test now asserts the RequestError shape, not just the in-process rejection. Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
1 parent 0649175 commit 2843258

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
NewSessionRequest,
55
PromptRequest,
66
} from "@agentclientprotocol/sdk";
7+
import { RequestError } from "@agentclientprotocol/sdk";
78
import { describe, expect, it } from "vitest";
89
import type {
910
AppServerClientHandlers,
@@ -1619,7 +1620,20 @@ describe("CodexAppServerAgent", () => {
16191620
},
16201621
});
16211622

1622-
await expect(done).rejects.toThrow("needs a paid PostHog plan");
1623+
const err = await done.then(
1624+
() => {
1625+
throw new Error("prompt resolved instead of rejecting");
1626+
},
1627+
(e: unknown) => e,
1628+
);
1629+
// Must be a RequestError with the gateway text in its message — a plain
1630+
// Error crosses the ACP boundary as a bare "Internal error", which the
1631+
// host classifies as fatal and answers with a kill/respawn loop.
1632+
expect(err).toBeInstanceOf(RequestError);
1633+
expect((err as RequestError).message).toContain("Internal error: ");
1634+
expect((err as RequestError).message).toContain(
1635+
"needs a paid PostHog plan",
1636+
);
16231637
});
16241638

16251639
it("ends the turn without turn/start when no prompt block is usable", async () => {

packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
SetSessionConfigOptionResponse,
2020
StopReason,
2121
} from "@agentclientprotocol/sdk";
22+
import { RequestError } from "@agentclientprotocol/sdk";
2223
import {
2324
classifyGatewayLimitError,
2425
mcpToolKey,
@@ -1259,13 +1260,15 @@ export class CodexAppServerAgent extends BaseAcpAgent {
12591260
});
12601261
const message = error?.message ?? "";
12611262
// A gateway billing denial rejects the prompt so the host classifies
1262-
// it and shows the upgrade gate; a silent refusal would hide it.
1263+
// it and shows the upgrade gate. It must be a RequestError: a plain
1264+
// Error serializes to a bare "Internal error" at the ACP boundary,
1265+
// which the host reads as fatal and answers with a respawn loop.
12631266
if (classifyGatewayLimitError(message) !== null) {
12641267
if (this.compactionActive) {
12651268
this.compactionActive = false;
12661269
this.emitCompactionBoundary();
12671270
}
1268-
this.turns.fail(new Error(message));
1271+
this.turns.fail(RequestError.internalError(undefined, message));
12691272
return;
12701273
}
12711274
void this.finalizeTurn("refusal");

0 commit comments

Comments
 (0)