Expand description
P5-5 (design §2 module 21 session.tree: “D5 in-place tree,
rewind-anywhere, branch summaries, entry labels”; §2.1 D-6 session.tree → core.session(tree-addressable transcript); §2.2 C7; §5.2 P5 row 5:
“loaders are already tree-aware (C7 resolution); adds in-place
rewind/branch/label on the native store”).
§What this is
Session/SessionStore
already carry a linear transcript — messages: Vec<ChatMessage> — and
the loaders already reconstruct MULTI-FILE tree structure on import
(Session::reconstruct_tree, the C7 resolution’s “import-side preserved
only”). What was missing is the native, IN-PLACE tree: the ability to
address any turn by id, rewind the active pointer to an earlier one
without deleting anything, explicitly fork a new branch, attach a short
summary to an off-path branch, and label any node — all on supercode’s
OWN session store (CC/PI’s defining in-place-tree feature, catalog D5).
§C7 — tree-with-linear-projection
Conflict 7 (design §2.2): session.tree’s in-place DAG is structurally
incompatible with a strictly-linear export target (the CX rollout shape).
The resolution the design commits to is core stays
tree-with-linear-projection: SessionTree::linear_projection always
derives the active branch’s message sequence deterministically by walking
parent pointers from the root to the active leaf — this is what
crate::session::Session::messages / the agent loop / exporters keep
consuming unchanged. A tree session with zero branches (the common case,
and the ONLY case before this module’s operations are ever invoked) is the
degenerate single-path tree: its projection is byte-for-byte the same
sequence crate::session::Session::messages already held — see
[tests::linear_projection_of_a_from_linear_tree_matches_the_source_messages].
Exporting a branched tree to a linear-only format is
SessionTree::splice_for_linear_export: it returns the active path
(spliced, exactly like today’s linear export) plus a
BranchSummary for every OFF-path branch. Nothing is deleted by this —
the full tree (every node of every branch) stays intact in
SessionTree / its <name>.tree.json sidecar
(crate::store::SessionStore::save_tree); the summary is an added,
human-readable POINTER (BranchSummary::branch names which branch the
full data still lives under), never a replacement for it (§1.13 lossless).
§Lossless rewind (§1.13)
SessionTree::rewind never deletes a node. Moving the active branch’s
leaf pointer backward leaves every node — including the ones the pointer
used to point through — exactly where it was in the DAG. Whenever the
rewind actually moves the pointer off the branch’s previous leaf, the OLD
leaf is preserved under a freshly-named sibling branch (so it stays
independently addressable/enumerable, not merely “still linked in but
orphaned from every named branch”) — see
[tests::rewind_preserves_the_rewound_past_as_a_recoverable_sibling_branch].
The next turn appended after a rewind becomes a NEW child of the rewind
target, i.e. a sibling of whatever used to follow it — exactly “rewind =
fork at the rewind point” (module 21’s row).
§Off by default / byte-identical
Nothing in this module is on any hot path. A SessionTree is only ever
constructed by an explicit caller (never implicitly by
crate::session::Session loading/saving, never by
crate::Agent’s loop) and its sidecar
(<name>.tree.json) is only ever written by an explicit
crate::store::SessionStore::save_tree call — so a session that never
invokes any tree operation has no .tree.json file at all, and every
existing linear read/write path (Session::to_native_jsonl/
from_native_str, SessionStore::save/load) is untouched
byte-for-byte. capabilities.session_tree.enabled (§3.1, module 21;
already wired into crate::modules::ModuleId::SessionTree since P3) is
surfaced on crate::Config as
crate::Config::session_tree_enabled/crate::Config::session_tree_branch_summaries/
crate::Config::session_tree_labels for a future caller (CLI/TUI) to
gate on — this module’s own API has no runtime dependency on that flag
(a library caller can always use SessionTree directly, exactly like
crate::store::SessionStore::fork doesn’t gate on any capability
either).
Structs§
- Branch
- A named pointer into the tree:
leafis the node this branch currently ends at (its “current-leaf pointer”, module 21’s phrase).Noneonly for a brand-new, still-empty tree’s implicit branch before any node exists. - Branch
Summary - A branch-carried summary (module 21 “branch summaries”, the C7
lossy→sidecar-backed path): a short human-readable digest of a branch,
paired with the pointer back to the full branch data (
branch, a key intoSessionTree::branches— the full nodes never move or get deleted, so this is always resolvable back to the source, §1.13). - Session
Tree - The native in-place conversation tree (module 21). See the module doc comment for the full design (C7 tree-with-linear-projection, lossless rewind, branch summaries).
- Tree
Node - One addressable turn in the tree (module 21’s “entry”). Carries the full
ChatMessage(this IS the full-fidelity source for a branched session — see the module doc’s “off by default” note: the plain linear transcript file remains the record for an UNBRANCHED session; this sidecar only exists once a tree operation actually ran), its parent/children links, and an optional human/agent-set label (module 21 “entry labels”).
Constants§
- MAIN_
BRANCH - The default/active branch name for a session that has never explicitly branched — the degenerate single-path tree’s one branch.
Traits§
- Branch
Summarizer - Injectable branch-summarization side-call (D-9), the module-21 analog of
reduce::summarize::SpanSummarizer— same shape, deliberately: a real implementation calls out to a cheap model; tests inject a deterministic fake. SeeSessionTree::summarize_branch_with’s doc comment for the “never blocks, never fails the caller” contract this trait’sErrfeeds into.
Type Aliases§
- NodeId
- A tree-node id. Assigned by
SessionTree::alloc_id— a monotonic counter ("n0","n1", …), not content-derived or random, so ids are deterministic and trivially testable, and so two nodes can never collide.