Skip to content

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

Open

Description

@janorga

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.

Activity

  1. janorga commented on Sep 10, 2026

    @janorga
    Author

    Related: #36944 documents the over-report side of the same fork cost-accounting mechanism — forked sessions double-count inherited history while the parent session is alive.

    This issue is the under-report counterpart: the same anti-double-counting logic means a fork does not count its parent's history, so when the parent session is deleted, those inherited messages are the only record of that spend and it silently disappears from stats. Verified against the provider console: stats reported $1.90 for a day the provider billed ~$5 (the $2.90 gap was exactly a fork's inherited history).

    Note for the fix of #36944: a naive dedup of pre-fork messages would introduce this under-report case. The rule that matched provider billing in our data excludes inherited history only while the parent session still exists.

  2. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed write-up, @janorga!

    You're asking for opencode2 stats to keep counting spend from a fork's copied history after its parent session is deleted, or more generally to keep spend totals for deleted sessions.

    The code works as you describe. When a session is forked, the parent's messages are copied into the fork with their original timestamps and costs. Stats then skip any fork message created before the fork, so the same spend isn't counted twice, and nothing checks whether the parent still exists. Stats are also built only from messages that are still stored, so deleting any session, forked or not, already removes its spend from the totals. That makes this a change to how stats are meant to work rather than a bug, so I've marked it as a feature request. Your two options (a rollup kept for deleted sessions, or counting copied history once the parent is gone) give maintainers a clear choice to consider.

    Maintainers will review it; I can't say whether or when it will change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions