# PR #22774 evidence fixture matrix (proof audit pass 1)

- Fix: `codexUsageReader.directories` now returns `<sharedHome>/sessions` **and** `<sharedHome>/archived_sessions` (`apps/server/src/provider/Drivers/codexUsage.ts:387`).
- Before = `6490a9e874`. After = `0ce8d72485` (PR head, a merge of main). Between the two, the only change on the usage path is that one hunk. `git diff --stat 6490a9e..0ce8d72` over `apps/server/src/usage`, `codexUsage.ts`, `CodexHomeLayout.ts` and `contracts/src/usage.ts` touches only `codexUsage.ts` and the test file.
- Each directory becomes its own `UsageSource` (fingerprint `resolvedHomePath` = the directory). After the fix, the codex `sources[]` has **two** entries: `sessions` and `archived_sessions`. Before the fix it has one.
- Token total = `uncachedInputTokens + cachedInputTokens + cacheCreationTokens + outputTokens`. Reasoning is a subset of output and is not added.

## Fixture shape (checked against the real `parseCodexLine` at the PR head)

Synthetic `CODEX_HOME` = `<root>/codex`. Each rollout file holds three JSONL lines. `ts` is the current UTC time at creation, so the event falls inside the window and the file's mtime is at least `windowStart`:

```jsonl
{"type":"session_meta","timestamp":"<ts>","payload":{"id":"<sessionId>"}}
{"type":"turn_context","timestamp":"<ts>","payload":{"model":"gpt-5.5"}}
{"type":"event_msg","timestamp":"<ts>","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":<in>,"output_tokens":<out>}}}}
```

- `a` = `sessions/2026/10/22/rollout-ev7090-a.jsonl`, session `ev7090-a`, 100 in / 20 out → **120**
- `b` = `archived_sessions/rollout-ev7090-b.jsonl`, session `ev7090-b`, 300 in / 60 out → **360**

Session IDs must differ. Codex sets `sharedSessionsAcrossFiles: true`, so one session ID found in both dirs (a moved copy) is deduplicated and counted once.

Builder script: `/tmp/ev-22774/audit1/make-fixture.sh <row> <root>`. It creates `<root>/codex`, `<root>/home` (fake `HOME`) and `<root>/t3/userdata/settings.json`.

## Matrix (codex buckets, window = today−29 … today, UTC)

| Row | Fixture (under `<root>/codex`) | Before (6490a9e) | After (0ce8d72) |
|---|---|---|---|
| **A** active only | `a` only. `archived_sessions/` absent | 120 tokens, 1 session. Sources: `sessions` ok (scanned 1) | 120 tokens, 1 session. Sources: `sessions` ok + `archived_sessions` **missing** ("No transcript directory on this environment.") |
| **B** active + archived | `a` + `b` | **120**, 1 session (archived omitted, the bug) | **480**, 2 sessions. Both sources ok, scanned 1 each |
| **C** archived only | `b` only. `sessions/` exists but is empty | **0**, 0 sessions. `sessions` ok, scanned 0 | **360**, 1 session. `archived_sessions` ok, scanned 1 |
| **D1** malformed archived record | `a` + `b` + `archived_sessions/rollout-ev7090-bad.jsonl` (token_count 50/10 with `total_tokens:1`, so it fails the `total ≥ in+out` filter) | 120, 1 session. The bad file is never seen | 480, 2 sessions. `archived_sessions` **partial**, skippedFiles 1, malformedRecords 1, message "1 malformed or inconsistent usage record(s) were skipped…" |
| **D2** truncated / invalid-JSON archived file | `a` + `b` + `archived_sessions/rollout-ev7090-trunc.jsonl` (one line `{"type":"event_msg","payload":{"type":"token_count"`) | 120, 1 | 480, 2. `archived_sessions` **ok**, skippedFiles 1, malformed 0 (a JSON parse failure is ignored silently, by design) |
| **D3** unreadable archived file | `a` + `b` + `archived_sessions/rollout-ev7090-locked.jsonl` (999/1) `chmod 000` | 120, 1 | 480, 2. `archived_sessions` **partial**, skippedFiles 1, message "1 transcript path(s) could not be read; usage may be incomplete." The locked file's 1000 tokens are absent |
| **D4** unreadable archived subdir | `a` + `b` + `archived_sessions/2026/` `chmod 000` holding a 999/1 rollout | 120, 1 | 480, 2. `archived_sessions` **partial** (readdir failure → `failedPaths`), same message |

The D rows are all expected to return the summary with no `UsageReadError` and no server crash. D3 and D4 need a non-root user (this box runs as `ubuntu`). Root would ignore `chmod 000`.

Optional row **E** (warm-cache masking, from the PR body): on Before, scan with `a`+`b` both in `sessions/` (480), then `mv` b into `archived_sessions/` and call again on the **same** server/T3 home: still 480, because the warm `fileCache` keeps it. That is why every row above needs a fresh T3 home.

## Reaching each row at runtime

