Skip to content

fix(stats): count forked sessions' copied messages once - #312

Merged
slkiser merged 3 commits into
mainfrom
t3code/f2828c3a
Oct 3, 2026
Merged

slkiser merged 3 commits into
mainfrom
t3code/f2828c3a

Conversation

@slkiser

@slkiser slkiser commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • When OpenCode forks a session, it copies the parent's finished messages into the fork with new ids but the same times, model, tokens and cost. The all-history, time-window and multi-session readers counted each copy again, so forking a $10 session without using the fork showed $20 in token/cost totals (/tokens_*), Cursor usage and local-estimate quotas.
  • dropForkCopies in src/lib/opencode-storage.ts now counts each finished fingerprint (created/completed time, provider, model, tokens, cost) as many times as the session that has it most often, and drops the extra copies. Identical rows inside one session are kept; unfinished rows are never merged.
  • A single session's own read (/tokens_session on a fork) is unchanged and still shows its copied history.
  • Known limit: two unrelated sessions with exactly the same created/completed millisecond, model, tokens and cost are counted once. Fork links aren't used because OpenCode 1 forks migrated into OpenCode 2's database have none.

Linked Issue

Refs #306 (reported on 4.x; the 4.x backport is a separate PR into release/4.x).

OpenCode Validation

  • Current production released OpenCode version tested: v2.0.20
  • Why this version is relevant to the fix: its session.forked projector copies the parent's finished session_message rows into the fork; the local package build was run in an isolated OpenCode 2.0.20 and a session was forked.

Before-and-after evidence

Before: not captured live. By the issue's steps, the fork's copied messages are counted again (here: 6 messages, about 22.9K tokens).

After (/tokens_today, same window, before and after forking "Quick check-in" with Full session and sending nothing in the fork):

Before fork:  Messages 4 | Sessions 2 | Tokens 15.7K | Cost $0.00
After fork:   Messages 4 | Sessions 2 | Tokens 15.7K | Cost $0.00

Surface checks:

  • /tokens_today command dialog (TUI): checked live, totals unchanged after the fork.
  • TUI sidebar, toast, compact line below the message input: not applicable; they show provider quota, not local token history.
  • Web output: not separately tested (same reader).

Quality Checklist

  • I ran pnpm verify
  • This change is focused and avoids unrelated behavior changes
  • I updated or added tests when behavior changed
  • I updated docs when user-facing workflow, command, or config behavior changed (no docs describe how totals are counted)
  • For provider changes, I followed Provider Changes, or this does not apply

When a session is forked, OpenCode copies the parent's finished messages
into the fork session with new ids but identical times, model, tokens,
and cost. The all-history and multi-session readers counted each copy
again, so forking a $10 session showed $20 in token/cost totals.

Drop finished assistant rows whose fingerprint (created/completed time,
provider, model, tokens, cost) matches an earlier row. Rows are sorted by
time then id and copies get newer ids, so the original is kept.
Unfinished rows are never merged, and a fork's own single-session read
still shows its copied history.

Refs #306
Review found that dropForkCopies never checked sessions, so two separate
finished assistant rows in the same session with identical times, model,
usage, and cost (for example two concurrent zero-usage requests) were
merged. That undercounted message-based quotas and made aggregate reads
disagree with the single-session read.

A fork copy always lives in a different session from its original, so
remember the session of the first row per fingerprint and drop a later
row only when it is in a different session. Same-session duplicates keep
their count, and a parent's two identical rows still drop both fork copies.

Refs #306
Review found that keeping only the first session per fingerprint
undercounts partial forks: a parent with two identical finished messages,
an earlier fork that copied one, and a later fork that copied both
returned one row when the parent was not read (or was deleted).

Count each finished fingerprint as many times as the session that has it
most often. Same-session identical rows are all kept, a parent and fork
with the same copies still count once, and unfinished rows are never
merged.

Refs #306
@slkiser
slkiser merged commit ee17665 into main Oct 3, 2026
4 checks passed
@slkiser
slkiser deleted the t3code/f2828c3a branch October 3, 2026 13:00
slkiser added a commit that referenced this pull request Oct 3, 2026
… (#313)

Backport of #312 to the 4.x (OpenCode 1) line.

Fixes #306
@slkiser slkiser mentioned this pull request Oct 3, 2026
2 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant