Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1317,3 +1317,11 @@ The process exits 0 only if all four live scenarios pass, 1 otherwise, and 2 for
invalid arguments or missing credentials. This is a **wire diagnostic**, not an
end-to-end Codex App/CLI interface test, live certification or instruction to enable
the experimental feature for production work.

## Starting unused quota windows

The per-account **Quota window auto activation** setting can start an unused five-hour or weekly window as well as activate one after its reset. It remains opt-in and uses a small normal model request; it never consumes reset credits or changes the reported usage counters.

Zero percent alone is not enough to trigger a request. OpenCodex compares fresh observations: a reset deadline that keeps moving forward with the observation time indicates an unused window, while a fixed deadline is already counting down. A second observation may take about a minute. Only windows actually reported for that account are eligible.

An initial attempt is recorded before sending and cannot be repeated immediately after a proxy restart. Failed or uncertain attempts wait at least five minutes. Paused accounts, pending account validation, reauthentication and native-main protection remain in force. Successful inference can precede the usage display update; a fixed reset deadline and decreasing remaining time confirm that the window has started, even if rounded usage still reads 0%.
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
}
},
"explicit": {
"codex-quota-initial-activation.test.ts": "codex-integration",
"provider-antigravity-quota-retry.test.ts": "providers",
"responses-compaction-recovery.test.ts": "responses", "compaction-recovery-settings.test.ts": "config", "responses-compaction-recovery-policy.test.ts": "responses", "plugin-loader.test.ts": "lib", "plugin-upstream-hooks.test.ts": "lib",
"cli-kiro-auto-selection.test.ts": "cli", "codebuddy-live-models.test.ts": "providers", "kiro-auto-selection.test.ts": "providers/kiro", "kiro-quota-metrics.test.ts": "providers/kiro", "management-provider-request-pacing.test.ts": "server",
Expand Down
9 changes: 9 additions & 0 deletions src/codex/quota-auto-refresh-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>(
export const retryAfterByAccount = new Map<string, number>();
export const scheduledByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
export const quotaRefreshAfterByAccount = new Map<string, number>();
export type InitialWindow = "fiveHour" | "weekly";
export const initialWindowObservations = new Map<string, {
binding: string;
observedAt: number;
probeAfter: number;
windows: Partial<Record<InitialWindow, { observedAt: number; resetAt: number; ready: boolean }>>;
}>();

/** Drop every activation record when its account is removed. */
export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
completedByAccount.delete(accountId);
retryAfterByAccount.delete(accountId);
scheduledByAccount.delete(accountId);
quotaRefreshAfterByAccount.delete(accountId);
initialWindowObservations.delete(accountId);
}

/** Clear the dependency-free activation bookkeeping for isolated tests. */
Expand All @@ -21,4 +29,5 @@ export function resetCodexQuotaAutoRefreshStateForTests(): void {
retryAfterByAccount.clear();
scheduledByAccount.clear();
quotaRefreshAfterByAccount.clear();
initialWindowObservations.clear();
}
30 changes: 24 additions & 6 deletions src/codex/quota-auto-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import { resolveNativeProfileContext } from "./native-profile-store";
import { getMainQuotaCredentialGeneration, observeMainQuotaCredential } from "./main-account-cache";
import { applyAccountQuotaFromUpstreamHeaders, getAccountQuota, type StoredAccountQuota } from "./quota";
import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
import { initialActivationRetryBlocked, inspectInitialQuotaActivation, noteInitialQuotaProbe, reserveInitialQuotaActivation, INITIAL_ACTIVATION_RETRY_MS } from "./quota-initial-activation";
import {
completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount,
completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount, initialWindowObservations,
resetCodexQuotaAutoRefreshStateForTests,
type CodexQuotaAutoRefreshWindows,
} from "./quota-auto-refresh-state";
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface CodexQuotaAutoRefreshRunDeps {
accountId: string,
completed: CodexQuotaAutoRefreshWindows,
) => boolean;
reserveInitial?: typeof reserveInitialQuotaActivation;
}

