# Proof audit pass 2 — issue23104

**Verdict: SUFFICIENT** for the bounded claim: on the same isolated native OpenCode 2.1.2 DB, the actual web Usage view counts one imported assistant response once after fork and fork-of-fork (with a directory move). Before the fix it counted three copies.

Worktree `/home/ubuntu/wt/issue-23104`, branch `fix/issue-23104`.
Exact head: `77a99086b929fe2024990efaa77b6e2e33825131`. Exact base: `968dd51704441138559a3fe9c348719f2b11e0c6`. merge-base(HEAD, base) = base. `git status --porcelain` is empty. The diff touches only three files: `packages/provider-opencode/src/server/usage{.ts,.test.ts,.fixture.ts}`. No UI code changed.

## Independently verified in this pass

| Check | Result |
|---|---|
| Source provenance | `git show base:usage.ts` SHA-256 = `0d920de0…4deaf`, which matches `sourceSha256` on all 4 "before" captures. `git show HEAD:usage.ts`, the working-tree file, `evidence/head-usage.ts` and `usage.fixed.ts` all = `e07d6723…dd0b3`, which matches all 4 "after" captures. The working tree is restored to exact head. |
| Backup DB | `recorded-opencode.db` SHA-256 = `94df4982…3066e1` (matches). |
| Live isolated DB vs backup | Read-only (`mode=ro`). Live `/tmp/triage-23104/native/data/opencode/opencode.db` (WAL is 0 bytes) and the backup have identical ordered `session_v2` (4 rows) and `session_message` (8 rows) contents. I used my own row hash: they agree with each other, but the hash method differs from the manifest's, so the hex values are not comparable. quick_check=ok on both. `session_pending` and `session_inbox` are empty. |
| Row content | Exactly 3 assistant rows: the original `msg_usageproofassistant0000001`, the fork copy `…GxURO0NU4GY4Az_2` and the fork-of-fork copy `…MA7a5GgmyUywMf_2`. All have seq=2, time_created=1792619988284, tokens 1000 in / 300 cache-read / 200 out, cost 0.01, model claude-sonnet-4-5. `fork_session_id` and `through` boundaries form the chain DJeK → 99bP → LjFktF. The fork-of-fork has a different directory and project (`/tmp/triage-23104`), plus a location-switched row and an idle row. No other assistant or user rows exist after the import, so there was no model request. One extra empty session (`…DJeJ`) has no messages. `native-api.py`/`fork-proof.py` call only `/session`, `/experimental/session/import`, `/session/{id}/fork` and export endpoints; there is no prompt/message endpoint. |
| 8 stills | SHA-256 of all 8 PNGs matches `manifest.json`. I viewed all eight. Before: 4.50K, 3 sessions, $0.03, cached 900 / uncached 3K / output 600, y-axis to 6K. After: 1.50K, 1 session(s), $0.01, cached 300 / uncached 1K / output 200, y-axis to 1.50K. Filters are OpenCode / Tokens / 30 days. Light and dark are as labelled. The sidebar is collapsed, with no thread titles or private data visible. 1440 stills: toggle bars and controls are present. 390 stills: responsive layout with select dropdowns. |
| Numeric delta | 4500 = 3000 + 900 + 600 and 1500 = 1000 + 300 + 200 = 3 × 1 copy. The delta is −3000 / −2000 / −600 / −400 / −$0.02 / −2 sessions, which is internally consistent and agrees with the DB rows. |
| Raster sizes | Desktop PNGs are 1280×800 and narrow PNGs are 780×1800, matching the manifest. The evidence labels 1440/390 as CSS viewports (evaluated innerWidth, DPR 2) and explicitly says desktop PNGs are preview-resampled, not 1440 px. Not mislabelled. 780×1800 / DPR 2 = 390×900 CSS, which is consistent. 1280×800 has the same 1.6 aspect as 1440×900. |
| Pixel diff | `pixel-differences.json` reproduces from `build-evidence.py` on the local files. The bounds cover the headline, chart and totals regions only. |
| Public copies | I fetched all 13 public URLs read-only (index, manifest, 8 PNGs, pixel-differences, transcript, proof-pass1). The bytes are SHA-identical to the local files. A grep for password, pairing, token=, secret, authorization, cookie and Basic auth found nothing. `native.log`, `dev.log` and the server.log password were not published. |
| Transcript honesty | `proof-transcript.md` labels `reader-main.json` as the earlier TWO-copy stage (2600/400/$0.02) and separates it from the three-copy screenshot baseline. `red.log` shows the new test failing on base for the intended reason: 4 records vs 2 expected. |
| Reviews | Codex r1 and Claude r1 both returned MERGE at the exact head. |

I did not rerun tests, lint or typecheck. Pass 1 independently ran 8/8, lint and package typecheck at this exact head, and nothing has changed since (same head, clean tree).

## Claims are bounded correctly

- Dedupe applies only to copies that native fork lineage identifies: fork session/boundary plus the parent's seq at or before the boundary, created before the fork. Genuinely new post-fork replies keep their usage, and the reviewers confirmed the continuation and sibling tests. Usage is not redefined: dedupeKey and totals of the surviving records are unchanged.
- Legacy V1 message-table and JSON paths are unchanged.
- Usage and cost are a synthetic imported fixture. The fork, move and import APIs are real native 2.1.2 calls. No billed live-provider turn happened.

## Material remaining risks and non-blocking notes

1. Deleted ancestors, a missing middle fork, or a missing boundary can still overcount, because the code falls back to counting surviving rows. This is disclosed and is not a regression.
2. Codex r1 nits: there are no direct tests for a `before` boundary or for seq reuse after a revert. The correlated MAX/CASE predicate may scan many parent rows in large fork-heavy histories. Nobody ran a benchmark.
3. Unverified surfaces: Electron, native iOS/Android, remote/relay/tunnel, other providers, and real billed OpenCode turns. The 390 stills are responsive web, not mobile.
4. Cosmetic: the 390 stills crop off before the Breakdown model row. The cost is still visible in the provider row. The public `proof-pass1.md` references local `/tmp` paths to earlier 1280 stills that were not published, and it says so. The index's "Model: gpt-6.1-sol. Harness: Codex." line describes the evidence capturer. The PR body still needs its own final model/harness line.
5. Screenshots cannot cryptographically prove capture-time code. Provenance rests on the recorded source SHAs and the consistency of the DB, reader and tests. Those all agree.

No code edits, commits, pushes, forge actions, browser sessions, dev servers, subagents or uploads were done. Only this file was written. Evidence artifacts are unchanged.
