会话树计划
| English | 中文 |
参考:session.md
本文档描述当前会话树导航的实现方式:内存中的 tree model、leaf movement rules、branching behavior,以及 extension/event integration。
子系统定位
会话以 append-only entry log 持久化,但运行时行为是 tree-based:
- 每个非 header entry 都有
id和parentId。 - 活跃位置是
SessionManager中的leafId。 - 追加 entry 时总是创建当前 leaf 的子节点。
- Branching 不会改写历史;它只在下一次追加前改变 leaf 指向。
关键文件:
src/session/session-manager.ts— tree data model、traversal、leaf movement、branch/session extractionsrc/session/session-context.ts—buildSessionContextcontext reconstruction(resolved root→leaf LLM context、compaction/branch-summary replay)src/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—/tree与/branch的 selector orchestrationsrc/slash-commands/builtin-registry.ts— command routing(/tree、/branch)src/modes/controllers/input-controller.ts— double-escape behavior 与app.session.tree/app.session.forkkeybinding wiringsrc/session/messages.ts— 把branch_summary、compaction与custom_messageentry 转换为 LLM context messages
SessionManager 中的 tree data model
Runtime indices 存放在 SessionEntryIndex helper 中,作为 SessionManager 上的 #index 持有,并与 journal array #entries 保持同步:
#entriesById: Map<string, SessionEntry>— 任意 entry 的快速查找#children: Map<string | null, SessionEntry[]>— parent→children adjacency#labels: Map<string, string>— 按 target entry id 解析的 label#leaf: string | null— 当前在 tree 中的位置#usage— 运行中的 usage totals
Tree APIs:
getBranch(fromId?)沿 parent links 走到 root,返回 root→node pathgetTree()返回SessionTreeNode[](entry、children、label)- parent links 变成 children arrays
- missing parent 的 entry 被视为 roots
- children 按 timestamp 从 old→new 排序
getChildren(parentId)返回直接 childrengetLabel(id)从#labelsmap 解析当前 label
getTree() 是 runtime projection;持久化仍是 append-only JSONL entries。
Leaf movement semantics
共有三种 leaf movement primitives:
branch(entryId)- 校验 entry 存在
- 设置
leafId = entryId - 不写入新 entry
resetLeaf()- 设置
leafId = null - 下一次 append 会创建新的 root entry(
parentId = null)
- 设置
branchWithSummary(branchFromId, summary, details?, fromExtension?)- 接受
branchFromId: string | null - 设置
leafId = branchFromId - 追加一个
branch_summaryentry 作为该 leaf 的子节点 - 当
branchFromId为null时,fromId被持久化为"root"
- 接受
/tree navigation behavior(同一 session file)
AgentSession.navigateTree() 是 navigation,不是 file forking。
流程:
- 校验 target 并计算 abandoned path(
collectEntriesForBranchSummary) - 携带
TreePreparationemitsession_before_tree - 可选总结 abandoned entries(hook-provided summary 或 built-in summarizer)
- 计算新 leaf target:
- 选中 user message:leaf 移到其 parent,message text 返回用于 editor prefill
- 选中 custom_message:与 user message 同规则(leaf = parent,text prefills editor)
- 选中其他 entry:leaf = 选中 entry id
- 应用 leaf move:
- 带 summary:
branchWithSummary(newLeafId, ...) - 不带 summary 且
newLeafId === null:resetLeaf() - 其他情况:
branch(newLeafId)
- 带 summary:
- 从新 leaf 重建 agent context 并 emit
session_tree
重要:summary entries 附加在新导航位置,不在 abandoned branch tail。
/branch behavior(新 session file)
/branch 与 /tree 有意不同:
/tree在当前 session file 内导航。/branch创建新的 session branch file(非持久化模式则为内存替换)。
用户可见 /branch 流程(SelectorController.showUserMessageSelector → AgentSession.branch):
- Branch source 必须是 user message。
- 选中用户文本被提取用于 editor prefill。
- 若选中 user message 是 root(
parentId === null):通过newSession({ parentSession: previousSessionFile })开启新 session。 - 否则:
createBranchedSession(selectedEntry.parentId)把历史 fork 到选中的 prompt boundary。
SessionManager.createBranchedSession(leafId) 细节:
- 通过
getBranch(leafId)构建 root→leaf path;缺失时报错。 - 从复制路径中排除现有
labelentries。 - 对路径中仍保留的 entry 从解析后的 label map(
labelsInEffect())重建 fresh label entries。 - 持久化模式:写入新 JSONL file 并切换 manager 到该文件;返回新文件路径。
- 内存模式:替换内存 entries;返回
undefined。
Context reconstruction 与 summary/custom 集成
buildSessionContext()(在 session-context.ts 中,通过 SessionManager.buildSessionContext() 暴露)解析活跃 root→leaf path 并构建有效 LLM context state:
- 跟踪 path 上最新的 thinking/model/service-tier/mode/TTSR/MCP-selection state。
- 处理 path 上最新 compaction:
- 先 emit compaction summary
- 从
firstKeptEntryId到 compaction point replay kept messages - 再 replay post-compaction messages
- 把
branch_summary与custom_messageentries 作为AgentMessageobjects 纳入。
session/messages.ts 随后把这些消息类型映射为 model input:
branchSummary与compactionSummary变成 user-role templated context messagescustom/hookMessage变成 developer-role content messages(通过 agent-core 的convertMessageToLlm)
因此 tree movement 通过改变活跃 leaf path 来改变 context,而不是通过修改旧 entries。
Labels 与 tree UI behavior
Label persistence:
appendLabelChange(targetId, label?)在当前 leaf chain 上写入labelentries。SessionEntryIndex中的#labels立即更新(set 或 delete)。getTree()把当前 label 解析到每个返回的 node 上。
Tree selector behavior(tree-selector.ts):
- 把 tree 扁平化以便导航,保持 active-path 高亮,并优先展示 active branch。
- 支持 filter modes:
default、no-tools、user-only、labeled-only、all。default会抑制label、custom、model_change和thinking_level_change;它不是完整的“hide all internal entries” filter。
- 支持对 rendered semantic content 做 free-text search。
Shift+L打开 inline label editing 并通过appendLabelChange写入。
Command routing:
/tree总是打开 tree selector。/branch打开 user-message selector,除非doubleEscapeAction=tree,此时也使用 tree selector UX。
Extension 和 hook touchpoints
Command-time extension API(ExtensionCommandContext):
branch(entryId)— 创建 branched session filenavigateTree(targetId, { summarize? })— 在当前 tree/file 内移动
Tree navigation 相关 events:
session_before_tree- 接收
TreePreparation:targetIdoldLeafIdcommonAncestorIdentriesToSummarizeuserWantsSummary
- 可取消 navigation
- 可提供 summary payload 替代 built-in summarizer
- 接收 abort
signal(Escape cancellation path)
- 接收
session_tree- emit
newLeafId、oldLeafId - 创建了 summary 时包含
summaryEntry fromExtension表示 summary origin
- emit
相邻但相关的 lifecycle hooks:
/branchflow 使用session_before_branch/session_branch- compaction entries 使用
session_before_compact、session.compacting、session_compact,后续影响 tree-context reconstruction
真实约束与边界条件
branch()不能 targetnull;root-before-first-entry state 使用resetLeaf()。branchWithSummary()支持nulltarget 并记录fromId: "root"。- 在 tree selector 里选中当前 leaf 是 no-op。
- Summarization 需要 active model;若无,summarize navigation 会快速失败。
- 若 summarization 被中止,navigation 被取消且 leaf 不变。
- 内存会话的
createBranchedSession不会返回 branch file path。 - Tree context reconstruction 包含 service-tier 与 MCP tool-selection state,但这些 entries 不会变成 LLM messages。
Plan approval session naming
当用户从 plan mode(InteractiveMode.#approvePlan)批准 plan 时,approval handler 会用 plan 的 title 作为 session name 的 seed,使生成的(fresh 或 compacted)session 不会保持 unnamed。
Trigger:
- Plan approval 到达
#approvePlan(...)且options.title已从 plan-approval details 填充。 - 这对所有 approval choice 都生效(
Approve and execute、Approve and compact context、Approve and keep context);合成plan-approvedprompt 原本会 bypass input-controller 的 title-generation path。
Naming source:
- 归一化后的 plan title 经
humanizePlanTitle(title)(packages/coding-agent/src/plan-mode/approved-plan.ts)人性化:- 把连续的
-/_替换为单个空格 - trim whitespace
- 首字符大写
- 对 whitespace-only / separator-only 输入返回
""
- 把连续的
- 人性化名称只在当前 session 没有名称时(
!sessionManager.getSessionName())应用;随后调用sessionManager.setSessionName(name, "auto"),后者同样拒绝覆盖用户命名的 session。 - 成功应用后,terminal title(
setSessionTerminalTitle)与 editor border color 会刷新以反映新名称。
humanizePlanTitle 示例:
migrate-mcp-loader→Migrate mcp loaderfix_session_naming→Fix session namingfoo--bar__baz→Foo bar bazRefactorRouter→RefactorRouter(无可展开分隔符)""/"---"→""(不应用名称)
Legacy compatibility still present
Session migrations 在 load 时仍会运行:
- v1→v2 增加
id/parentId并把 compaction index anchor 转为 id anchor - v2→v3 把 legacy
hookMessagerole 迁移为custom
迁移后的当前 runtime behavior 是 version-3 tree semantics。