anomalyco / anomalyco/opencode

opencode2 stats under-reports spend when a forked session's parent is deleted

Open
#48,306 1 comment 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Sep 10, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Issue Report: OpenCode under-reports spend when a forked session's parent is deleted

Summary

opencode2 stats (and the aggregated cost / token columns in the session_v2 table) excludes the cost of messages that a forked session inherited from its parent even when the parent session no longer exists in the database. If the parent is deleted, the inherited messages are the only remaining record of that spend, so real, provider-billed usage silently disappears from all reports.

Environment

  • OpenCode v2 (opencode2 v0.0.0-beta-19425), data in ~/.local/share/opencode/opencode.db
  • Sessions forked from other sessions (session_v2.fork_session_id)
  • Verified on Linux, September 2026

How fork accounting works today

When a session is forked, OpenCode copies the full parent message history into session_message under the fork's session_id, with new message IDs and the original cost values embedded in each message's data JSON. The aggregated session_v2.cost column of the fork only reflects messages created after the fork's time_created.

The under-reporting bug triggers when the parent session row is deleted while the fork survives:

  • The inherited messages in the fork are the only record of the cost paid for the parent conversation.
  • Nothing counts them: the parent is gone, and the fork's aggregate excludes them by design (they predate the fork's creation).

Reproduction (observed data)

Database state on the day of the fork:

  • A session was forked at 13:44 from a parent session.
  • The parent session row no longer exists (neither in session_v2 nor in the legacy session table) and has no messages.
  • The fork contains 380 assistant messages; the 130 messages created before 13:44 (inherited history) carry $2.8987 of real cost.
  • Content-hashing all 552 assistant messages of that day shows zero duplicated content: every inherited message is unique in the database.

Resulting numbers for that day:

Source Value
Sum of cost across all assistant messages of the day $4.8018
Sum excluding fork-inherited history (current opencode2 stats behavior) $1.9031
Provider-side web console (internal reporting tool) ~$5

The provider-side report (~$5) matches the full per-message sum: the $2.90 of inherited messages was real, billed usage.

Root cause

Observed behavior: a fork's aggregate (session_v2.cost) only reflects post-fork messages, and deleting the parent session removes the only other record of the inherited spend. The internal mechanism behind the aggregate (an exclusion query at read time vs. incremental aggregation at write time) has not been inspected; either way, the result is the same — inherited spend is not counted anywhere once the parent is gone.

Possible fixes

The right fix depends on where the aggregation actually happens internally (read-time query vs. write-time incremental update), which is up to the maintainers to determine. Candidate approaches by layer:

  1. If session_v2.cost is an incremental aggregate (most likely): when a session is deleted, preserve its aggregated cost/token totals in a rollup table (or reattribute inherited spend to surviving forks at fork time).

  2. If stats are computed by querying session_message: include a fork's inherited history when the parent no longer exists. Reference SQL validated against a real database:

    AND NOT (
        s.fork_session_id IS NOT NULL
        AND EXISTS (SELECT 1 FROM session_v2 p WHERE p.id = s.fork_session_id)
        AND m.time_created < s.time_created
    )
    

    With s being the fork's session_v2 row joined on
    s.id = m.session_id. IS NOT / IS NOT NULL are used deliberately for
    null-safe comparisons.

Note: the SQL above is a reference implementation validated only in a local reporting tool (where the calculation method is fully known); it is not a patch for the core, and patching the core at a different layer may be simpler.

Related observation: deleted sessions lose their messages

In the observed database the deleted parent also lost its session_message rows (foreign-key cascade), which is what makes the inherited copy in the fork the only record. If deleting a session is expected to keep historical cost reporting accurate, OpenCode could consider:

  1. Keeping aggregated cost/token totals of deleted sessions in a separate rollup table, or
  2. Making session_v2.cost of a fork include its inherited history (with the parent-exists caveat above), or
  3. Documenting that deleting a session discards its cost accounting and that forks become the only record of inherited spend.

Impact

  • Any user who forks sessions and later deletes parents under-reports real spend in opencode2 stats and in anything reading session_v2.cost`.
  • In a sampled 10-day window, the gap was $20.34 (reported) vs $23.37 (real): ~13% of the period's spend was invisible.

Verification of the reference rule

Against the same real database (same 10-day window), applying the reference SQL from the previous section:

  • One day with two forks of a live parent: $8.03 (the dedup correctly removed $11.99 of inherited history; content-hash confirms exact duplicates)
  • One day with an orphan fork: $4.80 (inherited history counted as real spend; matches provider console)
  • Total: $23.37, consistent with provider-side reporting.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.