Summary
opencode stats computes Total Cost as SUM(session.cost). Forking a session copies the parent's prior messages into the new session (new message IDs, but the originaltime.created / cost / tokens). Those turns were never re-sent to the provider, so they were never re-billed — yet each fork's session.cost rollup includes them. The result: shared history is counted once per fork, inflating the reported total.
Version
opencode 1.17.20
Impact
opencode statsTotal Cost is over-reported roughly in proportion to how much a user forks. In one dataset the over-count was ~11%. Deduplicating on the billed event brings the total down accordingly.
The figures below are illustrative/synthetic (not real usage), included only to show the relationship between the values. The ~11% over-count is the real observed magnitude.
sessions : 1200
opencode stats (SUM session.cost): $4,000.00
SUM(message.cost) raw : $4,060.00
deduplicated billed events : $3,556.00
OVER-COUNT vs reality : $444.00 (11.1%)
opencode stats matches SUM(session.cost)exactly, confirming that is the aggregate used.
Root cause
Cost is a property of a billed event, but the aggregate keys on the row/session, which forking duplicates. Note session.parent_id is often NULL on the copies, so lineage can't be relied on to detect them, and "different message IDs" does not imply independent spend — copies always get new IDs.
Concrete pattern (illustrative numbers): groups of sessions are byte-identical copies of one another (same message count, same per-message timestamps and cost) — i.e. forks that never diverged. Each copy's session.cost is added to the total again:
Identical-copy session clusters (same billed events, counted N times):
cluster #1: x3 @ $30.00 -> $60.00 phantom
cluster #2: x2 @ $12.00 -> $12.00 phantom
cluster #3: x3 @ $8.00 -> $16.00 phantom
cluster #4: x2 @ $2.50 -> $2.50 phantom
(These are only the forks that never diverged; continued forks also duplicate their inherited prefix, which typically accounts for the bulk of the over-count.)
Repro
cp ~/.local/share/opencode/opencode.db /tmp/repro.db # DB is live; snapshot it
python3 fork_cost_double_count_repro.py /tmp/repro.db
fork_cost_double_count_repro.py
#!/usr/bin/env python3"""Repro: `opencode stats` (== SUM(session.cost)) double-counts forked sessions."""importsqlite3, json, sys, collections, hashlibdb=sqlite3.connect(sys.argv[1] iflen(sys.argv) >1else"/tmp/repro.db")
db.row_factory=sqlite3.Rowstats_total=db.execute("SELECT COALESCE(SUM(cost),0) FROM session").fetchone()[0]
n_sessions=db.execute("SELECT COUNT(*) FROM session").fetchone()[0]
# De-duplicate on the *billed event* (immutable properties of one generation).raw=dedup=0.0seen=set()
forrindb.execute("SELECT data FROM message"):
d=json.loads(r["data"]); c=d.get("cost") or0.0ifc<=0: continuet=d.get("time") or {}; tk=d.get("tokens") or {}; ca=tk.get("cache") or {}
fp= (t.get("created"), d.get("providerID"), d.get("modelID"),
tk.get("input"), tk.get("output"), tk.get("reasoning"),
ca.get("read"), ca.get("write"), round(c, 10))
raw+=ciffpnotinseen:
seen.add(fp); dedup+=cprint(f"sessions : {n_sessions}")
print(f"opencode stats (SUM session.cost): ${stats_total:,.2f}")
print(f"SUM(message.cost) raw : ${raw:,.2f}")
print(f"deduplicated billed events : ${dedup:,.2f}")
print(f"OVER-COUNT vs reality : ${stats_total-dedup:,.2f} "f"({100*(stats_total-dedup)/stats_total:.1f}%)")
# Concrete example: byte-identical copy clusters (forks that never diverged).ev=collections.defaultdict(list)
forrindb.execute("SELECT session_id, data FROM message"):
d=json.loads(r["data"]); c=d.get("cost") or0.0ifc<=0: continueev[r["session_id"]].append(((d.get("time") or {}).get("created"), round(c, 10)))
cost_of= {r["id"]: (r["cost"] or0.0) forrindb.execute("SELECT id,cost FROM session")}
clusters=collections.defaultdict(list)
forsid, einev.items():
e.sort(); clusters[hashlib.md5(json.dumps(e).encode()).hexdigest()].append(sid)
print("\nIdentical-copy session clusters (same billed events, counted N times):")
rows= [((len(ss)-1)*cost_of[ss[0]], len(ss), cost_of[ss[0]])
forssinclusters.values() iflen(ss) >1]
fori, (extra, n, cost) inenumerate(sorted(rows, reverse=True)[:8], 1):
print(f" cluster #{i}: x{n} @ ${cost:.2f} -> ${extra:.2f} phantom")Suggested fix
Deduplicate on a billed-event fingerprint before summing — e.g. (time.created, providerID, modelID, tokens.input, tokens.output, tokens.reasoning, tokens.cache.read, tokens.cache.write, cost) — or give inherited (copied) messages a zero rollup cost in forks so each event is counted only in its originating session.
Summary
opencode statscomputes Total Cost asSUM(session.cost). Forking a session copies the parent's prior messages into the new session (new message IDs, but the originaltime.created/cost/tokens). Those turns were never re-sent to the provider, so they were never re-billed — yet each fork'ssession.costrollup includes them. The result: shared history is counted once per fork, inflating the reported total.Version
opencode 1.17.20Impact
opencode statsTotal Cost is over-reported roughly in proportion to how much a user forks. In one dataset the over-count was ~11%. Deduplicating on the billed event brings the total down accordingly.opencode statsmatchesSUM(session.cost)exactly, confirming that is the aggregate used.Root cause
Cost is a property of a billed event, but the aggregate keys on the row/session, which forking duplicates. Note
session.parent_idis oftenNULLon the copies, so lineage can't be relied on to detect them, and "different message IDs" does not imply independent spend — copies always get new IDs.Concrete pattern (illustrative numbers): groups of sessions are byte-identical copies of one another (same message count, same per-message timestamps and cost) — i.e. forks that never diverged. Each copy's
session.costis added to the total again:(These are only the forks that never diverged; continued forks also duplicate their inherited prefix, which typically accounts for the bulk of the over-count.)
Repro
fork_cost_double_count_repro.pySuggested fix
Deduplicate on a billed-event fingerprint before summing — e.g.
(time.created, providerID, modelID, tokens.input, tokens.output, tokens.reasoning, tokens.cache.read, tokens.cache.write, cost)— or give inherited (copied) messages a zero rollup cost in forks so each event is counted only in its originating session.