Session tree architecture (current)
Reference: session.md
This document describes how session tree navigation works today: in-memory tree model, leaf movement rules, branching behavior, and extension/event integration.
What this subsystem is
The session is stored as an append-only entry log, but runtime behavior is tree-based:
- Every non-header entry has
idandparentId. - The active position is
leafIdinSessionManager. - Appending an entry always creates a child of the current leaf.
- Branching does not rewrite history; it only changes where the leaf points before the next append.
Key files:
src/session/session-manager.ts— tree data model, traversal, leaf movement, branch/session extractionsrc/session/agent-session.ts—/treenavigation flow, summarization, hook/event emissionsrc/modes/components/tree-selector.ts— interactive tree UI behavior and filteringsrc/modes/controllers/selector-controller.ts— selector orchestration for/treeand/branchsrc/modes/controllers/input-controller.ts— command routing (/tree,/branch, double-escape behavior)src/session/messages.ts— conversion ofbranch_summary,compaction, andcustom_messageentries into LLM context messages
Tree data model in SessionManager
Runtime indices:
#byId: Map<string, SessionEntry>— fast lookup for any entry#leafId: string | null— current position in the tree#labelsById: Map<string, string>— resolved labels by target entry id
Tree APIs:
getBranch(fromId?)walks parent links to root and returns root→node pathgetTree()returnsSessionTreeNode[](entry,children,label)- parent links become children arrays
- entries with missing parents are treated as roots
- children are sorted oldest→newest by timestamp
getChildren(parentId)returns direct childrengetLabel(id)resolves current label fromlabelsById
getTree() is a runtime projection; persistence remains append-only JSONL entries.
Leaf movement semantics
There are three leaf movement primitives:
branch(entryId)- Validates entry exists
- Sets
leafId = entryId - No new entry is written
resetLeaf()- Sets
leafId = null - Next append creates a new root entry (
parentId = null)
- Sets
branchWithSummary(branchFromId, summary, details?, fromExtension?)- Accepts
branchFromId: string | null - Sets
leafId = branchFromId - Appends a
branch_summaryentry as child of that leaf - When
branchFromIdisnull,fromIdis persisted as"root"
- Accepts
/tree navigation behavior (same session file)
AgentSession.navigateTree() is navigation, not file forking.
Flow:
- Validate target and compute abandoned path (
collectEntriesForBranchSummary) - Emit
session_before_treewithTreePreparation - Optionally summarize abandoned entries (hook-provided summary or built-in summarizer)
- Compute new leaf target:
- selecting a user message: leaf moves to its parent, and message text is returned for editor prefill
- selecting a custom_message: same rule as user message (leaf = parent, text prefills editor)
- selecting any other entry: leaf = selected entry id
- Apply leaf move:
- with summary:
branchWithSummary(newLeafId, ...) - without summary and
newLeafId === null:resetLeaf() - otherwise:
branch(newLeafId)
- with summary:
- Rebuild agent context from new leaf and emit
session_tree
Important: summary entries are attached at the new navigation position, not on the abandoned branch tail.
/branch behavior (new session file)
/branch and /tree are intentionally different:
/treenavigates within the current session file./branchcreates a new session branch file (or in-memory replacement for non-persistent mode).
User-facing /branch flow (SelectorController.showUserMessageSelector → AgentSession.branch):
- Branch source must be a user message.
- Selected user text is extracted for editor prefill.
- If selected user message is root (
parentId === null): start a new session vianewSession({ parentSession: previousSessionFile }). - Otherwise:
createBranchedSession(selectedEntry.parentId)to fork history up to the selected prompt boundary.
SessionManager.createBranchedSession(leafId) specifics:
- Builds root→leaf path via
getBranch(leafId); throws if missing. - Excludes existing
labelentries from copied path. - Rebuilds fresh label entries from resolved
labelsByIdfor entries that remain in path. - Persistent mode: writes new JSONL file and switches manager to it; returns new file path.
- In-memory mode: replaces in-memory entries; returns
undefined.
Context reconstruction and summary/custom integration
buildSessionContext() (in session-manager.ts) resolves the active root→leaf path and builds effective LLM context state:
- Tracks latest thinking/model/mode/ttsr state on path.
- Handles latest compaction on path:
- emits compaction summary first
- replays kept messages from
firstKeptEntryIdto compaction point - then replays post-compaction messages
- Includes
branch_summaryandcustom_messageentries asAgentMessageobjects.
session/messages.ts then maps these message types for model input:
branchSummaryandcompactionSummarybecome user-role templated context messagescustom/hookMessagebecome user-role content messages
So tree movement changes context by changing the active leaf path, not by mutating old entries.
Labels and tree UI behavior
Label persistence:
appendLabelChange(targetId, label?)writeslabelentries on the current leaf chain.labelsByIdis updated immediately (set or delete).getTree()resolves current label onto each returned node.
Tree selector behavior (tree-selector.ts):
- Flattens tree for navigation, keeps active-path highlighting, and prioritizes displaying the active branch first.
- Supports filter modes:
default,no-tools,user-only,labeled-only,all. - Supports free-text search over rendered semantic content.
Shift+Lopens inline label editing and writes viaappendLabelChange.
Command routing:
/treealways opens tree selector./branchopens user-message selector unlessdoubleEscapeAction=tree, in which case it also uses tree selector UX.
Extension and hook touchpoints for tree operations
Command-time extension API (ExtensionCommandContext):
branch(entryId)— create branched session filenavigateTree(targetId, { summarize? })— move within current tree/file
Events around tree navigation:
session_before_tree- receives
TreePreparation:targetIdoldLeafIdcommonAncestorIdentriesToSummarizeuserWantsSummary
- may cancel navigation
- may provide summary payload used instead of built-in summarizer
- receives abort
signal(Escape cancellation path)
- receives
session_tree- emits
newLeafId,oldLeafId - includes
summaryEntrywhen a summary was created fromExtensionindicates summary origin
- emits
Adjacent but related lifecycle hooks:
session_before_branch/session_branchfor/branchflowsession_before_compact,session.compacting,session_compactfor compaction entries that later affect tree-context reconstruction
Real constraints and edge conditions
branch()cannot targetnull; useresetLeaf()for root-before-first-entry state.branchWithSummary()supportsnulltarget and recordsfromId: "root".- Selecting current leaf in tree selector is a no-op.
- Summarization requires an active model; if absent, summarize navigation fails fast.
- If summarization is aborted, navigation is cancelled and leaf is unchanged.
- In-memory sessions never return a branch file path from
createBranchedSession.
Legacy compatibility still present
Session migrations still run on load:
- v1→v2 adds
id/parentIdand converts compaction index anchor to id anchor - v2→v3 migrates legacy
hookMessagerole tocustom
Current runtime behavior is version-3 tree semantics after migration.