Expand description
OpenCode: sessions in a SQLite database, not a JSONL log.
Format notes (verified on OpenCode 1.18.15, 2026-09-05, against the live
opencode.db on this machine):
- The store is
$XDG_DATA_HOME/opencode/opencode.db(or~/.local/share/opencode/opencode.db), a WAL SQLite database. agent-top opens it read-only and never writes, which honours the observe-only rule and does not block OpenCode’s own writes. sessionis one row per conversation and already carries the accounting:directory,agent(the agent type,build/explore/plan),model(a JSON blob{"id","providerID","variant"}),cost(US dollars, computed by OpenCode),tokens_input,tokens_output,tokens_reasoning,tokens_cache_read,tokens_cache_write,time_created,time_updated(epoch ms),parent_idandversion. A subagent is asessionrow whoseparent_idis the parent’s id.- Because OpenCode has already priced the session, its
costis used directly rather than re-priced from agent-top’s table: OpenCode runs third-party models (DeepSeek, and so on) that the table does not carry, and the harness’s own figure is the real one. So an OpenCode row’s cost is exact and never a floor, andunpriced_tokensis zero. messageis one row per message,dataJSON withrole(user/assistant) andtime{created, completed}in epoch ms. Ausermessage opens a turn; eachassistantmessage is one inference, fromcreatedtocompleted, and extends the turn it belongs to, which ends at the last reply before the next prompt. A reply with nocompletedis still in flight, so its inference and turn stay open. Assistant messages are also the turn count.partis one row per message part,dataJSON withtype. Atoolpart hastool(the name),callIDandstatewithstatus(completed/error/ …) andtime{start,end}in epoch ms, which is one tool span.step-start/step-finish,reasoning,textandpatchparts are not read.- MCP calls (verified on OpenCode 1.18.15, 2026-09-13, with a filesystem
server named
scratch_fsand a live session): an MCP call is an ordinarytoolpart whosetoolis the server name and the tool name joined by_, each with every character outside[a-zA-Z0-9_-]replaced by_(scratch_fs_list_directory). There is nomcpprefix and nothing else in the part marks it as MCP; a call the server rejects hasstatuserror. So the name alone cannot say where the server ends, and a built-in tool with an underscore would look the same. The server names come from OpenCode’s config instead, key names ofmcponly: the globalconfig.json,opencode.jsonandopencode.jsoncin$XDG_CONFIG_HOME/opencode(or~/.config/opencode);opencode.jsonc,opencode.jsonand.opencode/opencode.json[c]in the session directory and each parent up to the worktree root; and~/.opencode. A tool part is an MCP call when its name starts with a configured server’s sanitised name and_, the longest such name winning. A server configured only throughOPENCODE_CONFIGin the agent’s own environment cannot be seen from outside the process and is not counted. Every tool part, MCP or not, is still counted as a tool call and a span. - Context by source (verified on OpenCode 1.18.15, 2026-09-13): each
assistantmessage is one model response and carries its owntokens{input, output, reasoning, cache: {read, write}},cost(US dollars) andmodelID. Thetoolparts of a message are the calls that response made, and the next assistant message’s prompt carries their results. So the ledger is fed per session intime_createdorder: each response’s usage, then that message’s tool calls as pending results. A compaction is written as a user message with acompactionpart followed by an assistant message withsummary: trueandmode: "compaction"; the ledger resets after that reply, exactly rather than by the prompt-halved fallback. The per-messagecostis one total, so the prompt-side share the ledger charges is taken from agent-top’s price table for the model, scaled so the classes add up to OpenCode’s figure; a model the table does not price gets tokens and no cost, which the UI shows as-. Subagent ledgers are folded into the parent’s.
A session has no file of its own, so a tracker is addressed by a virtual
path <db>/<session id>: unique, stable, and with the session id as its
file stem, which is all the collector and the trace resolver need.
Structs§
- Config
Roots - Where OpenCode’s config lives, for reading MCP server names. The fields are the roots that differ per machine, so a test can point them elsewhere.
- Open
Code Adapter - The OpenCode adapter. See the module notes for the store it reads.
- Open
Code Transcript - One OpenCode session as a
SessionSummary, read from the database. - Session
- One top-level conversation, enough to attribute it to a process and list it.
Functions§
- data_
dir $XDG_DATA_HOME/opencode, or~/.local/share/opencode.- db_path
- The session database, when it exists.
- mcp_
server_ names - The MCP server names configured for a session in
directory: the keys of everymcpsection OpenCode would read, and nothing else from those files. Sorted and deduplicated. - mcp_
server_ of - The configured server an OpenCode tool name belongs to, if any. The tool
key is
<server>_<tool>with both halves sanitised, so a name matches a server when it starts with the sanitised server name and_and has a tool name after it. Of several matches (githubandgithub_enterprise) the longest is the server. - recent_
sessions - Top-level sessions (no parent) written since
since, newest activity first. - session_
id_ of - The session id in a virtual path.
- session_
path - The virtual path that stands for a session on disk: the database path with the session id appended. Never opened as a file; only its stem is read.