let inFlight: Promise<void> | null = null;
Expand Down Expand Up @@ -273,9 +275,10 @@ function retryPendingMarkers(
/** Coalesce sweeps, refresh stale metadata and activate due accounts with bounded concurrency. */
export async function runCodexQuotaAutoRefresh(
config: OcxConfig,
now = Date.now(),
requestedNow?: number,
deps: CodexQuotaAutoRefreshRunDeps = {},
): Promise<void> {
const now = requestedNow ?? Date.now();
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
if (!openai || openai.disabled === true || !isCanonicalOpenAiForwardProvider(openai)) return;
if (providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return;
Expand All @@ -284,6 +287,9 @@ export async function runCodexQuotaAutoRefresh(
const warm = deps.warmAccount ?? warmAccount;
const persist = deps.persistCompleted ?? persistCompleted;
const refresh = deps.refreshQuota ?? refreshQuota;
const reserveInitial = deps.reserveInitial ?? reserveInitialQuotaActivation;
const credentialBinding = (id: string) => id === MAIN_CODEX_ACCOUNT_ID
? `main:${getMainQuotaCredentialGeneration()}` : `pool:${readCodexAccountRecord(id)?.generation ?? "missing"}`;
inFlight = (async () => {
retryPendingMarkers(config, persist);
const accountIds = [
Expand All @@ -305,22 +311,34 @@ export async function runCodexQuotaAutoRefresh(
};
for (let index = 0; index < accountIds.length; index += CONCURRENCY) {
await Promise.all(accountIds.slice(index, index + CONCURRENCY).map(async accountId => {
if (!eligible(accountId)) return;
if (!eligible(accountId)) { initialWindowObservations.delete(accountId); return; }
// Restart must not turn the same recent initial attempt into a due-window replay.
if (initialActivationRetryBlocked(config, accountId, now)) return;
// Capture before WHAM can move an idle window's reset into the future.
rememberWindows(config, accountId, quotaFor(accountId));
const quota = quotaFor(accountId);
if ((!quota || now - quota.updatedAt >= RETRY_MS)
&& (quotaRefreshAfterByAccount.get(accountId) ?? 0) <= now) {
const initialProbe = inspectInitialQuotaActivation(config, accountId, quota, credentialBinding(accountId), now).probe;
if (((!quota || now - quota.updatedAt >= RETRY_MS)
&& (quotaRefreshAfterByAccount.get(accountId) ?? 0) <= now) || initialProbe) {
quotaRefreshAfterByAccount.set(accountId, now + RETRY_MS);
noteInitialQuotaProbe(accountId, now);
try { await refresh(config, accountId); } catch { /* Retry metadata at the bounded cadence. */ }
}
if (!eligible(accountId)) return;
rememberWindows(config, accountId, quotaFor(accountId));
if ((retryAfterByAccount.get(accountId) ?? 0) > now) return;
const windows = dueCodexQuotaAutoRefreshWindows(config, accountId, quotaFor(accountId), now);
if (!windows) return;
if (!windows) {
// Metadata is timestamped after the awaited fetch, not at sweep entry.
const observedNow = requestedNow ?? Date.now();
const initial = inspectInitialQuotaActivation(config, accountId, quotaFor(accountId), credentialBinding(accountId), observedNow).windows;
if (!initial.length || !reserveInitial(config, accountId, initial, observedNow) || !eligible(accountId)) return;
retryAfterByAccount.set(accountId, observedNow + INITIAL_ACTIVATION_RETRY_MS);
}
try {
if (await warm(config, accountId) === false) return;
// A completed warmup is not proof that the upstream clock is already ticking.
if (!windows) return;
retryAfterByAccount.delete(accountId);
const completed = { ...completedByAccount.get(accountId), ...windows };
completedByAccount.set(accountId, completed);
Expand Down
91 changes: 91 additions & 0 deletions src/codex/quota-initial-activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { mutatePersistedConfig } from "../config";
import { normalizeResetAt } from "../providers/quota-wire";
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
import { providerCodexAccountMode } from "../providers/registry";
import type { OcxConfig } from "../types";
import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
import type { StoredAccountQuota } from "./quota-types";
import { initialWindowObservations, type InitialWindow } from "./quota-auto-refresh-state";

export const INITIAL_ACTIVATION_RETRY_MS = 300_000;
const PROBE_MS = 60_000;
const MIN_MOVEMENT_MS = 30_000;
const RESET_SLOP_MS = 3_000;
const DURATIONS = { fiveHour: 18_000_000, weekly: 604_800_000 } as const;

export function initialActivationRetryBlocked(config: OcxConfig, accountId: string, now: number): boolean {
const last = normalizeResetAt(config.codexQuotaAutoRefresh?.[accountId]?.lastInitialActivationAttemptAt);
return last !== undefined && now - last < INITIAL_ACTIVATION_RETRY_MS;
}

/** Two fresh zero-use snapshots must move together; 0% alone does not prove an idle window. */
export function inspectInitialQuotaActivation(
config: OcxConfig, accountId: string, quota: StoredAccountQuota | null, binding: string, now: number,
): { probe: boolean; windows: InitialWindow[] } {
const empty = { probe: false, windows: [] as InitialWindow[] };
const setting = config.codexQuotaAutoRefresh?.[accountId];
if (!setting || !binding || !quota || !Number.isSafeInteger(now)
|| !Number.isSafeInteger(quota.updatedAt) || now < quota.updatedAt
|| now - quota.updatedAt > INITIAL_ACTIVATION_RETRY_MS
|| initialActivationRetryBlocked(config, accountId, now)) {
initialWindowObservations.delete(accountId);
return empty;
}
const previous = initialWindowObservations.get(accountId);
const prior = previous?.binding === binding ? previous : undefined;
const next: NonNullable<typeof prior> = { binding, observedAt: now,
probeAfter: prior?.probeAfter ?? now + PROBE_MS, windows: {} };
for (const name of ["fiveHour", "weekly"] as const) {
const percent = name === "fiveHour" ? quota.shortPercent : quota.weeklyPercent;
const observedAt = name === "fiveHour" ? quota.shortObservedAt ?? quota.updatedAt : quota.updatedAt;
const resetAt = normalizeResetAt(name === "fiveHour" ? quota.shortResetAt : quota.weeklyResetAt);
if (setting[name] !== true || percent !== 0 || resetAt === undefined
|| (name === "fiveHour" && quota.shortWindowSeconds !== 18_000)
|| !Number.isSafeInteger(observedAt) || now < observedAt
|| now - observedAt > INITIAL_ACTIVATION_RETRY_MS
|| Math.abs(resetAt - observedAt - DURATIONS[name]) > RESET_SLOP_MS) continue;
const old = prior?.windows[name];
const elapsed = old ? observedAt - old.observedAt : 0;
const ready = !!old && (observedAt === old.observedAt && resetAt === old.resetAt ? old.ready
: elapsed >= MIN_MOVEMENT_MS && elapsed <= INITIAL_ACTIVATION_RETRY_MS
&& Math.abs((resetAt - old.resetAt) - elapsed) <= RESET_SLOP_MS);
next.windows[name] = { observedAt, resetAt, ready };
}
const names = Object.keys(next.windows) as InitialWindow[];
if (names.length === 0) { initialWindowObservations.delete(accountId); return empty; }
initialWindowObservations.set(accountId, next);
return { probe: now >= next.probeAfter, windows: names.filter(name => next.windows[name]!.ready) };
}

/** A failed observation never becomes a busy polling loop. */
export function noteInitialQuotaProbe(accountId: string, now: number): void {
const observed = initialWindowObservations.get(accountId);
if (observed) observed.probeAfter = now + PROBE_MS;
}

/** Persist intent before inference, so a restart cannot immediately repeat an uncertain attempt. */
export function reserveInitialQuotaActivation(
config: OcxConfig, accountId: string, windows: readonly InitialWindow[], now: number,
): boolean {
if (!windows.length || !Number.isSafeInteger(now) || now < 0) return false;
try {
const result = mutatePersistedConfig(persisted => {
const current = persisted.codexQuotaAutoRefresh?.[accountId];
const provider = persisted.providers[OPENAI_CODEX_PROVIDER_ID];
const last = normalizeResetAt(current?.lastInitialActivationAttemptAt);
if (!current || !windows.some(name => current[name] === true)
|| !provider || provider.disabled === true || !isCanonicalOpenAiForwardProvider(provider)
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, provider) !== "pool"
|| persisted.pausedCodexAccountIds?.includes(accountId)
|| (accountId !== MAIN_CODEX_ACCOUNT_ID && !persisted.codexAccounts?.some(account => account.id === accountId && isSelectableCodexPoolAccount(account)))
|| (last !== undefined && now - last < INITIAL_ACTIVATION_RETRY_MS)) return { changed: false, value: null };
const updated = { ...current, lastInitialActivationAttemptAt: now };
persisted.codexQuotaAutoRefresh = { ...persisted.codexQuotaAutoRefresh, [accountId]: updated };
return { changed: true, value: updated };
});
if (result.status === "unavailable" || !result.value) return false;
config.codexQuotaAutoRefresh = { ...config.codexQuotaAutoRefresh, [accountId]: result.value };
initialWindowObservations.delete(accountId);
return true;
} catch { return false; }
}
1 change: 1 addition & 0 deletions src/config/schema/leaf-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ const codexQuotaAutoRefreshEntrySchema = z.object({
lastWeeklyResetAt: z.number().finite().nonnegative().optional(),
nextFiveHourResetAt: z.number().finite().nonnegative().optional(),
nextWeeklyResetAt: z.number().finite().nonnegative().optional(),
lastInitialActivationAttemptAt: z.number().finite().nonnegative().optional(),
}).strict();
const CODEX_QUOTA_AUTO_REFRESH_KEY_ERROR =
"quota auto-refresh keys must be a Codex pool-account id or the main Codex account and cannot be reserved JavaScript object keys";
Expand Down
2 changes: 2 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,8 @@ export interface OcxConfig {
/** Observed boundaries retained until activation, even if an idle upstream clock moves. */
nextFiveHourResetAt?: number;
nextWeeklyResetAt?: number;
/** Durable pre-send backoff for initial activation, not a claimed reset or success. */
lastInitialActivationAttemptAt?: number;
}>;
/**
* Selection order per account id, higher used earlier; absent = 0. Keyed by id
Expand Down
2 changes: 1 addition & 1 deletion structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ merge cannot turn them into a valid config while discarding the original bytes.
| Canonical ChatGPT upstream transport | `providers.openai.upstreamWebsocket` | Omitted uses upstream WebSocket when eligible; explicit `false` selects HTTP/SSE without changing the canonical provider identity. `true` is rejected on the canonical row. This is independent of the client-facing `websockets` setting. |
| Provider egress | `providers.<name>.proxy`, `providers.<name>.noProxy` | An absent `proxy` inherits global egress; `"direct"` or `null` forces direct egress; HTTP(S) and SOCKS5(H) URLs select a provider-owned proxy. `noProxy` uses NO_PROXY syntax and sends a matching destination direct across either a provider-owned or inherited global proxy. `src/lib/provider-egress.ts` owns parsing and request-local resolution. |
| Credentials | `apiKeys` | Data-plane only; never admitted to `/api/*`. |
| Lifecycle | `codexAutoStart`, shim/start behavior, resume-history sync, storage cleanup | Startup safety reads these; see [`gui-and-management-api.md`](gui-and-management-api.md). |
| Lifecycle | `codexAutoStart`, shim/start behavior, resume-history sync, storage cleanup, `codexQuotaAutoRefresh` | Startup safety reads these; see [`gui-and-management-api.md`](gui-and-management-api.md). Quota activation's internal `lastInitialActivationAttemptAt` is a pre-send retry fence, not an upstream reset or usage value; see [initial activation](providers/openai-accounts.md#initial-codex-quota-window-activation). |

Env values are resolved through `src/config/proxy-env.ts`, so a config value naming an env var never persists
the secret itself.
Expand Down
2 changes: 2 additions & 0 deletions structure/gui-and-management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ After `PUT /api/native-integrations/claude-desktop` persists its intent, and whe
persisted, the route adopts the committed state into the running server config, because the Claude intercept's
per-request first-party callback reads that live object; a failed write leaves it unchanged.

The quota activation toggle includes [initial unused-window activation](providers/openai-accounts.md#initial-codex-quota-window-activation). Its durable attempt timestamp is internal scheduler bookkeeping, not a new operator control or a claim that a reset has completed. The existing validated opt-in boundary remains unchanged.

| Endpoint area | Responsibility |
| --- | --- |
| Config/settings | Read safe config/settings views; mutate supported settings only. Full `PUT /api/config` is disabled so masked secrets are not round-tripped. `PUT /api/settings` accepts `codexAutoStart`, `streamMode`, integer `appOwnedMemoryBudgetMb` (64..4096), strict boolean `codexAccountPickerEnabled`, strict boolean `fastRows`, and a validated per-account `codexQuotaAutoRefresh` toggle (each optional, at least one required). `fastRows` defaults on when absent: false is persisted, true deletes the key, and successful writes echo the effective boolean. An effective change converges the Codex catalog and refreshes enabled or already-owned client integrations after persistence. Picker enable initializes an empty UI-managed selector map, persists before one bounded catalog convergence, and reports only `catalogRefreshPending`; allocation/save failure restores every touched live field and skips convergence. Budget changes synchronously enforce the process-wide evictable retained-state cap; this is separate from RSS/native memory. `streamMode` persists the #314 stream-shape selection in config.json (Windows services need persisted input; macOS eager relay is explicit-only). |
Expand Down
6 changes: 6 additions & 0 deletions structure/providers/openai-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,9 @@ quota-cache freshness after the existing attempt backoff. Main refresh keeps its
passive intent: cache bypass does not clear an inference reauthentication mark. Other prime reasons
retain their existing cache rules. The split config schema degrades malformed optional values to
false. Exact-account and Direct routes are unchanged.

## Initial Codex quota-window activation

The existing per-account `codexQuotaAutoRefresh` opt-in also covers unused windows. `src/codex/quota-initial-activation.ts` requires two fresh, zero-use observations at least thirty seconds apart whose reset deadline moves with observation time. A fixed deadline at 0% is already counting down and never authorizes initial activation. Only reported five-hour or weekly windows qualify; account-credential generation changes discard the witness. The minute worker may request a second bounded metadata observation, while pause, validation, reauthentication, pool-mode and native-main protection remain authoritative.

Before inference, `lastInitialActivationAttemptAt` is persisted under the existing config mutation lock. This is a five-minute retry fence across restart, not a successful reset marker or a changed usage count. Persistence failure sends nothing, and current on-disk opt-out, provider disable or account pause wins over a stale worker snapshot. The existing warmup routine is reused once per coalesced account attempt. A completed warmup does not claim that the client cache or upstream clock has already changed; subsequent authentic observations retain those responsibilities. The ephemeral witness is owned by `src/codex/quota-auto-refresh-state.ts` and is removed with the account.
Loading
Loading