Read Local Session Data

How to read OpenCode session, token, and cost data for local tooling: the SQLite store for cheap polling, and the opencode api CLI for live server surfaces. Verified against v2.0.22. Reference consumer: plugins/opencode-cost-tokens/ in peteretelej/herdr-plugins.

The local SQLite store

OpenCode v2 keeps its journal at ~/.local/share/opencode/opencode.db (honor $XDG_DATA_HOME), WAL mode, with -wal/-shm siblings. Opening it SQLITE_OPEN_READ_ONLY from other processes is safe while OpenCode runs; a 500 ms busy timeout absorbs write bursts. The database can grow large (hundreds of MB), so never scan it - use the indexed exact-id lookups.

Key tables:

  • session_v2 - one row per session with aggregate columns already computed: cost (real dollars; ~0 for subscription-billed providers, real for API-billed ones), tokens_input/output/reasoning/cache_read/cache_write, model (a JSON object: {"id", "providerID", "variant"}), agent, directory, title, time_created/time_updated/time_idle. One exact-id lookup returns full session totals.
  • session_message - session_id, type (assistant/user/system/synthetic/ idle/compaction/model-switched/…), seq, and a JSON data column. Assistant data carries tokens{input,output,reasoning,cache{read,write}}, cost, finish, model{id,providerID} (note: id, not modelID), and time{created,streamed,completed} (ms epoch).
  • session_pending - transient in-flight rows; drained immediately in practice, not a polling surface.
  • event - journal table, pruned empty; not a live surface.

Semantics worth knowing:

  • In-progress turns: an assistant row appears with no tokens and no time.completed until the turn finishes; time_updated bumps during streaming, so row presence is a reliable “working” signal, but token counts land only at completion.
  • time.streamed is the final flush, not streaming start - it is within tens of ms of completed on fast providers. Effective generation rate is tokens.output / (completed - created), not anything involving streamed.
  • Context size of a turn = input + output + reasoning + cache.read + cache.write of the newest completed assistant message. Compare against the model limit in ~/.cache/opencode/models.json ([providerID].models[modelID].limit.context).

The opencode api CLI

opencode api <operationId> calls the running background service and handles authentication internally; raw HTTP to the service port 401s without the token. Read-only operations useful for tooling (operation IDs):

  • session.active - foreground live sessions of this OpenCode process
  • experimental.session.stats - aggregate usage over a range: sessions, prompts, steps, tokens (incl. cache), real dollar cost, tool reliability, per-day activity
  • session.context (--param sessionID=...) - context messages after the last compaction
  • event.subscribe (GET /api/event) - SSE stream of all events. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. Fine for dashboards that can keep up; polling SQLite is the robust path.

Each CLI call spawns a process (~100+ ms), so poll SQLite at second cadence and reserve opencode api for the surfaces SQLite lacks (e.g. the aggregate stats endpoint).