opencode session format
Store and files
opencode stores sessions in ~/.local/share/opencode/opencode.db. XDG_DATA_HOME moves it to $XDG_DATA_HOME/opencode/opencode.db on every OS, and OPENCODE_DB names the file, absolute or relative to that directory. When XDG_DATA_HOME is set but no store exists there, deja reads the default one. DEJA_OPENCODE_DB overrides all of this. The store is SQLite and deja reads it through the sqlite3 command-line tool.
Beside the database, opencode writes one file per session recording what that
session changed: storage/session_diff/ses_<id>.json, a list of
{file, status, additions, deletions, patch} with patch in unified-diff form.
On one machine that is 1,002 files, and of 400 of those sessions only 17 held an
edit record from the database — so for the rest it is the only account of what
the session touched. deja reads it and folds the records into the session the
database gives, by id; DEJA_OPENCODE_DIFFS overrides where it looks.
Schema
opencode 2.0 moved the tables. Sessions live in session_v2, and a session's turns move out of message and part into one session_message table: one row per turn, the role in its type column, an assistant turn's parts inside its data blob:
session_v2(id, project_id, workspace_id, parent_id, slug, directory, path, title, version, ...)
session_message(id, session_id, type, seq, time_created, time_updated, data)
A fresh 2.0 store has no session, message or part table at all. Measured on opencode 2.0.12 (@opencode/cli), run in a throwaway HOME: the tables are session_v2, session_message, session_inbox, session_pending and the rest of the 2.0 set.
A store upgraded from 1.x keeps both sets. The 2.0 migration creates session_v2 beside session and drops none of session, message or part. opencode's v1 migration (packages/core/src/database/v1-migration.bun.ts) then copies each old session into session_v2 under the same id and rewrites its turns into session_message, one session per transaction; a session it has not reached yet has its turns only in message and part. One such store, from opencode 2.0.18, held 5 sessions in session, 10 in session_v2, 483 rows in message and 58 in session_message (#4151).
A user row keeps its text at the top level; an assistant row holds its parts under $.content:
{"time":{"created":1790102971000},"text":"edit main.go so add returns a+b+1"}
{"time":{"created":1790102977894},"agent":"build","content":[{"type":"reasoning","text":"…"},{"type":"tool","id":"call_565d…","name":"shell","state":{"status":"completed","input":{"command":"go vet ./..."},"content":[{"type":"text","text":"pattern ./...: directory prefix . does not contain main module"}],"metadata":{"exit":1}},"time":{"created":1790102980000}},{"type":"text","text":"add now returns a+b+1"}]}
What changed inside a turn, measured on the same store:
- a tool names itself under
$.name;bashis nowshellandapply_patchis nowpatch - what a tool printed is the block list
$.state.content, where 1.x wrote the string$.state.output - the file a
read,editorwritenames is$.state.input.path, where 1.x wrotefilePath; it can be relative to the session directory editis the editing tool, and it carries both sides:oldStringandnewString;writecarries the whole file undercontent- times are epoch milliseconds
The message types on a 2.0 store are user, assistant, synthetic, system, idle, shell, skill, compaction, model-switched, agent-switched and location-switched. deja reads the first two and a compaction's summary; the rest are opencode talking to itself.
deja reads both layouts and picks by asking where the turns are: session_message when it holds rows, message and part otherwise. The sessions come from session_v2, or from session on a store that has no session_v2. A store that has session_v2 beside session, message and part is read both ways at once: a session with turns in session_message is read from there, since that copy is the one opencode keeps writing to, and every other session in session is read from message and part. Session and message counts, titles, parents and the newest session come from both tables, each session counted once.
The per-session diff store (storage/session_diff/) is a 1.x store; a 2.0 home does not write it.
opencode 1.x
The parser joins three tables:
session(id, directory, time_created, time_updated)
message(id, session_id, time_created, data)
part(id, message_id, data)
message.data and part.data are JSON. A real-shaped pair is:
{"role":"assistant","time":{"created":"2026-07-17T09:00:01Z"}}
{"type":"text","text":"The query now uses the index.","time":{"start":"2026-07-17T09:00:01Z"}}
Parts with type: "text" are messages; the role comes from message.data.role. Parts marked synthetic or ignored are opencode's own text and are dropped, and a message with summary set is a compaction digest, indexed under the summary role. Five tool parts are read as well: read gives a file record from state.input.filePath, bash a command record from state.input.command with a non-zero state.metadata.exit and its state.output as tool output, apply_patch edit records from state.input.patchText, edit the replaced oldString and the written newString, and write the written content. An edit or write in the error state changed nothing and is not recorded. Message time prefers part.data.time.start, then message.data.time.created, then the message.time_created column; session times come from the session row. Strings in RFC 3339 form and numeric Unix seconds or milliseconds are accepted. session.directory supplies the project, session.title the title, and session.parent_id marks a subagent run.
Wiring
- MCP:
deja install opencodeadds amcp.dejaentry of typelocaltoopencode.json(oropencode.jsoncwhen that is the one present) in$XDG_CONFIG_HOME/opencode, else~/.config/opencode. - Skill:
~/.config/opencode/skills/deja-history/SKILL.md. - Command:
~/.config/opencode/commands/deja.md, invoked as/deja. - Auto-recall:
deja install opencode-autoalso writes a plugin,~/.config/opencode/plugins/deja.js. It puts the session digest into the first system block (experimental.chat.system.transform), appends per-prompt recall to the user message it was asked for, in every model call after it too, since opencode rebuilds each call from its store (experimental.chat.messages.transform), adds recall to a spawnedtaskagent's prompt (tool.execute.before), appends a file's history or a failed command's earlier fix to the tool output (tool.execute.after), runsdeja hook-precompactatexperimental.session.compacting, and on 1.x runsdeja hook-session-endatsession.idleand when the plugin is disposed, so a finished session is back in the next one's MCP recall. The plugin shape follows the installed opencode's major version (opencode --version, orDEJA_OPENCODE_MAJORwhere the binary is not onPATH), and the store's layout when neither answers: 2.0 loads only a default{ id, setup }, 1.x a named export.DEJA_OPENCODE_MAJORpicks the plugin and nothing else; the store is read by its tables. - Package: the
opencode-dejanpm package shipsindex.jsfor 1.x andserver.js, exported asopencode-deja/server, for 2.x. 2.x resolves that subpath first and wants a default{ id, setup }; 1.x from 1.3.4 resolves it too and calls itsserver; older 1.x loadsindex.js. Inopencode.jsonit goes underpluginon 1.x andpluginson 2.x. - Resume:
opencode -s <id>, run in the session's directory, or from the current one when that directory is gone.
Known quirks and drift
- The database can be several gigabytes. deja projects JSON scalars in SQL instead of streaming complete blobs.
- Message content is split across
messageandpart; one message can have several parts. - Tool parts other than the ones above are ignored, and so is the output of
read. A message is capped at 1 MiB. - A missing database must not be passed to SQLite because the CLI would create it.
- The committed conformance fixture is SQL rather than a binary database; the test creates a temporary SQLite file.
Last verified: 2026-09-30
deja reads this format and thirty-seven others, and turns what it finds into memory your agents can search. See the harness matrix for what is wired where, or install it and search your own history.
Found this useful? Star deja-vu on GitHub.