Skip to content

feat: added OpenCode v2 support - #11

Open
ilyhalight wants to merge 2 commits into
Cateds:masterfrom
ilyhalight:master
Open

ilyhalight wants to merge 2 commits into
Cateds:masterfrom
ilyhalight:master

Conversation

@ilyhalight

Copy link
Copy Markdown

Added OpenCode v2 support (support for v1 also saved).

I don't make:

  • tests for v2 (if need i can add it with ai)
  • test with clean OpenCode v2, i test it only with OpenCode v2 upgraded from v1

before:
WindowsTerminal_WdLur3TFeD

after:
WindowsTerminal_j505XK4MRZ

@LegolasUndomiel LegolasUndomiel left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling v2 support. The overall direction is right: reading session_v2 / session_message, taking the role from the type column, and the model.id alias are exactly what is needed. A few suggestions after testing this against a real v2 database (OpenCode 2.0.32, with a DB upgraded from v1).

1. Merge v1 + v2 tables instead of switching exclusively to v2

A database upgraded from v1 keeps both schemas. The migration copies messages into session_message keeping the same ids (on my DB: 2808 ids shared between message and session_message, of which 2563 are assistant messages), but it is not a complete superset: I found 48 assistant messages and 2 sessions that exist only in the legacy session / message tables. Since this PR uses session_v2 / session_message and never touches the legacy tables whenever the v2 tables exist, that data is silently dropped.

Suggestion: read both and deduplicate by message id — e.g. seed a HashSet from the v1 message ids first, then skip ids already seen while streaming session_message; merge the session lists the same way (v2 rows win on shared ids, since they also carry directory). This also keeps a single code path when only v1 exists.

2. Keep the table-existence probe read-only

database_has_tables now calls open_database (read-write), whereas the previous database_has_expected_tables used Connection::open_with_flags(..., SQLITE_OPEN_READ_ONLY). Opening a live database read-write just to list tables can take write locks; consider preserving the read-only flag.

3. Tests

Good that you flagged this yourself. A synthetic-v2 unit test (create project / session / message / session_v2 / session_message, insert a v2-only session plus an assistant payload with the nested model.id, assert exactly one usage event and that idle records are skipped) and a "same id present in both tables is counted once" test would protect the merge logic. I have both passing locally.

Minor / optional:

  • session_v2.directory is a better per-session project path than project.worktree (COALESCE(s.directory, p.worktree)).
  • type IN ('user', 'assistant') drops compaction / synthetic, which the legacy schema stored with role = "user"; include them if you want message counts to stay consistent with v1.

I have the above implemented locally (merge + dedup + 3 tests, full suite green). Happy to share the diff or open a follow-up PR if that makes it easier.

@LegolasUndomiel

Copy link
Copy Markdown

Follow-up: an implementation that merges the v1 and v2 tables

Following the suggestion in the review above, I implemented the "merge v1 + v2 and de-duplicate by message id" approach as a standalone branch you can merge or cherry-pick:

  • Branch: https://github.com/LegolasUndomiel/opencode-stats/tree/feat/opencode-v2-support
  • Based on the current master (so it keeps the read-only open, OC_STATS_DB_PATH, and the other recent cleanups)
  • Scope: 3 files (src/db/connection.rs, src/db/models.rs, src/db/queries.rs), +403/-56

How it differs from #11

#11 this branch
Table selection only v2 if session_v2 exists, otherwise only v1 reads both tables and merges, de-duplicating message ids
type filter user, assistant user, assistant, compaction, synthetic
Table probe switched to a read-write open stays read-only
Tests none 2 new unit tests (synthetic v2 database, shared-id de-duplication)

Why an either/or choice loses data (measured on a real opencode 2.0.32 database)

  • message and session_message share message ids: 2808 ids overlap, 2563 of them assistant messages.
  • 48 assistant messages and 2 sessions exist only in the v1 tables - reading v2 only drops them.
  • 408 assistant messages exist only in session_message - reading v1 only drops them.

Approach

  1. Read the v1 message table first (the existing parser already handles the overlapping ids).
  2. Append the rows from session_message whose id has not been seen yet.
  3. Merge sessions from session and session_v2 (v2 wins on shared ids, working directory from COALESCE(directory, worktree)).
  4. v2 specifics: the role comes from the session_message.type column (the v2 data payload has no role), the model id is also accepted as model.id, and system / idle / model-switched rows are skipped.

End-to-end on a real database: events=3132 messages=3496 sessions=105, with the v2-only sessions correctly included.

Happy to adjust anything on that branch.

@ilyhalight

Copy link
Copy Markdown
Author

Thanks for tackling v2 support. The overall direction is right: reading session_v2 / session_message, taking the role from the type column, and the model.id alias are exactly what is needed. A few suggestions after testing this against a real v2 database (OpenCode 2.0.32, with a DB upgraded from v1).

1. Merge v1 + v2 tables instead of switching exclusively to v2

A database upgraded from v1 keeps both schemas. The migration copies messages into session_message keeping the same ids (on my DB: 2808 ids shared between message and session_message, of which 2563 are assistant messages), but it is not a complete superset: I found 48 assistant messages and 2 sessions that exist only in the legacy session / message tables. Since this PR uses session_v2 / session_message and never touches the legacy tables whenever the v2 tables exist, that data is silently dropped.

Suggestion: read both and deduplicate by message id — e.g. seed a HashSet from the v1 message ids first, then skip ids already seen while streaming session_message; merge the session lists the same way (v2 rows win on shared ids, since they also carry directory). This also keeps a single code path when only v1 exists.

2. Keep the table-existence probe read-only

database_has_tables now calls open_database (read-write), whereas the previous database_has_expected_tables used Connection::open_with_flags(..., SQLITE_OPEN_READ_ONLY). Opening a live database read-write just to list tables can take write locks; consider preserving the read-only flag.

3. Tests

Good that you flagged this yourself. A synthetic-v2 unit test (create project / session / message / session_v2 / session_message, insert a v2-only session plus an assistant payload with the nested model.id, assert exactly one usage event and that idle records are skipped) and a "same id present in both tables is counted once" test would protect the merge logic. I have both passing locally.

Minor / optional:

* `session_v2.directory` is a better per-session project path than `project.worktree` (`COALESCE(s.directory, p.worktree)`).

* `type IN ('user', 'assistant')` drops `compaction` / `synthetic`, which the legacy schema stored with `role = "user"`; include them if you want message counts to stay consistent with v1.

I have the above implemented locally (merge + dedup + 3 tests, full suite green). Happy to share the diff or open a follow-up PR if that makes it easier.

  1. Opencode already merge v1 data to v2 on upgrade. if something was dropped, it means it was needed for some reason.
  2. Code already open READ ONLY connection

synthetic don't contain tokens

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.

2 participants