Skip to content
Merged
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
24 changes: 24 additions & 0 deletions devlog/_plan/260912_accounts/050_history.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,27 @@ Reflection REF-04: fixed aggregate bounds: 64 account identities, 4096 rows, 2 M
A1 accepted: native main history is deliberately NOT hydrated from disk in this slice. It can be sampled in-process only after identity observation and cleared on identity change; persistence omits __main__. Pool history envelopes bind a stable private publication UUID; hydration prunes identity mismatches, while ordinary generation changes on refresh retain prior observations. This avoids attributing offline identity replacements to an old main label. Acceptance explicitly covers main replacement while stopped and account-id reuse. Main cross-restart history remains a documented limitation; bounded durable history is provided for stored pool accounts.

P refinement depends on new048 history-identity cycle. Adopt HIST-01..06: generation gates each physical sample; private random publication UUID persists through refresh and changes on explicit save. Capture PoolQuotaWriter before upstream calls, refresh it after replay token resolution, and forward through every WHAM/WS/HTTP/compact/warmup path. Omit staged login/reauth samples until first post-publication fenced observation; do not retrofit ambient provenance. Native main is excluded from durable endpoint/capacity in this slice. Raw QuotaObservation carries observedAt, wham|response-header source, bounded windows with account|spark family and short|weekly|monthly name, percentage/resetAtMs/duration/primary provenance; no arbitrary upstream label. Envelope private identity binds samples but is omitted from read DTO. Retain best-effort single-writer atomic cache semantics; no multi-process merge/durability claim. Read endpoint GET /api/codex-auth/quota/history?accountId=<poolid>&limit=<1..200>; CLI ocx account history openai <poolid> [--limit N] [--json]. Unknown/deleted404, invalid/duplicate selector400, emptyhistory200. No upstream call on reads.

## Executable history child contract after identity foundation D

Parent PR4375/e9007429c5 provides PoolQuotaWriter and store capture/live/retention helpers. This child depends on that branch; the capacity child follows this one. Previous D delivered only identity and deferred hosted proof.

NEW src/codex/quota-history.ts, pure leaf (imports quota types and pure account-id only): closed HistoryWindow family account|spark, window short|weekly|monthly, usedPercent, optional resetAtMs/windowSeconds/monthlyIsPrimaryWindow; HistorySample observedAt/source/credentialGeneration/windows; private envelope identity/samples. CodexQuotaHistory owns append/hydrate/read/clear/reconcile/serialize. Keep 200 samples/account,30days,64accounts,4096samples,2MiB conservative serialized-byte budget; max5 windows/sample. Track per-sample byte costs incrementally, evict by observedAt then accountId and insertion order. Hydration rejects an over-limit envelope before admitting rows (>64 accounts, >200 rows/account, >4096 total samples or >2MiB serialized payload); accepted rows are validated and sorted by timestamp before age retention. Unknown fields/labels never survive. Read returns deep copies; private identity never reaches API. No filesystem/config/store import in the leaf.

MODIFY quota.ts: own the history instance and optional history:{version:1,accounts:{...}} in existing quota-cache version1. Hydrate history before latest-quota six-hour TTL filtering; native-main never hydrates/records in this durable layer. Replace unbounded file allocation with a local fd/readSync loop capped at4MiB+1; oversized/corrupt cache is a cache miss, never an inference failure. Keep latest in-memory state untouched. Existing debounced atomic persistence serializes bounded history, so no new timer/store and no multi-process merge claim. Clear and roster reconcile remove history-only identities too; read compares current store UUID before returning, even after offline replacement.

setAccountQuotaFromParsed gains optional sixth QuotaObservationEvidence {writer,observedAt,source,raw}. After config/main write guards, append only when writer.accountId matches and isPoolQuotaWriterLive. Convert only fresh raw percentages into closed history windows, normalizing resets with resetAtToMs. Account short/weekly/monthly map directly; Spark uses existing short label plus a new canonical weekly-label constant shared with the WHAM parser. No arbitrary custom labels. Credits-only/metadata-only updates append nothing. The legacy latest-snapshot merge remains unchanged. applyAccountQuotaFromUpstreamHeaders options adds poolWriter; builds evidence from original parse result BEFORE custom-window carry. Missing writer/evidence preserves latest cache but appends no trusted sample.

