Skip to content

fix(reporter): read opencode sessions from opencode.db - #37

Open
nicko170 wants to merge 1 commit into
jackmcpickle:mainfrom
nicko170:fix/opencode-sqlite-storage
Open

fix(reporter): read opencode sessions from opencode.db#37
nicko170 wants to merge 1 commit into
jackmcpickle:mainfrom
nicko170:fix/opencode-sqlite-storage

Conversation

@nicko170

Copy link
Copy Markdown

Problem

tokenmaxer backfill opencode returns zero rows on any machine running a current opencode, and reports it as success:

$ tokenmaxer backfill opencode
tokenmaxer: backfilled 0 opencode row(s)
tokenmaxer: backfill complete — 0 row(s) total

opencode >= 1.x stores messages in a SQLite database at <data-dir>/opencode.db. The reporter only knows the older layout — one JSON file per message under <data-dir>/storage/message/<sessionID>/. On opencode 1.18.15 that directory does not exist at all:

$ ls ~/.local/share/opencode
auth.json  log  opencode.db  opencode.db-shm  opencode.db-wal  repos  snapshot  tool-output

So readdirSync(root) throws, collectOpencodeRows hits its catch { continue }, and the source silently contributes nothing. opencode-sessionstart / opencode-sessionend are no-ops for the same reason, so ongoing reporting is affected too, not just backfill.

Fix

Read opencode.db when it is present, and keep the legacy tree as a fallback so machines that have not upgraded — or that still hold pre-upgrade sessions alongside the database — keep reporting. Sessions found in the database win over a same-id legacy directory.

The message.data column holds the same message object the JSON files did (role, modelID, tokens.{input,output,reasoning,cache.{read,write}}, time.created), so parseOpencodeMessages is reused unchanged and both layouts share one parser.

Notes:

  • node:sqlite is already used for Cursor's state.vscdb, so no new dependency.
  • The database is opened readOnly, and an unreadable file or unexpected schema returns null to fall through to the legacy path rather than throwing.
  • sinceMs is applied as a WHERE time_created >= ? predicate, matching the mtime filter the file path uses.

Verification

pnpm check passes — lint, format, typecheck, 374 tests across 42 files.

New src/__tests__/reporter-opencode-db.test.ts covers per-session/per-model summing, started_at being the earliest message, sinceMs filtering, the legacy layout still working, and the empty case. The tests seed a real SQLite database and pin OPENCODE_DATA_DIR/XDG_DATA_HOME/HOME at a temp dir so they do not read the developer's own opencode store.

End-to-end against a real 25-session opencode.db, the reporter's totals match the aggregates opencode maintains independently in its own session table:

reporter opencode session table
sessions 25 25
input 255,090,316 255,090,316
output 962,656 962,656

Possibly worth a follow-up

A source whose data directory is entirely absent is indistinguishable from one with no recent activity — both print backfilled 0 row(s) and exit 0. That is what made this quiet for so long. Happy to add a warning when no candidate root exists for a requested source, if you'd want it.

opencode >= 1.x stores messages in a SQLite database at
<data-dir>/opencode.db; the reporter only knew the older layout of one
JSON file per message under <data-dir>/storage/message/<sessionID>/.

On any machine running a current opencode that directory no longer
exists, so readdirSync throws, collectOpencodeRows skips the root, and
`tokenmaxer backfill opencode` reports "backfilled 0 opencode row(s)"
as a success. The opencode-sessionstart hook is silently a no-op too.

Read the database when it is present, falling back to the legacy tree so
machines that have not upgraded (or that still hold pre-upgrade sessions)
keep reporting. The `data` column holds the same message object the JSON
files did, so parseOpencodeMessages is reused unchanged; sessions found
in the database take precedence over a same-id legacy directory.

node:sqlite is already used for Cursor's state.vscdb, so this adds no
dependency. The database is opened read-only, and an unreadable or
unexpected schema falls through to the legacy path rather than throwing.

Verified against a real 25-session opencode.db: input/output totals match
the aggregates opencode keeps in its own `session` table exactly.
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.

1 participant