How Oda stores chats
DataMind Oda keeps every chat as a lasting record on your computer: a log file that grows as the chat works. This page describes that storage for the people who build, ship or debug Oda itself: the files, the events inside them, and how older logs are brought forward. The user-facing view of the same files is on Chat logs on disk, and the rules that keep older logs readable are on Chat log format and compatibility.
Where a chat's history lives
- Line-oriented JSON. A log holds one event per line, each line a JSON object. The file is optionally compressed, as checksummed Zstandard frames, and its name carries the format version once the format has one. With compression off, you can read the file as plain text.
- The sessions root. Every chat's log lives under one root folder, the sessions root. The root is set for the installation, and by default it sits inside Oda's own data folder in your home area. Oda creates it when the first log is written.
- A private write. Where the system supports it, a chat's folder and its log file are created so that only your user account can use them.
- One header, then events. The file opens with a header that names the chat and its format version; the events follow. The header's fields are on Chat log format.
The event envelope
Every event in a log carries the same small envelope:
| Part | What it carries |
|---|
type | What the event records |
seq | The event's position in the log |
time | When the event happened |
data | The event's payload |
ignorable (optional) | Marks the event as safe to skip when a reader does not know its type |
surfaceOp, sourceEventSeqs (conditional) | For a surface event: the history change it applies and the recorded events it is built from |
- An event is either a surface event, which produces the history a model reads when the chat continues, or a log-only event, which is kept for the record but never enters that history.
- Oda refuses to interpret a log that holds an event type it does not know, unless that event carries the
ignorable marker: skipping a record silently could rebuild the chat wrongly.
- For an event type that changes the history a model reads, the effect is produced by one explicit interpreter for that type; it is never applied by general code.
What a chat records as it works
A chat's record grows as the work happens, and nothing already written is rewritten.
- A prompt is admitted, and the chat reads as running.
- A turn starts; the system prompt is assembled for the request; the first step begins.
- The request goes out; the system message and your message are recorded; the answer is recorded as it arrives, and every tool call and result is recorded too.
- The step ends; further steps may follow; the turn ends, and the chat reads as idle again.
- Every successful model call is recorded, even one that returned no content or stopped at the output limit. An empty answer stays out of the history later derived for the model.
Where a stored message came from
- A stored message carries its source, and the reserved kinds name where it came from: you, the model, a tool, the system prompt, a checkpoint saved when a chat is shortened, a generated chat title, the tool registry, runtime context, a plugin, an automatic review, and a compaction output.
- A new source kind cannot reuse a reserved name, and a kind a reader does not know is preserved, not dropped.
- Every stored message carries both an identity and a source; a value built only for one request is refused where a stored message is required.
Reading the record from outside
- Two event surfaces serve programs: the session event stream carries the record for replay, while a live surface carries coordination while work runs, such as queue and status, steering a turn, and errors.
- A read-only set of chat-reading tools is available only where a deployment enables it. The set never exposes its internal paging cursors, and every answer is authorised from the chat that asked for it.
Bringing older logs forward
An older log is converted while the chat is used: opening it only reads it, and the current-format copy is written when the chat next records something new. That story is on Opening older chats. For a whole root at once, maintainers use a one-time migration command, pnpm run migrate:sessions-to-v4.
- The command refuses to run unless the build it ships in writes the current log format (
this one-time command requires a V4 Session writer).
- It finds each chat's generations under the root by the expected folder shape, without following symbolic links, and skips a file that does not sit in that shape.
- Each log is converted and its successor published beside the untouched original, or reported as already current; a log already in the current format is opened read-only.
- A log that changed while the run was working is deferred and retried once after the first pass.
- A failure on one chat does not stop the rest. The run finishes with a summary of what it converted, found current, failed and skipped, and exits with a failure code when anything failed.
- The run uses no model and no API key, and its text log and final summary are written privately in a temporary folder.
- By default it converts as many logs at once as the machine has processors, never more than 16;
--jobs N sets the number and --sessions-dir points the run at another root. A --jobs value that is not a positive whole number is refused with --jobs must be a positive safe integer.
Questions, answered
What does an event in a log look like?
Every event carries type, seq, time and data, plus an optional ignorable marker and, for a surface event, the conditional surfaceOp and sourceEventSeqs fields. The table above says what each part carries.
Why do some recorded events never reach the model's history?
By design: an event is either a surface event, which produces the history a model reads when the chat continues, or a log-only event, which is kept for the record but never enters that history.
What can a stored message's source be?
One of a reserved set: you, the model, a tool, the system prompt, a checkpoint saved when a chat is shortened, a generated chat title, the tool registry, runtime context, a plugin, an automatic review, or a compaction output. A new kind cannot reuse a reserved name, and an unknown kind is preserved rather than dropped.
Why does a log record a model call that returned nothing?
So the record stays complete: every successful call is recorded, even one with no content or one that stopped at the output limit. An empty answer is kept out of the history derived for the model, so it never misleads a later turn.
What does the log record while a turn runs?
The whole path of the turn: the prompt admission, the running state, the turn start, the assembled system prompt, each step start, the request with its system and user messages, the answer and its tool calls and results, then the step end, the turn end and the idle state.
What do other programs read to replay a chat?
The session event stream, which carries the record for replay. Coordination while work runs, such as queue and status, steering and errors, travels on a separate live surface.
How are the chat-reading tools authorised?
They are a read-only set, available only where a deployment enables it; each answer is authorised from the chat that asked for it, and the set never exposes its internal paging cursors.
Why does the one-time upgrade refuse to run?
Because the build it ships in does not write the current log format. Run it from a build whose writer is current (this one-time command requires a V4 Session writer); nothing is converted otherwise.
What happens if a log changes while the upgrade runs?
That log is deferred and retried once after the first pass, so the run never converts it mid-change.
Does the upgrade call a model?
No. The run uses no model and no API key, and its text log and final summary are written privately in a temporary folder.