Sessions as a Tree
On this page
A kli session is an append-only tree of entries, each one pointing at its parent.
What a session is
A session is an append-only collection of entries. Once an entry is written, it is never edited and never removed. Each entry carries the id of its parent, the entry that came before it on the same line of conversation. Follow those parent links from any entry back toward the start and you trace a single path; that path is a branch. The session also tracks one entry as its leaf: the current tip, the entry the next one will attach to.
So "the conversation right now" is not the whole table of entries. It is the branch from the leaf back to the root, read in order. Everything kli sends to the model on a turn is built by walking that branch. Entries that are not on it are still in the table; they just are not part of what the model sees.
Entries are typed, and the type says what the entry is. A message entry holds one message: your prompt, the assistant's reply, or a tool result. A model-change entry records that you switched providers or models mid-conversation. An option-change entry records a change to a semantic model option such as reasoning-effort. A compaction entry holds a summary that stands in for older history. A branch-summary entry records what happened on a path you left. Walking a branch and keeping only the entries that enter model context is how kli turns the tree back into a prompt.
Why a tree, and not a list
A flat list can only grow at the end. To go back, it would have to delete what came after, and then the alternative you abandoned is gone. A tree never deletes. Going back means pointing the leaf at an earlier entry; the entries past it stay in the table, off the current branch but intact. That single property is what makes the next three behaviors possible, and it is why each of them leaves the path you came from intact.
Rewind
When you rewind, kli steps the conversation back to before one of your earlier prompts. It does this by branching at that prompt's parent and moving the session onto the new branch. The prompts you can rewind past are the user prompts on the current branch, newest first; the rewind menu lists them by what you typed. The original branch is untouched, so the path you rewound away from is still there to return to. (Rewinding your very first prompt has no parent to branch at, so it starts a fresh session instead.)
Branches
A branch shares a prefix and then diverges. When kli branches a session at an entry, it makes a new session whose entries are the chain from that entry back to the root, with the leaf set to the chosen entry. The two sessions now share every entry up to the fork and own their continuations separately. Rewind is built on exactly this: it is a branch at a chosen prompt's parent. The shared prefix is the same entries, not a copy of them, however long the shared history is.
Compaction summaries as nodes
A long conversation eventually carries more history than is useful to resend on every turn. Compaction handles this without throwing anything away. kli picks a cut point in the branch, summarizes everything older than it, and appends a compaction entry holding that summary together with the id of the first entry it kept. The cut snaps forward to a prompt or reply so the kept window never begins on a tool result with no call before it.
The summary is a node like any other, on the branch, with a parent. When kli builds the messages for the next turn and finds a compaction entry on the branch, it sends the summary in place of everything before the cut and then the kept entries after it. The older entries are not deleted; they are simply no longer on the path the model reads. A later rewind or branch can still reach them.
Compaction is also why kli keeps a token estimate per entry: it sums the recent entries to decide where the cut should fall, keeping roughly the last stretch of conversation and summarizing the rest.
One readable record per line
Every entry serializes to a single self-describing record. An entry's record names its type and lists its fields by name: its id, its parent's id, a timestamp, and the type-specific contents. The parent link is right there in the record, so the tree is reconstructed by reading the records and following the ids. Records are read back with evaluation disabled, so loading a session never runs code, and message content that contains newlines round-trips because a record is read as one form, not split on line breaks.
Persistence is opt-in
By default a session lives only in memory. Start kli, work, quit, and the tree is gone when the process ends. Nothing is written to disk unless you ask for it.
You opt in with a session directory. Point the sessionDir setting at a folder and kli swaps the in-memory store for one backed by files. Each session becomes a file: a versioned header, then one entry record per line, in the order the entries were appended. A new entry is appended to the file as it is written, so the file grows with the conversation rather than being rewritten each time. A crash mid-write can leave a torn record at the end of the file; on reload kli drops that trailing fragment and keeps everything before it, so an interrupted write costs you at most the last entry. With a session directory configured, past sessions show up under /resume, where the branch structure is rendered as a forest so you can see which conversations forked from which.
The in-memory store and the file store are the same tree behind the same operations. Persistence changes where the entries live, not what they are or how branches, rewind, and compaction work over them.
Related
- The Agent Loop — the turn that appends to the tree and reads the current branch
- Context and the System Prompt — how a branch becomes the messages the model sees
- Configuration — where
sessionDirand other settings live