authorgravatar for 92423096+nateEc@users.noreply.github.comYukun Shan <92423096+nateEc@users.noreply.github.com> 2026-09-04 09:16:35+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 18:16:35-07:00
log75ab5ab3fb6ad35117da754644c404a31b2fed84
tree84dbe12f5605ca367f8a843a2f241b479bcdd01a
parentb90898077e60290a1eb7bea4224f325fd3bb1595
signature Signed by PGP key B5690EEEBB952194

fix(codex): accept rate limit errors on thread resume (#8897)


3 files changed, 86 insertions(+), 1 deletions(-)

packages/effect-codex-app-server/scripts/generate.ts+36-1
...@@ -194,6 +194,40 @@ const Codex0150DefinitionSchemas: Record<string, Schema.Json> = {...@@ -194,6 +194,40 @@ const Codex0150DefinitionSchemas: Record<string, Schema.Json> = {
194 },194 },
195};195};
196196
197function applyCodex0151DefinitionCompatibility(
198 exportName: string,
199 definitionName: string,
200 definitionSchema: Schema.Json,
201): Schema.Json {
202 const isThreadResponse =
203 exportName === "V2ThreadReadResponse" ||
204 exportName === "V2ThreadResumeResponse" ||
205 exportName === "V2ThreadRollbackResponse";
206 if (
207 !isThreadResponse ||
208 definitionName !== "CodexErrorInfo" ||
209 typeof definitionSchema !== "object"
210 ) {
211 return definitionSchema;
212 }
213
214 const schema = definitionSchema as {
215 readonly oneOf?: ReadonlyArray<{ readonly enum?: ReadonlyArray<string> }>;
216 };
217 const [firstVariant, ...remainingVariants] = schema.oneOf ?? [];
218 if (!firstVariant?.enum || firstVariant.enum.includes("rateLimitExceeded")) {
219 return definitionSchema;
220 }
221
222 return {
223 ...definitionSchema,
224 oneOf: [
225 { ...firstVariant, enum: [...firstVariant.enum, "rateLimitExceeded"] },
226 ...remainingVariants,
227 ],
228 };
229}
230
197const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {231const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {
198 const path = yield* Path.Path;232 const path = yield* Path.Path;
199 const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated");233 const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated");
...@@ -660,7 +694,8 @@ const generateFiles = Effect.fn("generateFiles")(function* () {...@@ -660,7 +694,8 @@ const generateFiles = Effect.fn("generateFiles")(function* () {
660694
661 for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) {695 for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) {
662 const compatibleDefinitionSchema =696 const compatibleDefinitionSchema =
663 Codex0150DefinitionSchemas[definitionName] ?? definitionSchema;697 Codex0150DefinitionSchemas[definitionName] ??
698 applyCodex0151DefinitionCompatibility(file.exportName, definitionName, definitionSchema);
664 aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults(699 aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults(
665 normalizeNullableTypes(700 normalizeNullableTypes(
666 rewriteExternalRefs(701 rewriteExternalRefs(
packages/effect-codex-app-server/src/_generated/schema.gen.ts+6
...@@ -16854,6 +16854,7 @@ export type V2ThreadReadResponse__CodexErrorInfo =...@@ -16854,6 +16854,7 @@ export type V2ThreadReadResponse__CodexErrorInfo =
16854 | "badRequest"16854 | "badRequest"
16855 | "threadRollbackFailed"16855 | "threadRollbackFailed"
16856 | "sandboxError"16856 | "sandboxError"
16857 | "rateLimitExceeded"
16857 | "other"16858 | "other"
16858 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }16859 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
16859 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }16860 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
...@@ -16877,6 +16878,7 @@ export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union(...@@ -16877,6 +16878,7 @@ export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union(
16877 "badRequest",16878 "badRequest",
16878 "threadRollbackFailed",16879 "threadRollbackFailed",
16879 "sandboxError",16880 "sandboxError",
16881 "rateLimitExceeded",
16880 "other",16882 "other",
16881 ]),16883 ]),
16882 Schema.Struct({16884 Schema.Struct({
...@@ -17207,6 +17209,7 @@ export type V2ThreadResumeResponse__CodexErrorInfo =...@@ -17207,6 +17209,7 @@ export type V2ThreadResumeResponse__CodexErrorInfo =
17207 | "badRequest"17209 | "badRequest"
17208 | "threadRollbackFailed"17210 | "threadRollbackFailed"
17209 | "sandboxError"17211 | "sandboxError"
17212 | "rateLimitExceeded"
17210 | "other"17213 | "other"
17211 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }17214 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
17212 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }17215 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
...@@ -17230,6 +17233,7 @@ export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union(...@@ -17230,6 +17233,7 @@ export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union(
17230 "badRequest",17233 "badRequest",
17231 "threadRollbackFailed",17234 "threadRollbackFailed",
17232 "sandboxError",17235 "sandboxError",
17236 "rateLimitExceeded",
17233 "other",17237 "other",
17234 ]),17238 ]),
17235 Schema.Struct({17239 Schema.Struct({
...@@ -17485,6 +17489,7 @@ export type V2ThreadRollbackResponse__CodexErrorInfo =...@@ -17485,6 +17489,7 @@ export type V2ThreadRollbackResponse__CodexErrorInfo =
17485 | "badRequest"17489 | "badRequest"
17486 | "threadRollbackFailed"17490 | "threadRollbackFailed"
17487 | "sandboxError"17491 | "sandboxError"
17492 | "rateLimitExceeded"
17488 | "other"17493 | "other"
17489 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }17494 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
17490 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }17495 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
...@@ -17508,6 +17513,7 @@ export const V2ThreadRollbackResponse__CodexErrorInfo = Schema.Union(...@@ -17508,6 +17513,7 @@ export const V2ThreadRollbackResponse__CodexErrorInfo = Schema.Union(
17508 "badRequest",17513 "badRequest",
17509 "threadRollbackFailed",17514 "threadRollbackFailed",
17510 "sandboxError",17515 "sandboxError",
17516 "rateLimitExceeded",
17511 "other",17517 "other",
17512 ]),17518 ]),
17513 Schema.Struct({17519 Schema.Struct({
packages/effect-codex-app-server/src/schema.test.ts+44
...@@ -4,6 +4,9 @@ import * as Schema from "effect/Schema";...@@ -4,6 +4,9 @@ import * as Schema from "effect/Schema";
4import * as CodexSchema from "./schema.ts";4import * as CodexSchema from "./schema.ts";
55
6const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);6const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);
7const isThreadReadResponse = Schema.is(CodexSchema.V2ThreadReadResponse);
8const isThreadResumeResponse = Schema.is(CodexSchema.V2ThreadResumeResponse);
9const isThreadRollbackResponse = Schema.is(CodexSchema.V2ThreadRollbackResponse);
710
8it("keeps async questions in live notifications and thread history", () => {11it("keeps async questions in live notifications and thread history", () => {
9 const item = {12 const item = {
...@@ -97,6 +100,47 @@ it("accepts Codex 0.150 multi-agent values", () => {...@@ -97,6 +100,47 @@ it("accepts Codex 0.150 multi-agent values", () => {
97 assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse)(resumeResponse), true);100 assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse)(resumeResponse), true);
98});101});
99102
103it("accepts Codex rate limit errors for thread responses", () => {
104 const failedThread = {
105 cliVersion: "0.150.0",
106 createdAt: 0,
107 cwd: "/tmp/project",
108 ephemeral: false,
109 id: "thread-1",
110 modelProvider: "openai",
111 preview: "",
112 sessionId: "session-1",
113 source: "cli",
114 status: { type: "idle" },
115 turns: [
116 {
117 error: {
118 codexErrorInfo: "rateLimitExceeded",
119 message: "Rate limit exceeded",
120 },
121 id: "turn-1",
122 items: [],
123 status: "failed",
124 },
125 ],
126 updatedAt: 0,
127 };
128 assert.equal(isThreadReadResponse({ thread: failedThread }), true);
129 assert.equal(
130 isThreadResumeResponse({
131 approvalPolicy: "never",
132 approvalsReviewer: "user",
133 cwd: "/tmp/project",
134 model: "gpt-5.6-sol",
135 modelProvider: "openai",
136 sandbox: { type: "dangerFullAccess" },
137 thread: failedThread,
138 }),
139 true,
140 );
141 assert.equal(isThreadRollbackResponse({ thread: failedThread }), true);
142});
143
100it("accepts Codex 0.150 account plan values", () => {144it("accepts Codex 0.150 account plan values", () => {
101 const planTypes = [145 const planTypes = [
102 "self_serve_business_prolite",146 "self_serve_business_prolite",