You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cli: stats totals differ from stored session usage #57878
I want to track total token usage, including work I later revert. The current stats calculation reports less usage than the stored session counters.
A manual, read-only analysis of my local database found:
All-time source
Tokens
Recorded cost
Current stats calculation
18,998,987,936
$11,778.98
Stored session counters
19,071,908,477
$11,819.08
Difference
72,920,541
$40.09
Tokens include input, output, reasoning, cache read, and cache write, following TokenUsage.total. This was calculated directly from SQLite, not captured from CLI output.
Code inspection suggests one way stats can lose historical usage:
packages/core/src/session/stats.ts sums normal model usage from remaining message rows.
Committing a revert deletes message rows from the boundary onward in packages/core/src/session/projector.ts, without reducing cumulative session usage counters.
Sending a new prompt after a staged revert commits it.
Expected: historical token/cost totals should include already-consumed usage, even after a revert, without double-counting forked history. Legacy/migrated usage must also remain counted.
The measured gap is real, but I have not established how much comes from reverts versus other differences in accounting. My database's event table is empty, so I could not validate an event-based total or attribute the gap to individual reverts.
Plugins
No plugin entries found in the global config. Full effective plugin status was not verified.
OpenCode version
2.1.2, latest channel. Version read from the local service registration.
Steps to reproduce
Suggested regression case from code inspection; not yet run end-to-end:
Complete two model steps and record token/cost totals.
Revert before the second step, then submit a new prompt to commit the revert.
Check historical stats: the reverted step's consumed tokens and cost should remain included.
Screenshot
Not applicable; database analysis and code inspection.
Stage a revert to the second prompt, then send a new prompt, which commits the revert and runs another model step.
opencode stats still shows 2 steps · 2.2k tokens, although three completions (3.3k tokens) were used. The session's stored totals still include the reverted step (input 4000, output 400, cost $0.008).
So the reverted step's usage drops out of stats once the revert is committed. That matches your reading: stats add up the remaining message rows, and committing the revert deletes them.
One more thing I noticed: the stored session totals also include the session-title generation call, which stats never count (related: #46371). That is probably part of your measured gap besides reverts.
The script below reproduces it.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduce anomalyco/opencode#57878: committed reverts drop consumed usage from `opencode stats`.# Usage: ./repro.sh (uses `opencode` from PATH)# OC_DEV=/path/to/repo ./repro.sh (runs CLI from a source checkout)set -euo pipefail
W=$(mktemp -d /tmp/opencode/repro57878.XXXX)export HOME=$W/home XDG_DATA_HOME=$W/home/data XDG_CONFIG_HOME=$W/home/cfg XDG_STATE_HOME=$W/home/state XDG_CACHE_HOME=$W/home/cache
mkdir -p $HOME$W/proj
export OPENCODE_SERVER_PASSWORD=repro57878
oc() {
if [ -n"${OC_DEV:-}" ];then
bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts "$@"else opencode "$@";fi
}
# Mock OpenAI-compatible provider: every completion reports 1000 input + 100 output tokens.
cat >$W/mock.ts <<'TS'Bun.serve({ port: 4011, async fetch(req) { const body: any = await req.json().catch(() => ({})) const usage = { prompt_tokens: 1000, completion_tokens: 100, total_tokens: 1100 } const base = { id: "c1", created: 1, model: "mock" } if (!body.stream) return Response.json({ ...base, object: "chat.completion", choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], usage }) const c = (o: any) => `data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", ...o })}\n\n` return new Response(c({ choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }] }) + c({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }] }) + c({ choices: [], usage }) + "data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } })} })TS
bun $W/mock.ts & MOCK=$!
cat >$W/proj/opencode.json <<'JSON'{ "model": "mock/mock", "small_model": "mock/mock", "provider": { "mock": { "npm": "@ai-sdk/openai-compatible", "name": "Mock", "options": { "baseURL": "http://127.0.0.1:4011/v1", "apiKey": "x" }, "models": { "mock": { "name": "Mock", "cost": { "input": 1, "output": 10 } } } } } }JSONcd$W/proj && git init -q
oc serve --port 4097 >$W/server.log 2>&1& SRV=$!trap"kill $MOCK; pkill -f \"serve --port 4097\"" EXIT
foriin$(seq 60);do curl -s -o /dev/null http://127.0.0.1:4097/ &&break; sleep 1;done
DB_DIR=$XDG_DATA_HOME/opencode
q() { DB=$(ls $DB_DIR/opencode*.db | head -1); bun -e "import {Database} from 'bun:sqlite';const db=new Database('$DB',{readonly:true});$1"; }
oc run --server http://127.0.0.1:4097 "first">/dev/null 2>&1
oc run --server http://127.0.0.1:4097 -c "second">/dev/null 2>&1echo"== stats after two model steps"; oc stats --server http://127.0.0.1:4097 2>&1| grep -E "prompts"
S=$(q "console.log(db.query('select id from session_v2').get().id)")
U2=$(q "console.log(db.query(\"select id from session_message where type='user' order by seq\").all()[1].id)")echo"== stage revert to second prompt ($U2), then commit it by sending a new prompt"
oc api --server http://127.0.0.1:4097 session.revert.stage --param sessionID=$S -d "{\"messageID\":\"$U2\"}">/dev/null
oc run --server http://127.0.0.1:4097 -s $S"third">/dev/null 2>&1echo"== model steps actually consumed: 3 (3.3k tokens, excluding title generation)"echo"== stored session counters:"; q "console.log(db.query('select tokens_input, tokens_output, cost from session_v2').get())"echo"== stats after revert commit:"; oc stats --server http://127.0.0.1:4097 2>&1| grep -E "prompts"
For the workaround, I'd keep a response-usage record separate from the editable conversation. A revert changes which messages remain in context, but shouldn't remove already-consumed model usage. The title-generation call in the reproduction is another reason to count completed provider requests rather than only visible assistant turns.
Disclosure: I build Torana, a local proxy. Its usage_logger plugin writes content-free JSONL with request ID, model, input/output and cache counts for completed responses routed through it. That gives a separate record to compare against stats after a revert, without reading prompts. It starts when enabled and is best-effort, with bounded file rotation; it isn't a billing ledger or a fix to OpenCode's counters.
I haven't run this particular revert reproduction through Torana, so I'm suggesting an independent measurement path, not claiming a verified fix.
5. For the workaround, I'd keep a response-usage record separate from the editable conversation. A revert changes which messages remain in context, but shouldn't remove already-consumed model usage. The title-generation call in the reproduction is another reason to count completed provider requests rather than only visible assistant turns.
Disclosure: I build Torana, a local proxy. Its usage_logger plugin writes content-free JSONL with request ID, model, input/output and cache counts for completed responses routed through it. That gives a separate record to compare against stats after a revert, without reading prompts. It starts when enabled and is best-effort, with bounded file rotation; it isn't a billing ledger or a fix to OpenCode's counters.
I haven't run this particular revert reproduction through Torana, so I'm suggesting an independent measurement path, not claiming a verified fix.
Why tho? You could just read from durable event stream. No need to duplicate data.
fair, for fixing stats itself the durable event stream is the right source. i suggested an outside record because you mentioned your event table was empty, so there was no event history to check the gap against. useful as a cross-check, not as the fix.
Activity
[-]fix(cli): reconcile stats with recorded token usage[/-][+]cli: stats totals differ from stored session usage[/+]opencode-agent commentedon Oct 21, 2026
Thanks for the detailed analysis. I reproduced this on 2.1.2 and it still happens on the latest
dev(863ea41).Setup: a local mock OpenAI-compatible provider that reports 1000 input + 100 output tokens for each completion.
opencode statsshows2 prompts · 2 steps · 2.2k tokens.opencode statsstill shows2 steps · 2.2k tokens, although three completions (3.3k tokens) were used. The session's stored totals still include the reverted step (input 4000, output 400, cost $0.008).So the reverted step's usage drops out of stats once the revert is committed. That matches your reading: stats add up the remaining message rows, and committing the revert deletes them.
One more thing I noticed: the stored session totals also include the session-title generation call, which stats never count (related: #46371). That is probably part of your measured gap besides reverts.
The script below reproduces it.
Reproduction script (repro.sh)
Run it:
bash repro.shprojectescape commentedon Oct 23, 2026
For the workaround, I'd keep a response-usage record separate from the editable conversation. A revert changes which messages remain in context, but shouldn't remove already-consumed model usage. The title-generation call in the reproduction is another reason to count completed provider requests rather than only visible assistant turns.
Disclosure: I build Torana, a local proxy. Its usage_logger plugin writes content-free JSONL with request ID, model, input/output and cache counts for completed responses routed through it. That gives a separate record to compare against stats after a revert, without reading prompts. It starts when enabled and is best-effort, with bounded file rotation; it isn't a billing ledger or a fix to OpenCode's counters.
I haven't run this particular revert reproduction through Torana, so I'm suggesting an independent measurement path, not claiming a verified fix.
maharshi365 commentedon Oct 23, 2026
Why tho? You could just read from durable event stream. No need to duplicate data.
projectescape commentedon Oct 23, 2026
fair, for fixing stats itself the durable event stream is the right source. i suggested an outside record because you mentioned your event table was empty, so there was no event history to check the gap against. useful as a cross-check, not as the fix.