MODIFY auth-context.ts pool union with poolQuotaWriter?:PoolQuotaWriter, capture immediately after getValidCodexToken before dispatch. MODIFY core.ts WS closure, rejected-first response, ordinary HTTP, and refreshedAuthCtx to forward/re-capture exact serving writer; compact.ts refresh/rejection follows same rule. MODIFY quota-auto-refresh.ts pool warmup captures before I/O. MODIFY auth-api.ts WHAM initial and refreshed replay capture before fetch, commitPoolQuotaResponse carries writer and sends raw parsed result with observedAt after JSON read; keep all current generation/mayPublish checks. Staged login quota writes intentionally omit history until a post-publication observation; native main and legacy updateAccountQuota omit it. No token material is added to response objects/logs.

GET /api/codex-auth/quota/history?accountId=<poolid>&limit=<1..200> is read-only cached data, no upstream/auth refresh/warmup. Add before existing /quota handler; registry entry+capability map. Validate exactly one accountId, optional single numeric limit and no unknown query fields. Invalid/main400, unknown configured pool404, known account200 even empty. DTO: {accountId,observations:[{observedAt,source,windows}],retention:{maxObservations:200,maxAgeDays:30},truncated:boolean}; omit UUID and credential generation. Public array follows ascending observed time, limit chooses newest rows. Capacity is added only in next child.

NEW src/cli/account-history.ts exports cmdAccountHistory(args,deps). Shape `ocx account history openai <poolid> [--limit N] [--json]`; reject other provider/main/extraargs before any network. Use resolveBaseUrl/apiJson/apiError/proxyUnreachable from account-api owner. JSON prints DTO; human output prints observed time/source/window/percent/reset and no-observation state. Wire lazy dispatcher and help/capabilities; source-only skill surface generator allowed (not product suite).

Tests: new pure codex-quota-history.test.ts (register both layout maps), existing quota-store integration hydration harness for raw-vs-carried, writer mismatch/refresh/replacement, stage omission, native omission, clear/reconcile and disk limits; authenticated server route tests+CLI transport fixture. No local runtime execution. All touched source-area ownership docs and English+Korean account command docs synchronized; other translations must not contradict additions.

Read unavailability refinement: undefined current identity (legacy/missing/unreadable) returns empty/unavailable evidence without deleting a retained envelope. Only a confirmed different UUID or authoritative roster removal clears it; this avoids transient permission/read errors destroying history. Restored matching identity may expose retained valid rows again. Cache eviction/expiry remains bounded.

Deferred history-plan review findings (actual A entry was refused because persisted active work phase is tun): reuse pre-clamp invalid-percentage checking for all WHAM primary/secondary/tertiary and additional Spark windows, and response-header raw usage fields; any invalid numeric/nonfinite/out-of-range percentage omits the ENTIRE trusted observation while leaving legacy display behavior unchanged. Add before-clamp history parser/evidence guard so clamped values cannot masquerade as measured percentages. Hydration rejects an entire over-limit history payload (>64accounts,>200rows/account,>4096rows,>2MiB) instead of slicing by lexical key/array position; bounded accepted rows are sorted by observedAt before retention. Tests include65th-newestaccount and unordered rows. These need fresh independent A review when history resumes.

Implementation review HIST-IMPL-01 accepted: compact final response now records actualoutcomeCtx poolwriter beforebuffering, coveringordinary/401replay/alternate; rejectedfirstaccount retains its separateexistingwrite, so everyresponse contributesonce. Add compactregression withquotaheaders onoriginalsuccess andA429→Bsuccess. This sourcework is user-authorized whilehostgoal remainsblocked; no FSM A/B/C/D advancement claimed.
9 changes: 9 additions & 0 deletions devlog/_plan/260912_accounts/051_history_delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Bounded raw quota history implementation

Extends publication identity foundation #4375 with a pure bounded history leaf, existing quota-cache persistence, fenced WHAM/HTTP/WS/compact/warmup producers, a management read route and account history CLI. Invalid upstream percentages never become trusted samples after display clamping. Native-main, staged-login and legacy unproven setters are omitted.

Regression sources cover chronological retention, limits/corrupt disk, private-field stripping, generation/identity changes, raw-versus-carried windows, cached API auth/validation, CLI argument rejection and compact serving-account attribution. Independent source review identified missing compact final-response capture; it was added with ordinary/alternate regressions. CLI skill surface regenerated by its source-only generator, not a product build or suite. Local suites/build/typecheck/install NOT RUN.