- **RPC**: WS method `server.getUsageSummary` (`WS_METHODS.serverGetUsageSummary`, `packages/contracts/src/rpc.ts:510`, handler `apps/server/src/ws.ts:2412` → `UsageService.readSummary`). Payload `UsageSummaryInput`: `{ sinceDay:"YYYY-MM-DD", untilDay:"YYYY-MM-DD", timeZone:"UTC", awaitRefresh:true }`. Scope: `AuthDiagnosticsReadScope` (`RpcAuthorization.ts:107`), which a desktop-bootstrap admin token satisfies. No HTTP equivalent.
- **Pointing at the fixture**: `<t3Home>/userdata/settings.json` = `{"providerInstances":{"codex":{"driver":"codex","enabled":false,"config":{"homePath":"<root>/codex","shadowHomePath":""}}}}`. An explicit `homePath` with no shadow gives `resolveCodexHomeLayout` direct mode, so `sharedHomePath` = the fixture. `usageInstances` ignores `enabled`, so `enabled:false` still scans the fixture and keeps the server from probing or spawning the codex CLI against it. Also run the server with `HOME=<root>/home`: `HostProcess.HomeDirectory` = `os.homedir()`, so every unconfigured provider (claude, grok, …) falls back to the empty fake home and never reads the real `~/.claude` or `~/.codex`. `CODEX_HOME` env is only consulted when `homePath` is empty and setupMode ≠ managed. It is set to the fixture anyway as a belt-and-braces measure.
- **Cache**: the scan cache lives in memory (`fileCache`) plus `<t3Home>/userdata/usage-scan-cache-v8.json`. Use a **fresh `<root>` (fresh T3 home + process) per row and per build**: 8 boots for A, B, C, D1 across Before and After, plus D2–D4 as wanted. Do not reuse a T3 home between Before and After either.
- **Auth**: copy `apps/server/scripts/verify-background-live.ts`. Spawn `node apps/server/src/bin.ts start --bootstrap-fd 3` with the bootstrap JSON `{mode:"desktop",noBrowser:true,port,t3Home,host:"127.0.0.1",desktopBootstrapToken:<uuid>,…}` on fd 3. Wait for `startup phase: complete` (needs `--log-level debug`). Then `POST /oauth/token` (token-exchange, subject_token_type `urn:t3:params:oauth:token-type:environment-bootstrap`) → bearer, then `POST /api/auth/websocket-ticket` → `ws://…/ws?orchestrationProtocol=<v>&wsTicket=<t>`, then `RpcClient.make(WsRpcGroup)`. No pairing URL, no dev runner, no Vite. All of these exist at the Before SHA too (checked).
- **Client**: `/tmp/ev-22774/audit1/usage-client.mjs <worktree> <root>/t3 <root>/codex`. It lives outside the worktree and resolves `effect/*` via `createRequire(<wt>/apps/server/package.json)` and contracts via `<wt>/packages/contracts/src/index.ts`. Both were checked to load one shared `effect@4.0.5` instance on Node 24.22. It boots the server, makes one call, prints `{codexTokens, codexDistinctSessions, sources[]}` and kills the process group by the PID it spawned.

## Recipe

```bash
S=/tmp/ev-22774/audit1
# Before checkout (none exists; ~/wt/issue-7090 is gone). Needs deps:
git -C ~/wt/pr-22774 worktree add --detach /tmp/ev-22774/before 6490a9e8748aef55d04d3f4f61b77fba90b4cf1b
(cd /tmp/ev-22774/before && vp i)
for build in before:/tmp/ev-22774/before after:$HOME/wt/pr-22774; do
  name=${build%%:*}; wt=${build#*:}
  for row in A B C D1 D2 D3 D4; do
    root=/tmp/ev-22774/run/$name-$row; rm -rf "$root"; $S/make-fixture.sh $row "$root" >/dev/null
    node $S/usage-client.mjs "$wt" "$root/t3" "$root/codex" > "$root/result.json" 2> "$root/client.err"
    echo "$name $row $(node -e 'const r=require(process.argv[1]);console.log(r.codexTokens,r.codexDistinctSessions,r.sources.map(s=>s.dir.split("/").pop()+":"+s.status).join(","))' "$root/result.json")"
  done
done
# D3/D4 cleanup: chmod -R u+rwx /tmp/ev-22774/run/*-D3 /tmp/ev-22774/run/*-D4
```

## Risks / substitutions

1. **Rates fetch**: the first summary loads LiteLLM pricing over the network. Offline it degrades to `pricing.status: unavailable` and `costSource: unpriced`. Token totals are unaffected, so assert on tokens, not cost.
2. **Startup marker** is a debug log line, so `--log-level debug` is required. If startup hangs (for example on the persisted runtime-state check), the server log is at `<root>/server.log`.
3. **Before needs its own checkout + `vp i`**, which adds a worktree entry to the shared `.git`. Substitute if that is not allowed: the PR's regression test, run on Before with the fix hunk reverted, already shows 120 vs 480 and 0 vs 360 (PR body). That is unit-level, not runtime.
4. **`chmod 000` rows** (D3, D4) are meaningless as root, and they leave unreadable dirs behind. Clean them up as shown above.
5. **The UI path** (`/usage` page) uses the same RPC. If browser evidence is wanted, it can reuse the same fixture roots via `vp run dev --home-dir <root>/t3` with `HOME=<root>/home`. That uses the stateDir `<root>/t3/userdata` because `--home-dir` is explicit. Treat this as an assumption to verify. A fresh home per row still applies.
6. **D2 is not "skipped with a warning"**: invalid JSON is dropped silently (`parseCodexLine` returns null without counting it as malformed), so the source stays `ok`. That is by design, not a defect of the PR. D1 is the row that shows the `partial` warning.
