[Bug]: Usage counts OpenCode session replays from forks/continuations once per copy (2.45x token, 2.61x cost inflation)#23104
Summary
The Usage view over-reports OpenCode token and cost totals, sometimes by a lot. A single conversation that T3 Code replays into a new OpenCode session is counted once per replay, because the OpenCode usage reader dedupes on the OpenCode message id, and every replayed message gets a fresh id.
On the machine below, 32,700 of 51,772 counted assistant messages are duplicates. Input tokens are inflated 2.45x and cost 2.61x.
Area
apps/server
Steps to reproduce
- Use T3 Code with the OpenCode provider (
opencode serve) as the only enabled provider instance. - Fork a thread, or continue a thread into a new worktree, so T3 Code replays the prior conversation into a new OpenCode session.
- Open Settings → Usage (Cmd+U) and switch to the token or cost view.
- Compare the totals against the raw per-message records.
Expected: every assistant message is counted once, so the totals match the raw records.
Actual: each replayed conversation is counted once per copy. One conversation was counted 17 times.
Diagnosis
readOpenCodeUsage (apps/server/src/usage/) reads each OpenCode session's messages from the message / session_message tables of opencode.db (and the legacy storage/message/*.json files). parseOpenCodeMessage produces a record with:
| 1 | dedupeKey: id ? `opencode:${id}` : null |
UsageAggregator.add then dedupes on that key only, across the whole scan.
The problem is that when T3 Code forks or continues a thread, it replays the entire prior conversation into a new OpenCode session. The replayed messages are the same messages — same content, same time_created — but OpenCode assigns them new message ids, so opencode:<message id> never matches and every copy is added to the buckets.
The ids are structured as msg_<12-char session prefix><3-char sequence><14-char random>. The session prefix differs per replay and the random tail differs per message, so the id carries nothing that identifies the replayed original.
Measured on the machine below, grouping sessions that share a first-message timestamp (and confirming the grouping is a replay, not a coincidence):
| group | copies |
|---|---|
| 1 | 17 |
| 2 | 11 |
| 3 | 9 |
| 4 | 9 |
| 5 | 6 |
| 6 | 6 |
Within one group of 4 sessions, the first 6 messages were compared across sessions: 6/6 identical message text hashes and identical time_created values, with 0 shared message ids.
Impact
Major degradation or frequent failure — the headline cost and token numbers are wrong, and they are wrong upward, so the Usage view cannot be used to decide whether a model is affordable.
| metric | reported | de-duplicated | inflation |
|---|---|---|---|
| assistant messages | 51.77K | 25.28K | 2.05x |
| uncached input tokens | 504.76M | 210.27M | 2.40x |
| cached input tokens | 12.60B | 5.13B | 2.45x |
| output tokens | 23.87M | 11.81M | 2.02x |
| total input tokens | 13.10B | 5.35B | 2.45x |
| cost (priced models only) | $429.72 | $164.72 | 2.61x |
Per month: 2026-09 reports $370.78 vs $118.97 de-duplicated; 2026-10 reports $58.95 vs $45.75.
The worst-affected model was over-reported 3.07x.
Expected behavior
Each underlying assistant message contributes to the Usage totals once, regardless of how many OpenCode sessions contain a replay of it.
Actual behavior
Every replay copy is counted separately, because the de-duplication key is the OpenCode message id and a replay is issued new ids.
Logs or stack traces
The OpenCode provider's own log shows the replay as separate sessions with a shared origin:
| 1 | ses_f1fdf6bd2ffeO3XZxPsTKfz1bW msgs=1105 in=23,626,030 cacheRead=578,634,187 |
| 2 | ses_f20087207ffeEaZLoYhzULT6Yi msgs=1079 in=23,477,532 cacheRead=577,540,551 |
| 3 | ses_f25b1265effeWrT3qcA6XMFPtz msgs=1076 in=22,418,649 cacheRead=576,773,427 |
| 4 | ses_f200902afffeXzwV4nlPrXnOWc msgs=1050 in=22,280,945 cacheRead=575,947,466 |
All four share the same first-message time_created (2026-09-24T16:28:29.214Z) and the same title, and share no message ids.
Possible fix directions
- Make the OpenCode de-duplication key survive a replay. The replayed messages keep their original
time_createdand content, so a key derived from content (e.g. hash of the text part plus token totals) or from(session origin, message ordinal)would collapse the copies. - Alternatively, de-duplicate at the session level: sessions that share an origin (same first-message timestamp and title) could be collapsed to one before aggregation, and the
sessionscounter already carries a per-bucketSetfor exactly this kind of thing. - The scanner already acknowledges the analogous Claude Code case ("Claude Code copies a message's records forward when a session is resumed or forked, so the same
dedupeKeylegitimately appears in several transcripts"). The same class of problem exists for OpenCode session replays, but there the id changes, so the existing key does not help.
Related issues
- #17071 — Usage counts most Antigravity calls three times (step, generation and retry copies are all added). Same class of problem, different provider and different mechanism.
- #20246 — Claude usage undercounts subagent output because dedupe keeps the first partial copy of each message. Dedupe-related, but it undercounts rather than overcounts, and it is Claude-specific.
- #7858 — Usage double-counts a provider transcript directory shared between Windows and a WSL environment. Different cause (source fingerprint, not message de-duplication).
I searched the tracker and did not find an issue covering OpenCode session replays being counted once per copy.
Environment
- macOS 27.0.1 (26A434), arm64
- T3 Code Nightly
0.1.1-nightly.20261020.3411(d36b7da2485b) - opencode v2.1.2 (
opencode serve --hostname=127.0.0.1 --port=…) - Provider instance:
opencodeonly;codex,claudeAgentandantigravitydisabled
Workaround
None for the totals. The de-duplicated figures can be reconstructed by grouping sessions that share a first-message timestamp and keeping only the largest session in each group; that is how the numbers above were produced.