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 JSONdatacolumn. Assistantdatacarriestokens{input,output,reasoning,cache{read,write}},cost,finish,model{id,providerID}(note:id, notmodelID), andtime{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
tokensand notime.completeduntil the turn finishes;time_updatedbumps during streaming, so row presence is a reliable “working” signal, but token counts land only at completion. time.streamedis the final flush, not streaming start - it is within tens of ms ofcompletedon fast providers. Effective generation rate istokens.output / (completed - created), not anything involvingstreamed.- Context size of a turn =
input + output + reasoning + cache.read + cache.writeof 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 processexperimental.session.stats- aggregate usage over a range: sessions, prompts, steps, tokens (incl. cache), real dollar cost, tool reliability, per-day activitysession.context(--param sessionID=...) - context messages after the last compactionevent.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).