Repository navigation
Conversation
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused Codex server bug fix that restores reset timestamps for existing usage-limit recovery, including 99% and pre-notification cases. The implementation is localized, uses the existing published limits contract, and is covered by targeted tests without changing defaults, schemas, or quota enforcement. You can add or adjust custom eligibility rules. Learn more. |
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes.
|
| Layer / File(s) | Summary |
|---|---|
Reset calculation and provider tests apps/server/src/provider/codexUsageLimits.ts, apps/server/src/provider/codexUsageLimits.test.ts |
The calculation filters windows whose reset time has passed. It selects windows at 100% usage, or windows at 99% or higher when none are at 100%. Tests cover selection at a supplied time. |
Failure reset resolution and session updates apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts |
The adapter resolves reset times from session snapshots before published usage limits. It tracks the reset source and applies session updates to limited-turn errors when the reset value changes. |
Provider wiring and adapter scenarios apps/server/src/provider/Drivers/CodexDriver.ts, apps/server/src/provider/Drivers/CodexManagedProvider.ts, apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts |
Both provider drivers pass usage limits from the current snapshot. Replay tests cover near-full and published resets, stale published limits, and later error-item updates. |
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix · Severity of issue fixed: Medium
Possibly related issues
- [Bug]: Codex usage-limit failures lose resetAt when a run's first request hits the 429 (no auto-resume, no reset time on the card) #27773 — Covers Codex usage-limit failures that lose
resetAt; this change resolves terminal resets from session or published limits. - [Bug]: Orchestrator V2 Usage Reset Tracker Broken #14169 — Covers a missing reset known from Usage → Limits; this change supplies published usage-limit reset data for failed turns.
Possibly related PRs
- pingdotgg/t3code#27787 — Both changes use provider limits as reset data for Codex usage-limit failures.
- pingdotgg/t3code#27198 — Both changes modify reset selection for Codex usage-limit failures.
- pingdotgg/t3code#27791 — Both changes address first-request failures before a session snapshot is available.
Suggested labels: bugfix
Suggested reviewers: t3dotgg
Merge Risk: 
Minimal · previous review b6fa4
-
The reviewed reset-time behavior has no identified issue requiring a fix before merge. Normal validation remains appropriate.
Security Architecture Review
-
Security architecture risk: 🔵 Low · up to
98815The change is narrowly scoped to reporting when Codex usage limits reset. No new permissions or cross-account access were identified. Combined interruption and recovery scenarios remain incompletely covered.
Retained concerns
No architecture-level concerns identified.Security review details
-
Security Blast Radius
- inferred — The demonstrated new influence is on reset metadata for usage-limit failures in sessions belonging to the captured provider instance, including tracked failures awaiting late enrichment. The getter does not accept an instance selector or expose another instance’s snapshot.
Trust Boundaries and Controls
- observed — Existing notification processing excludes explicitly non-Codex quota buckets, ignores non-finite usage percentages, clamps percentages, and normalizes valid epoch resets. Published limits originate from the existing instance status path. The PR reuses this observational data without adding an authority-bearing sink.
-
Pre-merge checks: 
3 · 
1
-
Failed checks: 1 warning Passed checks: 3 passed-
Full details: Title check
-
Explanation
The title "fix(server): Codex usage-limit stops keep their reset time" describes the main change: the server now keeps a usage-limit reset time for Codex refusals that previously lacked one. The changeset adds reset-time selection for 99% and exhausted windows, a published-limits fallback via getUsageLimits, and stale-reset handling, all of which serve that goal. The title is concise, uses a conventional commit prefix, and is specific enough for a teammate scanning history.
Full details: Description check
-
Explanation
The description follows the repository template in substance. It has a Problem section that explains the lost reset time and the hidden resume and snooze actions, and it links issue
#14169. The Change section explains the reset selection rules and the provisional published-limits fallback. The Scope and approval section links the issue and the prior work (#18721), and it notes that the maintainer takeover preserved contributor history. The Verification section lists specific commands, test counts, and the limits of what was checked (no real quota refusal, no macOS/Windows verification). The template asks for approval evidence such as a maintainer approval comment. The description references the issue and the takeover but does not quote an explicit approval comment, so this is a minor gap. The template also asks for the model and harness used, which the description includes at the end.
Full details: Out of Scope Changes check
-
Explanation
All changed areas support
#14169or the PR's stated reset-selection scope. These areas are: codexUsageLimits.ts (reset selection, expiry, published-limit helper), CodexAdapterV2.ts (session-first reset lookup, provisional published resets, turn-item update gating, limitId filter), CodexDriver.ts and CodexManagedProvider.ts (wiring of getUsageLimits), and the matching tests. The diff adds no client, contract, or unrelated product-area changes. The limitId filter matches the stated rule that model-specific buckets cannot claim the main allowance.
-
Full details: Linked Issues check
-
Explanation
Issue
#14169requires the composer banner to show a known reset time and enable Resume at reset and Snooze until reset. The PR adds session-first and published-limit reset selection, the 99% fallback, expiry filtering, and the model-bucket filter (limitId must be codex). These changes address the reported cases in codexUsageLimits.ts and CodexAdapterV2.ts. The tests cover the near-full, published, expired, and stale-published cases.The terminal handler still breaks the requirement. In CodexAdapterV2.ts, the terminal-failure path sets
failure.resetAttoreset.resetAtwithout checking the incomingevent.failure.resetAt. The earlier code usedevent.failure.resetAt ?? derived. If the terminal event carries a non-null reset and no usable session or published reset exists, the failure is stored withresetAt: null. The banner then reports the reset as unavailable. The same issue was found in the previous review. The current diff does not change this code path.This review did not confirm whether
event.failure.resetAtis non-null for any usage-limit failure before this handler. The base code treats it as possible, and that is the basis for this FAIL.Resolution
In the terminal usage-limit handler, keep a non-null
event.failure.resetAt. Use the derived session or published reset only when the incoming failure reset is null. Add a regression test where the terminal failure has a known reset, the session snapshot and published limits have no usable reset, and the emitted failure keeps the known reset.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
…stream pingdotgg#18721) Own diff of pingdotgg#18721 at 98815ea, applied on stark/main. The test table keeps the cases of pingdotgg#17501 next to the new ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hit this today on The last I wrote a smaller fix before finding this PR (fall back to the fullest window when none reads 100, skipped when a workspace credit/spend cap caused the stop) and ran it against the existing tests. The 99% case fails on |
|
Additional Windows reproduction on T3 Code 0.0.46-nightly.20261015.3176, Codex 0.163.0, ChatGPT Plus. This matches the missing in-session snapshot path described here and in #14169. A read-only inspection of the local state found a Codex root failure with: {
"class": "usage_limit",
"code": "usageLimitExceeded",
"resetAt": null
}The same failure's message ended with "try again at 5:24 AM", while the composer showed "Reset time unavailable; retry manually". After the allowance reset, Usage showed current countdowns and available capacity, while that chat retained its saved failure. The later usage panel does not establish the exact percentages at the original refusal. Before finding this PR, we installed a small local workaround. Sharing it here rather than opening a competing PR:
One implementation detail: the lookup must be forked. The raw protocol reader awaits notification callbacks, so awaiting another RPC inline inside the terminal notification callback can prevent that reader from consuming its reply. Starting the lookup after the failed item is registered also avoids a reply arriving before the late-fill map contains the item. This complements the discussion here; it is not a replacement for this PR's 99% handling. Our workaround still relies on the existing helper's exhausted-window rule, so a refreshed snapshot that only reports 99% can still leave the reset unknown. Verification: nine isolated regression cases passed using T3's bundled Effect runtime and mocked usage responses: a session reset, both windows exhausted (later reset wins), multi-bucket selection, an already-reset allowance, a missing timestamp, an unrelated bucket, request failure, request timeout/nonblocking dispatch, and preserving an existing reset. The patched server passed the JavaScript syntax check and the desktop app restarted normally. We have not run this against a new real quota refusal, or run the repository's native adapter suite for this bundled workaround. Exact locally installed workaround against the shipped server bundleThis is the tested bundled-code diff, not a source-ready PR or a request to commit generated files. The source location is --- a/apps/server/dist/binCli-BU_S3Cf-.mjs
+++ b/apps/server/dist/binCli-BU_S3Cf-.mjs
@@ -223198,7 +223198,7 @@
turnItem: artifacts.turnItem
});
}).pipe(orDie));
- yield* client.handleServerNotification("account/rateLimits/updated", (payload) => gen$1(function* () {
+ const applyT3ResetSnapshot = (payload) => gen$1(function* () {
yield* update$4(rateLimitSnapshot, (previous) => mergeCodexRateLimits(previous, payload.rateLimits));
const resetAt = codexUsageLimitResetAt(yield* get$7(rateLimitSnapshot));
for (const item of (yield* get$7(limitedTurnItems)).values()) {
@@ -223226,7 +223226,8 @@
checkedAt
});
}
- }).pipe(orDie));
+ });
+ yield* client.handleServerNotification("account/rateLimits/updated", (payload) => applyT3ResetSnapshot(payload).pipe(orDie));
yield* client.handleServerNotification("thread/tokenUsage/updated", (payload) => gen$1(function* () {
accumulateCodexTurnTokenUsage(usageStateForThread(payload.threadId), payload.turnId, payload.tokenUsage);
const context = yield* awaitActiveTurn(payload.turnId);
@@ -224130,6 +224131,19 @@
occurredAt: yield* now
});
yield* update$4(limitedTurnItems, (items) => new Map(items).set(context.providerThread.id, item));
+ // Local T3 reset-time fix: refresh after saving the failed item.
+ // Fork so the protocol reader remains free to receive the reply.
+ yield* client.request("account/rateLimits/read", null).pipe(
+ timeoutOption(millis(3000)),
+ flatMap$1((response) => {
+ if (!isSome(response)) return void_$1;
+ const snapshot = response.value.rateLimitsByLimitId?.codex ?? response.value.rateLimits;
+ if (!snapshot || snapshot.limitId && snapshot.limitId !== "codex") return void_$1;
+ return applyT3ResetSnapshot({ rateLimits: snapshot });
+ }),
+ catch_(() => void_$1),
+ forkIn(scope)
+ );
}
});
const emitOrDeferRootTerminal = fn("CodexAdapterV2.emitOrDeferRootTerminal")(function* (input) {Prepared with Codex (GPT-6) in the Codex desktop app. |
|
Commenting on behalf of @jaikhuranna (posted by Claude Code). This adds another reproduction of case 1 and an end-to-end check of this PR in a real client. Same refusal, different outcome, 3 seconds apart. On 2026-10-17, two Codex threads on the same account hit the limit 2.4 s apart. Codex sent both the same "try again at 7:12 AM" message:
So the reset was already known for thread A. The End-to-end check. I wrapped the real
The reset shown (1:22:57 PM) matches the faked 99% window's Rebasing onto current
Wrapper used for the end-to-end check#!/usr/bin/env node
// Wraps the real `codex app-server`, but answers every turn/start itself with a
// 99% usage update followed by a usageLimitExceeded refusal.
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";
const child = spawn(process.env.REAL_CODEX ?? "codex", process.argv.slice(2), {
stdio: ["pipe", "pipe", "inherit"],
});
child.on("exit", (code) => process.exit(code ?? 0));
const send = (frame) => process.stdout.write(`${JSON.stringify(frame)}\n`);
const now = () => Math.floor(Date.now() / 1000);
const turn = (id, status, error = null) => ({
id, items: [], itemsView: "notLoaded", status, error,
startedAt: now(), completedAt: status === "inProgress" ? null : now(), durationMs: null,
});
let turns = 0;
createInterface({ input: process.stdin }).on("line", (line) => {
let frame;
try { frame = JSON.parse(line); } catch { child.stdin.write(`${line}\n`); return; }
if (frame.method !== "turn/start") { child.stdin.write(`${line}\n`); return; }
const threadId = frame.params.threadId;
const turnId = `proxy-limit-turn-${++turns}`;
const resetsAt = now() + 4 * 60 * 60;
send({ id: frame.id, result: { turn: turn(turnId, "inProgress") } });
send({ method: "turn/started", params: { threadId, turn: turn(turnId, "inProgress") } });
setTimeout(() => {
send({ method: "account/rateLimits/updated", params: { rateLimits: {
limitId: "codex",
primary: { usedPercent: 99, resetsAt, windowDurationMins: 300 },
secondary: { usedPercent: 16, resetsAt: resetsAt + 6 * 86400, windowDurationMins: 10080 },
} } });
send({ method: "turn/completed", params: { threadId, turn: turn(turnId, "failed", {
message: "You've hit your usage limit. ... try again later.",
codexErrorInfo: "usageLimitExceeded",
}) } });
}, 500);
});
// Drop the real account's rate-limit updates so only the 99% one reaches T3 Code.
createInterface({ input: child.stdout }).on("line", (line) => {
if (!line.includes('"account/rateLimits/updated"')) process.stdout.write(`${line}\n`);
}); |
|
Taking over this one as maintainer for batch codex-06. I will preserve the contributor history, merge current main, keep focused regression coverage, and verify session reset precedence before publishing. No merge is planned. |
6cb3476 to
b6fa42a
Compare
A Codex turn refused while its last rate-limit update read 99%, or before the session saw any update, recorded no reset, so the composer hid Resume at reset and Snooze until reset. Treat a 99% window as exhausted when none reads 100%, ignore windows that have already renewed, and fall back to the instance's published limits when the session snapshot has no reset. Fixes pingdotgg#14169 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b6fa42a to
a8abab3
Compare


Codex can refuse a turn while its last quota update still reads 99%, or before the session receives any quota notification. Those failures lost a reset time the server already knew, hiding the existing resume-at-reset and snooze actions. Fixes #14169.
The reset selection prefers current exhausted windows, accepts 99% only when none is fully exhausted, and discards expired observations. Both external CLI and managed Codex sessions can use the provider instance’s published limits before any main-allowance session data arrives. That fallback remains provisional: the first session notification replaces or clears it. A known session-derived reset is preserved, and later session data can fill an unknown reset. Model-specific quota buckets cannot claim the main allowance.
This takes over the original work by voltcrash in #18721, preserves the contributor commits, and merges current main. The original fallback-on-null behavior was replaced because stale published data could become a permanent reset. No client or contract changes.
Verification:
pnpm exec vp test run apps/server/src/provider/codexUsageLimits.test.ts apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts --maxWorkers=2: 194 passed.tsc-rs --noEmitpasses with Effect suggestions only.git diff --checkpasses.Proof is deterministic app-server replay through the real adapter, not a live quota refusal. No screenshots apply to this server-only change. Real accounts, macOS, Windows, iOS, Android, and Electron desktop were not verified on this headless Linux VM. Existing historical failures are not repaired.
Implementation: GPT-6.1 Sol; tests: GPT-6 Astra; independent reviews: Claude Opus 5.5 and GPT-6 Sol. Harness: Codex / Claude in T3 Code.