From 7601c707b0c2e07de6f6cd19bee68c04b3804d8c Mon Sep 17 00:00:00 2001 From: eleith Date: Mon, 2 Nov 2026 07:10:32 -0800 Subject: [PATCH] refactor(codex): clarify rate-limit parsing and refresh package docs Separate Retry-After interpretation from HTTP error classification without changing accepted delays. Characterize numeric, date, invalid and absent headers through public account clients, retaining no-retry and error-body cleanup assertions. Correct Codex installation, controls and migration documentation, and prepare 0.2.1 with a Changesets-generated maintenance entry. --- README.md | 2 +- extensions/codex/CHANGELOG.md | 6 +++ extensions/codex/README.md | 46 ++++++++++++++-------- extensions/codex/package.json | 2 +- extensions/codex/src/client.test.ts | 60 +++++++++++++++++++++++++---- extensions/codex/src/client.ts | 23 +++++++---- 6 files changed, 106 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 36ed771..dbaa37e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ a handful of small personal extensions to Pi, only using documented APIs. | extension | description | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | [compact editor chrome](extensions/compact-editor-chrome/README.md) | minimal status details around the prompt input. less is more | -| [context codex](extensions/context-codex/README.md) | extend or restore local compaction windows for supported openai-codex models | +| [codex](extensions/codex/README.md) | local context windows, account usage, and fast-mode controls for Codex | | [notifications](extensions/notifications/README.md) | notifications on turn end using notify-send (with OSC777 fallback for remote support) | | [progress](extensions/progress/README.md) | OSC9;4 progress bars (with TMUX wrapping support) while waiting on a turn | | [title status](extensions/title-status/README.md) | simple terminal titles with icon progress indicators | diff --git a/extensions/codex/CHANGELOG.md b/extensions/codex/CHANGELOG.md index 5fa6dcb..4d628d7 100644 --- a/extensions/codex/CHANGELOG.md +++ b/extensions/codex/CHANGELOG.md @@ -1,5 +1,11 @@ # @eleith-pi/codex +## 0.2.1 + +### Patch Changes + +- Separate Codex Retry-After parsing from HTTP error classification and strengthen rate-limit regression coverage. Correct Codex names, links, installation and command instructions in the READMEs. + ## 0.2.0 ### Minor Changes diff --git a/extensions/codex/README.md b/extensions/codex/README.md index 6615f75..ccae433 100644 --- a/extensions/codex/README.md +++ b/extensions/codex/README.md @@ -1,7 +1,7 @@ -# context codex +# codex -extend or restore Pi's local compaction window for supported `openai-codex` models. -verified profiles currently cover GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Astra; +context controls, account usage, and fast-mode requests using Pi's existing Codex login. +verified extended-context profiles currently cover GPT-6 Sol, GPT-6.1 Sol, and GPT-6 Astra; unknown models are not guessed. ## why @@ -12,34 +12,48 @@ compaction window, not server capacity or account limits. larger context may cos ## install ```sh -pi install npm:@eleith-pi/context-codex +pi install npm:@eleith-pi/codex ``` ## use -- `/eleith:context-codex` shows context, compaction, and cache status. -- `/eleith:context-codex extend` selects the verified local window when larger. -- `/eleith:context-codex restore` returns to the catalog baseline, confirming reductions. +- `/eleith:codex` shows the local context and fast-mode dashboard, including compaction and cache status. +- `/eleith:codex context on` selects the verified local window when larger. +- `/eleith:codex context off` returns to the catalog baseline, confirming reductions. +- `/eleith:codex context toggle` switches the saved context preference. +- `/eleith:codex fast on` requests priority processing after confirmation of higher allowance use. +- `/eleith:codex fast off` requests standard processing. +- `/eleith:codex fast toggle` switches the saved fast preference. +- `/eleith:codex usage` fetches account allowance and reset information. in the terminal, the cancel key (Escape by default) aborts the request. -branch preferences persist across reloads. the catalog baseline is not guessed: +bare `context` and `fast` show the same dashboard as the root command, without account or catalog requests. +fast controls require a direct official legacy `openai-codex` selection with Pi OAuth and no provider override. +enabling priority also requires catalog eligibility. +priority is a request, not a scheduling guarantee; reported response tiers are shown separately. + +context and fast preferences persist per model on the active branch across reloads. the catalog baseline is not guessed: missing baselines are reported, and models already at or above the verified window get a no-extension notice rather than a smaller window. -preference replay only runs while Pi is idle. after tree navigation it waits for +local context preference replay only runs while Pi is idle. after tree navigation it waits for normal user input, before Pi checks compaction. custom-message-triggered turns use -the current window without replaying pending preferences; explicit extend/restore -commands also require idle. +the current window without replaying pending preferences; explicit context and fast +changes also require idle. ### migrate -run `/eleith:context restore` with the old extension loaded before removing it: +when migrating from `@eleith-pi/context-codex`, run its old `/eleith:context-codex restore` +command with that extension loaded before removing it: ```sh -pi remove npm:@eleith-pi/context-window -pi install npm:@eleith-pi/context-codex +pi remove npm:@eleith-pi/context-codex +pi install npm:@eleith-pi/codex ``` -branch preferences are preserved. do not load both packages together. +if migrating from the older `@eleith-pi/context-window` instead, first use its +`/eleith:context restore` command, then remove that package before installing codex. + +branch preferences are preserved. do not load the predecessor and codex together. ## config @@ -49,4 +63,4 @@ set a command prefix in `~/.pi/agent/extensions/eleith.json`: { "commandPrefix": "personal" } ``` -then use `/personal:context-codex`. +then use `/personal:codex`. diff --git a/extensions/codex/package.json b/extensions/codex/package.json index c210a9e..29740f7 100644 --- a/extensions/codex/package.json +++ b/extensions/codex/package.json @@ -1,6 +1,6 @@ { "name": "@eleith-pi/codex", - "version": "0.2.0", + "version": "0.2.1", "private": false, "description": "Context windows, account usage, and fast-mode controls for Codex in Pi", "keywords": [ diff --git a/extensions/codex/src/client.test.ts b/extensions/codex/src/client.test.ts index c36f422..aae57aa 100644 --- a/extensions/codex/src/client.test.ts +++ b/extensions/codex/src/client.test.ts @@ -323,17 +323,63 @@ it.each([401, 403])("reports HTTP %s without exposing the server's error body", expect(cancel).toHaveBeenCalledOnce(); }); -it("returns retry-after information without automatically retrying", async () => { - const f = fixture(); - f.fetcher.mockResolvedValue( - new Response("private error", { status: 429, headers: { "Retry-After": "60" } }), - ); - await expect(fetchCodexUsage(f.registry, undefined, f.fetcher)).rejects.toMatchObject({ +it.each([ + { label: "seconds", header: "60", retryAfterMs: 60_000 }, + { label: "zero seconds", header: "0", retryAfterMs: 0 }, + { label: "fractional seconds", header: "1.5", retryAfterMs: 1500 }, + { label: "numeric exponent notation", header: "1e1", retryAfterMs: 10_000 }, + { label: "a future HTTP date", header: "Wed, 02 Jan 2030 03:05:05 GMT", retryAfterMs: 60_000 }, + { label: "the current HTTP date", header: "Wed, 02 Jan 2030 03:04:05 GMT", retryAfterMs: 0 }, + { label: "a past HTTP date", header: "Wed, 02 Jan 2030 03:03:05 GMT", retryAfterMs: undefined }, + { label: "a malformed value", header: "later", retryAfterMs: undefined }, + { label: "negative seconds", header: "-1", retryAfterMs: undefined }, + { label: "a non-finite value", header: "Infinity", retryAfterMs: undefined }, + { label: "an empty header", header: "", retryAfterMs: undefined }, + { label: "a missing header", header: null, retryAfterMs: undefined }, +])( + "reports Retry-After with $label without automatically retrying", + async ({ header, retryAfterMs }) => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2030-01-02T03:04:05Z")); + const account = fixture(); + const headers = new Headers(); + if (header !== null) headers.set("Retry-After", header); + const response = new Response("private error", { status: 429, headers }); + const discard = vi.spyOn(response.body!, "cancel"); + account.fetcher.mockResolvedValue(response); + + await expect( + fetchCodexUsage(account.registry, undefined, account.fetcher), + ).rejects.toMatchObject({ + code: "http", + message: "Codex account service is rate limited.", + retryAfterMs, + }); + expect(account.fetcher).toHaveBeenCalledOnce(); + expect(discard).toHaveBeenCalledOnce(); + }, +); + +it("returns the catalog's HTTP-date retry delay without retrying or exposing its body", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2030-01-02T03:04:05Z")); + const account = fixture(); + const response = new Response("private catalog error", { + status: 429, + headers: { "Retry-After": "Wed, 02 Jan 2030 03:05:05 GMT" }, + }); + const discard = vi.spyOn(response.body!, "cancel"); + account.fetcher.mockResolvedValue(response); + + await expect( + fetchCodexModelCatalog(account.registry, undefined, account.fetcher), + ).rejects.toMatchObject({ code: "http", message: "Codex account service is rate limited.", retryAfterMs: 60_000, }); - expect(f.fetcher).toHaveBeenCalledOnce(); + expect(account.fetcher).toHaveBeenCalledOnce(); + expect(discard).toHaveBeenCalledOnce(); }); it("does not follow or display a redirected endpoint", async () => { diff --git a/extensions/codex/src/client.ts b/extensions/codex/src/client.ts index 043ffda..5ce1f74 100644 --- a/extensions/codex/src/client.ts +++ b/extensions/codex/src/client.ts @@ -226,18 +226,25 @@ async function requestAccountJson( function responseError(response: Response): CodexAccountError { if (response.status === 401 || response.status === 403) return authError(); if (response.status === 429) { - const header = response.headers.get("retry-after"); - let retryAfterMs: number | undefined; - if (header) { - const seconds = Number(header); - const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - Date.now(); - if (Number.isFinite(delay) && delay >= 0) retryAfterMs = delay; - } - return new CodexAccountError("http", "Codex account service is rate limited.", retryAfterMs); + return new CodexAccountError( + "http", + "Codex account service is rate limited.", + parseRetryAfterMs(response.headers.get("retry-after")), + ); } return new CodexAccountError("http", `Codex account request failed (HTTP ${response.status}).`); } +function parseRetryAfterMs(header: string | null): number | undefined { + if (!header) return undefined; + const seconds = Number(header); + let delayMs: number; + if (Number.isFinite(seconds)) delayMs = seconds * 1000; + else delayMs = Date.parse(header) - Date.now(); + if (!Number.isFinite(delayMs) || delayMs < 0) return undefined; + return delayMs; +} + function discardBody(response: Response): void { void response.body?.cancel().catch(() => {}); } -- 2.54.0