This child targets the existing history-identity branch at19cbe826d8. The pending plan-only commit was rebased onto the parent-updated branch; foundation product bytes were unchanged. Host goal remains blocked; actual FSMB(tun) remains untouched under explicit user instruction. These are authorized source implementation and independent reviews, not a claimed new persisted PABCD cycle. Complete hosted verification belongs to the eventual cumulative history/capacity tip; no merge or issue closure.

Review corrections: human CLI formats out-of-range dates as unknown; byte-limit fixtures now carry valid populated data and exercise append-byte eviction before row limits; authenticated API returns a populated sanitized history; WHAM refresh/replay, HTTP/WS and real warmup producer fixtures assert history including stale WS replacement rejection. Local suites remain NOT RUN.
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,9 @@ ocx models remove deepseek/deepseek-v4 --yes
슬래시가 있는 모델 선택기는 라우팅됩니다(`anthropic/claude-opus-5`). 슬래시가 없는 id는 native OpenAI 모델로 취급되므로, 라우팅된 것처럼 보일 수 있는 id에 대해 그 읽기를 강제하려면 `--native`가 필요합니다.

`--modalities`는 `text`, `image`, `audio`만 허용합니다. Codex는 이 필드를 닫힌 enum으로 해석하고 다른 값이 하나라도 있으면 카탈로그 전체를 거부하므로, `add`, `edit`, 관리 API는 나중에 카탈로그 작성기가 정리해야 할 값을 저장하지 않도록 잘못된 값을 바로 거부합니다(#759).

### 저장된 쿼터 기록

`ocx account history openai <pool-account-id> [--limit 1-200] [--json]`은 제공자에게 요청하지 않고 저장된 관측을 읽습니다. 관측 시각, WHAM·응답 헤더 출처, 한도 종류와 사용률을 구분해 표시합니다. 계정마다 최대 200개를 30일간 보관하며 전체 저장량에도 제한이 있습니다.

일반 토큰 갱신은 기록을 유지합니다. 재로그인·삭제·계정 교체는 이전 기록과 분리합니다. 네이티브 메인 계정과 로그인 저장 전 조회는 포함하지 않습니다. 기록이 없다는 것은 관측 부족이며 사용량 0을 뜻하지 않습니다. 이 명령은 토큰 용량을 추정하거나 쿼터를 소비하지 않습니다.
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,9 @@ otherwise look routed.
and rejects an entire catalog containing any other value, so `add`, `edit`, and the management API
all refuse the bad value rather than storing something the catalog writer would have to strip later
(#759).

### Cached quota history

`ocx account history openai <pool-account-id> [--limit 1-200] [--json]` reads stored observations without contacting the provider. The output separates actual observation time, WHAM or response-header source, window family and usage percentage. At most 200 observations per account are retained for 30 days, with global storage bounds.

Ordinary token refresh preserves history. Reauthentication, removal or account replacement retires the old publication. Native main and probes performed before a login is published are not included. Missing history means insufficient observations, not zero usage. This command does not estimate token capacity or spend quota.
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
"codex-prompt-text-probe.test.ts": "codex-integration",
"codex-quota-auto-refresh-main-admission.test.ts": "codex-integration",
"codex-quota-auto-refresh.test.ts": "codex-integration",
"codex-quota-history.test.ts": "codex-integration",
"codex-quota-parser-parity.test.ts": "codex-integration",
"codex-quota-prime.test.ts": "codex-integration",
"codex-quota-rejection.test.ts": "codex-integration",
Expand Down
19 changes: 18 additions & 1 deletion skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,23 @@ Recently detected quota resets and whether reset notifications are enabled.

JSON mode: `payload`.

### `ocx account history`

Cached quota observations for one stored Codex pool account.

| Method | Route |
|---|---|
| GET | `/api/codex-auth/quota/history` |

| Flag | Value | Meaning |
|---|---|---|
| `--json` | boolean | Emit the bounded observation history. |
| `--limit` | number | Return the newest 1 to 200 observations. |

JSON mode: `payload`.

- Use account history openai <pool-account-id>. Reads cached observations only; no refresh or warmup. Native main is not included.

### `ocx account list`

Codex OAuth accounts with pool priority and pause state.
Expand Down Expand Up @@ -769,6 +786,6 @@ JSON mode: `payload`.

## Counts

- declared capabilities: 41
- declared capabilities: 42
- of those, state-changing: 20
- head-resolved invocations: 2
45 changes: 45 additions & 0 deletions src/cli/account-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isValidCodexAccountId } from "../codex/account-id";
import { apiError, apiJson, proxyUnreachable, resolveBaseUrl, type AccountDeps } from "./account-api";

function historyDate(value: unknown): string {
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
const date = new Date(value);
return Number.isFinite(date.getTime()) ? date.toISOString() : "unknown";
}

/** Read cached pool observations without refreshing credentials or spending quota. */
export async function cmdAccountHistory(args: string[], deps: AccountDeps): Promise<number> {
const [provider, accountId, ...flags] = args;
let json = false;
let limit = 200;
let hasLimit = false;
let valid = provider === "openai" && isValidCodexAccountId(accountId);
for (let index = 0; index < flags.length; index++) {
if (flags[index] === "--json" && !json) json = true;
else if (flags[index] === "--limit" && !hasLimit && /^(?:[1-9]|[1-9][0-9]|1[0-9]{2}|200)$/.test(flags[index + 1] ?? "")) {
limit = Number(flags[++index]); hasLimit = true;
} else valid = false;
}
if (!valid) {
console.error("Usage: ocx account history openai <pool-account-id> [--limit <1-200>] [--json]");
return 1;
}
const baseUrl = await resolveBaseUrl(deps);
if (!baseUrl) return proxyUnreachable();
const result = await apiJson(deps, baseUrl, "GET", `/api/codex-auth/quota/history?accountId=${encodeURIComponent(accountId)}&limit=${limit}`);
if (result.status === 0) return proxyUnreachable(result.transportError);
if (result.status !== 200) return apiError(result.json, "Quota history unavailable", result.status);
if (json) { console.log(JSON.stringify(result.json, null, 2)); return 0; }
const observations = result.json.observations;
if (!Array.isArray(observations)) return apiError({}, "Invalid quota history response", 502);
console.log("OBSERVED\tSOURCE\tWINDOW\tUSED\tRESET");
if (!observations.length) console.log("No quota observations for this credential publication.");
for (const observation of observations) {
if (!observation || typeof observation !== "object" || !Array.isArray(observation.windows)
|| !Number.isFinite(observation.observedAt)) return apiError({}, "Invalid quota history response", 502);
for (const window of observation.windows) {
console.log(`${historyDate(observation.observedAt)}\t${observation.source}\t${window.family}/${window.window}\t${window.usedPercent}%\t${historyDate(window.resetAtMs)}`);
}
}
return 0;
}
5 changes: 5 additions & 0 deletions src/cli/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const REPLACEMENT_STYLE_OAUTH = new Set<string>();

const ACCOUNT_USAGE = `Usage:
ocx account list [provider] [--json] [--all] [--quota [--refresh]]
ocx account history openai <pool-account-id> [--limit <1-200>] [--json]
ocx account current <provider> [--json]
ocx account use <provider> <account-or-key-id|main> [--json]
ocx account refresh <provider> [--json]
Expand Down Expand Up @@ -335,6 +336,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
const [sub, ...rest] = args;
try {
if (sub === "list") return await cmdList(rest, deps);
if (sub === "history") {
const { cmdAccountHistory } = await import("./account-history");
return await cmdAccountHistory(rest, deps);
}
if (sub === "current") return await cmdCurrent(rest, deps);
if (sub === "use") return await cmdUse(rest, deps);
if (sub === "refresh") return await cmdRefresh(rest, deps);
Expand Down
12 changes: 12 additions & 0 deletions src/cli/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@ export const CAPABILITIES: readonly Capability[] = [
"Headless services usually have no unlocked keychain session; prefer ${ENV_VAR} references there.",
],
},
{
command: ["account", "history"],
summary: "Cached quota observations for one stored Codex pool account.",
routes: [{ method: "GET", path: "/api/codex-auth/quota/history" }],
flags: [
{ name: "--json", value: "boolean", summary: "Emit the bounded observation history." },
{ name: "--limit", value: "number", summary: "Return the newest 1 to 200 observations." },
],
mutates: false,
json: "payload",
details: ["Use account history openai <pool-account-id>. Reads cached observations only; no refresh or warmup. Native main is not included."],
},
{
command: ["account", "list"],
summary: "Codex OAuth accounts with pool priority and pause state.",
Expand Down
Loading
Loading