From f3f686f6eae843f18626df36e69e676b422292fb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 06:14:49 +0000 Subject: [PATCH] fix(server): a reset credit's limits refresh when the first read after it fails After a confirmed redemption the Codex and Claude drivers re-read usage once. A failed read republishes the pre-redemption windows, so the Usage tile kept the exhausted limits and the redemption reported a failure until the periodic provider check. Retry the re-probe a few times with a short backoff before giving up. Fixes pingdotgg/t3code#33770 AI-assisted; verified with the tests and tools listed above. --- .../src/provider/Drivers/ClaudeDriver.ts | 37 +++++++++--------- .../src/provider/Drivers/CodexDriver.ts | 38 ++++++++----------- .../provider/resetCreditCoordinator.test.ts | 26 +++++++++++++ .../src/provider/resetCreditCoordinator.ts | 33 +++++++++++++++- 4 files changed, 92 insertions(+), 42 deletions(-) diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index 3ada12297649d..af20b76754943 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -349,25 +349,24 @@ export const ClaudeDriver: ProviderDriver - Effect.gen(function* () { - const before = (yield* snapshot.getSnapshot).usageLimits?.checkedAt; - yield* Cache.invalidateAll(capabilitiesProbeCache); - const refreshed = yield* snapshot.refresh; - const after = refreshed.usageLimits?.checkedAt; - if ( - outcome === "reset" && - (after === undefined || - after === before || - refreshed.usageLimits?.unavailable?.reason === "probeFailed") - ) { - return yield* new ProviderDriverError({ - driver: DRIVER_KIND, - instanceId, - detail: - "The reset was applied, but Claude could not confirm the new limits. Refresh to check.", - }); - } - }), + ResetCreditCoordinator.refreshAfterRedemption({ + outcome, + getSnapshot: snapshot.getSnapshot, + refresh: Cache.invalidateAll(capabilitiesProbeCache).pipe( + Effect.andThen(snapshot.refresh), + ), + }).pipe( + Effect.filterOrFail( + (confirmed) => confirmed, + () => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: + "The reset was applied, but Claude could not confirm the new limits. Refresh to check.", + }), + ), + ), ), ); diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 58ef7d27142c9..2d47e22162559 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -347,29 +347,23 @@ export const CodexDriver: ProviderDriver< cause, }), ), - // The windows just changed; re-probe so the snapshot says so. A - // failed probe republishes the pre-redemption limits rather than - // marking them failed, so "confirmed" means `checkedAt` moved past - // what was published before the redemption started. Effect.tap((outcome) => - Effect.gen(function* () { - const before = (yield* snapshot.getSnapshot).usageLimits?.checkedAt; - const refreshed = yield* snapshot.refresh; - const after = refreshed.usageLimits?.checkedAt; - if ( - outcome === "reset" && - (after === undefined || - after === before || - refreshed.usageLimits?.unavailable?.reason === "probeFailed") - ) { - return yield* new ProviderDriverError({ - driver: DRIVER_KIND, - instanceId, - detail: - "The reset was applied, but Codex could not confirm the new limits. Refresh to check.", - }); - } - }), + ResetCreditCoordinator.refreshAfterRedemption({ + outcome, + getSnapshot: snapshot.getSnapshot, + refresh: snapshot.refresh, + }).pipe( + Effect.filterOrFail( + (confirmed) => confirmed, + () => + new ProviderDriverError({ + driver: DRIVER_KIND, + instanceId, + detail: + "The reset was applied, but Codex could not confirm the new limits. Refresh to check.", + }), + ), + ), ), ); diff --git a/apps/server/src/provider/resetCreditCoordinator.test.ts b/apps/server/src/provider/resetCreditCoordinator.test.ts index a7a8010a3f5f8..b863569b06cf3 100644 --- a/apps/server/src/provider/resetCreditCoordinator.test.ts +++ b/apps/server/src/provider/resetCreditCoordinator.test.ts @@ -3,6 +3,7 @@ import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Ref from "effect/Ref"; +import * as TestClock from "effect/testing/TestClock"; import * as ResetCreditCoordinator from "./resetCreditCoordinator.ts"; @@ -100,4 +101,29 @@ describe("ResetCreditCoordinator", () => { assert.strictEqual(yield* Ref.get(peak), 2); }).pipe(Effect.provide(ResetCreditCoordinator.layerTest)), ); + + it.effect("re-probes after a reset until the limits are fresh, then gives up", () => + Effect.gen(function* () { + const stale = { usageLimits: { checkedAt: "t0", windows: [] } }; + const run = (freshFrom: number) => + Effect.gen(function* () { + const probes = yield* Ref.make(0); + const refresh = Ref.updateAndGet(probes, (n) => n + 1).pipe( + Effect.map((n) => + n >= freshFrom ? { usageLimits: { checkedAt: `t${n}`, windows: [] } } : stale, + ), + ); + const fiber = yield* ResetCreditCoordinator.refreshAfterRedemption({ + outcome: "reset", + getSnapshot: Effect.succeed(stale), + refresh, + }).pipe(Effect.forkChild); + yield* TestClock.adjust("1 minute"); + return { confirmed: yield* Fiber.join(fiber), probes: yield* Ref.get(probes) }; + }); + + assert.deepStrictEqual(yield* run(3), { confirmed: true, probes: 3 }); + assert.deepStrictEqual(yield* run(Infinity), { confirmed: false, probes: 4 }); + }), + ); }); diff --git a/apps/server/src/provider/resetCreditCoordinator.ts b/apps/server/src/provider/resetCreditCoordinator.ts index 153728bc8ae43..c311dad7c5ab5 100644 --- a/apps/server/src/provider/resetCreditCoordinator.ts +++ b/apps/server/src/provider/resetCreditCoordinator.ts @@ -1,11 +1,12 @@ /** Instances sharing a login directory share credits; serialize redemptions on that directory. */ -import type { ProviderConsumeResetCreditOutcome } from "@t3tools/contracts"; +import type { ProviderConsumeResetCreditOutcome, ServerProvider } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as SecureRandom from "effect/crypto/SecureRandom"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; +import * as Schedule from "effect/Schedule"; import * as Semaphore from "effect/Semaphore"; interface AccountRedemptionState { @@ -70,6 +71,36 @@ export const make = Effect.gen(function* () { return { redeem } satisfies ResetCreditCoordinator["Service"]; }); +type UsageSnapshot = Pick; + +/** + * Re-probe after a redemption; true once a `reset` shows fresh limits. A failed + * usage read republishes the pre-redemption windows, which are known stale after + * a reset, so retry briefly instead of leaving them until the periodic check. + */ +export const refreshAfterRedemption = Effect.fn("resetCreditCoordinator.refreshAfterRedemption")( + function* (input: { + readonly outcome: ProviderConsumeResetCreditOutcome; + readonly getSnapshot: Effect.Effect; + readonly refresh: Effect.Effect; + }) { + const before = (yield* input.getSnapshot).usageLimits?.checkedAt; + const confirmed = ({ usageLimits }: UsageSnapshot) => + input.outcome !== "reset" || + (usageLimits !== undefined && + usageLimits.checkedAt !== before && + usageLimits.unavailable?.reason !== "probeFailed"); + return yield* input.refresh.pipe( + Effect.map(confirmed), + Effect.repeat({ + until: (ok) => ok, + times: 3, + schedule: Schedule.exponential("1 second"), + }), + ); + }, +); + export const layer = Layer.effect(ResetCreditCoordinator, make); export const layerTest = Layer.effect(