[Bug]: Usage counts OpenCode session replays from forks/continuations once per copy (2.45x token, 2.61x cost inflation)#23104

opened25d agobyPPrpCena

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

  1. Use T3 Code with the OpenCode provider (opencode serve) as the only enabled provider instance.
  2. Fork a thread, or continue a thread into a new worktree, so T3 Code replays the prior conversation into a new OpenCode session.
  3. Open Settings → Usage (Cmd+U) and switch to the token or cost view.
  4. 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:

js
1dedupeKey: 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):

groupcopies
117
211
39
49
56
66

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.

metricreportedde-duplicatedinflation
assistant messages51.77K25.28K2.05x
uncached input tokens504.76M210.27M2.40x
cached input tokens12.60B5.13B2.45x
output tokens23.87M11.81M2.02x
total input tokens13.10B5.35B2.45x
cost (priced models only)$429.72$164.722.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:

text
1ses_f1fdf6bd2ffeO3XZxPsTKfz1bW msgs=1105 in=23,626,030 cacheRead=578,634,187
2ses_f20087207ffeEaZLoYhzULT6Yi msgs=1079 in=23,477,532 cacheRead=577,540,551
3ses_f25b1265effeWrT3qcA6XMFPtz msgs=1076 in=22,418,649 cacheRead=576,773,427
4ses_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_created and 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 sessions counter already carries a per-bucket Set for 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 dedupeKey legitimately 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.
  • #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: opencode only; codex, claudeAgent and antigravity disabled

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.