Skip to content

Latest commit

 

History

History
77 lines (62 loc) · 3.21 KB

File metadata and controls

77 lines (62 loc) · 3.21 KB

opencode storage schema notes

Observed 2026-06-11 against a real opencode.db on this machine plus the sst/opencode source and public parsers (ccusage, tokscale, opencode-wrapped, opencode-tokenscope). Defensive parsing only.

Three storage generations

  1. SQLite (>= ~1.2, current): ~/.local/share/opencode/opencode.db (respects XDG_DATA_HOME; same path shape on Windows). WAL mode, so .db-shm/.db-wal siblings exist and the CLI may hold the file open: open strictly read-only (mode=ro + PRAGMA query_only).
  2. JSON tree (pre-1.2): ~/.local/share/opencode/storage/ with session/<projectID>/ses_*.json (info), message/<sessionID>/msg_*.json, part/<sessionID>/<messageID>/prt_*.json.
  3. Oldest: storage/session/info/ses_*.json plus storage/session/message/<sid>/*.json with parts inline and the assistant payload under metadata.assistant ("message v1").

The migration into SQLite does not always delete the old tree; if the db exists we read only the db, so nothing is counted twice.

SQLite tables that matter

  • session(id, project_id, parent_id, directory, title, time_created, ..., cost, tokens_input, tokens_output, ...)directory is the cwd; parent_id set means a subagent (child) session. Aggregate token columns exist but are absent/zero for migrated legacy data, so we fold messages instead.
  • message(id, session_id, time_created, data)data is the message JSON (see below). All times are unix milliseconds.
  • part(id, message_id, session_id, time_created, data).

Message JSON

User: {"role":"user","time":{"created":ms},...} — the prompt text lives in text parts, not on the message.

Assistant (verbatim shape from a real db):

{"role":"assistant","mode":"build","agent":"build",
 "path":{"cwd":"...","root":"..."},
 "cost":0,
 "tokens":{"total":4117,"input":4096,"output":21,"reasoning":0,
           "cache":{"write":0,"read":0}},
 "modelID":"qwen2.5-coder:7b","providerID":"ollama",
 "time":{"created":1780398431744,"completed":1780398480810},
 "finish":"stop"}
  • cost is opencode's own USD figure; often 0 for subscription or local providers — fall back to the price table then.
  • tokens.reasoning is separate from output (we add it to output).
  • Aborted turns lack time.completed and/or carry error: {"name":"MessageAbortedError"}; we count those as interrupts.
  • In SQLite rows, id/sessionID may be missing from the JSON (they live in the table columns).

Part JSON

data.type: text, reasoning, tool, step-start, step-finish, patch. Tool part:

{"type":"tool","tool":"read","callID":"call_...","state":{
  "status":"completed","input":{"filePath":"..."},
  "output":"...","time":{"start":ms,"end":ms}}}

state.status is pending|running|completed|error; errored tools have state.error text instead of output. Tool names are lowercase (read, edit, bash, grep, glob, task, ...) and inputs use filePath; the adapter maps both onto the Claude Code vocabulary so the metrics layer stays harness-agnostic. Tool results sit on the assistant's own parts, so the adapter emits a synthetic follow-up event to match the Claude Code "results arrive as a user message" shape.