- Home
- Documentation
- sessions
- Session Tree Architecture
Session Tree Architecture
Session tree architecture (current)
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “/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)
Section titled “/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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.