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> = {
194194 },
195195};
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
197231const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {
198232 const path = yield* Path.Path;
199233 const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated");
......@@ -660,7 +694,8 @@ const generateFiles = Effect.fn("generateFiles")(function* () {
660694
661695 for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) {
662696 const compatibleDefinitionSchema =
663 Codex0150DefinitionSchemas[definitionName] ?? definitionSchema;
697 Codex0150DefinitionSchemas[definitionName] ??
698 applyCodex0151DefinitionCompatibility(file.exportName, definitionName, definitionSchema);
664699 aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults(
665700 normalizeNullableTypes(
666701 rewriteExternalRefs(
packages/effect-codex-app-server/src/_generated/schema.gen.ts+6
......@@ -16854,6 +16854,7 @@ export type V2ThreadReadResponse__CodexErrorInfo =
1685416854 | "badRequest"
1685516855 | "threadRollbackFailed"
1685616856 | "sandboxError"
16857 | "rateLimitExceeded"
1685716858 | "other"
1685816859 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
1685916860 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
......@@ -16877,6 +16878,7 @@ export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union(
1687716878 "badRequest",
1687816879 "threadRollbackFailed",
1687916880 "sandboxError",
16881 "rateLimitExceeded",
1688016882 "other",
1688116883 ]),
1688216884 Schema.Struct({
......@@ -17207,6 +17209,7 @@ export type V2ThreadResumeResponse__CodexErrorInfo =
1720717209 | "badRequest"
1720817210 | "threadRollbackFailed"
1720917211 | "sandboxError"
17212 | "rateLimitExceeded"
1721017213 | "other"
1721117214 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
1721217215 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
......@@ -17230,6 +17233,7 @@ export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union(
1723017233 "badRequest",
1723117234 "threadRollbackFailed",
1723217235 "sandboxError",
17236 "rateLimitExceeded",
1723317237 "other",
1723417238 ]),
1723517239 Schema.Struct({
......@@ -17485,6 +17489,7 @@ export type V2ThreadRollbackResponse__CodexErrorInfo =
1748517489 | "badRequest"
1748617490 | "threadRollbackFailed"
1748717491 | "sandboxError"
17492 | "rateLimitExceeded"
1748817493 | "other"
1748917494 | { readonly httpConnectionFailed: { readonly httpStatusCode?: number | null } }
1749017495 | { readonly responseStreamConnectionFailed: { readonly httpStatusCode?: number | null } }
......@@ -17508,6 +17513,7 @@ export const V2ThreadRollbackResponse__CodexErrorInfo = Schema.Union(
1750817513 "badRequest",
1750917514 "threadRollbackFailed",
1751017515 "sandboxError",
17516 "rateLimitExceeded",
1751117517 "other",
1751217518 ]),
1751317519 Schema.Struct({
packages/effect-codex-app-server/src/schema.test.ts+44
......@@ -4,6 +4,9 @@ import * as Schema from "effect/Schema";
44import * as CodexSchema from "./schema.ts";
55
66const isGetAccountResponse = Schema.is(CodexSchema.V2GetAccountResponse);
7const isThreadReadResponse = Schema.is(CodexSchema.V2ThreadReadResponse);
8const isThreadResumeResponse = Schema.is(CodexSchema.V2ThreadResumeResponse);
9const isThreadRollbackResponse = Schema.is(CodexSchema.V2ThreadRollbackResponse);
710
811it("keeps async questions in live notifications and thread history", () => {
912 const item = {
......@@ -97,6 +100,47 @@ it("accepts Codex 0.150 multi-agent values", () => {
97100 assert.equal(Schema.is(CodexSchema.V2ThreadResumeResponse)(resumeResponse), true);
98101});
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
100144it("accepts Codex 0.150 account plan values", () => {
101145 const planTypes = [
102146 "self_serve_business_prolite",