TTSR Injection Lifecycle
This document covers the current Time Traveling Stream Rules (TTSR) runtime path from rule discovery to stream interruption, retry injection, extension notifications, and session-state handling.
Implementation files
../src/sdk.ts../src/export/ttsr.ts../src/session/agent-session.ts../src/session/session-manager.ts../src/prompts/system/ttsr-interrupt.md../src/capability/index.ts../src/extensibility/extensions/types.ts../src/extensibility/hooks/types.ts../src/extensibility/custom-tools/types.ts../src/modes/controllers/event-controller.ts
1. Discovery feed and rule registration
At session creation, createAgentSession() loads discovered rules, constructs a TtsrManager, and buckets rules through bucketRules(...):
const ttsrSettings = settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
const rulesResult = await loadCapability<Rule>(ruleCapability.id, { cwd });
const { rulebookRules, alwaysApplyRules } = bucketRules(
rulesResult.items,
ttsrManager,
{
builtinRules: ttsrSettings.builtinRules,
disabledRules: ttsrSettings.disabledRules,
},
);
bucketRules(...) drops names listed in ttsr.disabledRules, drops embedded builtin-defaults rules when ttsr.builtinRules === false, registers accepted TTSR rules, and then routes the remaining rules to always-apply/rulebook buckets.
Pre-registration dedupe behavior
loadCapability("rules") deduplicates by rule.name with first-wins semantics (higher provider priority first). Shadowed duplicates are removed before TTSR registration.
TtsrManager.addRule() behavior
Registration is skipped when:
- TTSR is disabled (
ttsr.enabled === false) - both
rule.condition(regex) andrule.astCondition(ast-grep patterns) are absent, or every regex condition fails to compile and there are no AST conditions - a rule with the same
rule.namewas already registered in this manager - the rule scope excludes all monitored streams
Invalid regex conditions and unreachable scopes are logged as warnings and ignored; session startup continues. If a TTSR rule defines globs, those globs are compiled as a global file-path gate for matching.
AST conditions (astCondition)
A rule may carry astCondition: a list of ast-grep patterns (OR’d, same as regex condition), matched structurally instead of textually. A repeated metavariable inside one pattern requires both occurrences to be equal (if ($X) clearTimeout($X) matches but if ($X) clearTimeout($Y) does not).
AST conditions only evaluate on edit/write tool-argument streams — they need a language, which is inferred from the file extension on the tool’s path argument, and they match against the tool’s matcherDigest: the source-bearing payload the call introduces, not the raw wire delta. For edit that digest is new_text in replace mode, + body rows or added diff lines in the other update modes, and the full content for a patch create; for write it is the entire content. It is not the whole prospective file: pre-existing target content is invisible unless the edit explicitly repeats it in its source-bearing payload. Matching is performed in memory by the native astMatch engine (no temp files) with Smart strictness. Streams without a usable file path (prose, thinking, path-less tool calls) skip AST conditions entirely. A rule may mix condition and astCondition; the regex paths keep working on every scope while AST paths apply only to those tool streams.
Setting gating
TtsrSettings.enabled gates the manager: when ttsr.enabled === false, addRule() refuses registration and checkDelta()/checkSnapshot()/checkAstSnapshot()/hasRules()/hasAstRules() all return empty/false, so no matching runs.
2. Streaming monitor lifecycle
TTSR detection runs inside AgentSession.#handleAgentEvent.
Turn start
On turn_start, the stream buffer is reset:
ttsrManager.resetBuffer()
During stream (message_update)
When assistant updates arrive and rules exist:
- monitor
text_delta,thinking_delta, andtoolcall_delta - for tools exposing
matcherDigest(edit/write), replace the scoped buffer with that tool’s source-bearing payload —new_textin replace mode,+body rows or added diff lines in the other edit update modes, full content for a patch create, and the entirecontentforwrite— and callcheckSnapshot(snapshot, matchContext); otherwise append the delta into a source/tool scoped manager buffer and callcheckDelta(delta, matchContext)(synchronous regex matching either way) - for edit/write tool streams, when
hasAstRules()is true,await checkAstSnapshot(snapshot, matchContext)(asynchronous AST matching)
checkDelta()/checkSnapshot() iterate registered rules and return all matching rules that pass scope, global path-glob, regex condition, and repeat policy checks. checkAstSnapshot() applies the same scope/path/repeat gates, then runs each candidate rule’s astCondition patterns against the snapshot via the native astMatch engine. It is throttled per stream key: an identical consecutive snapshot (common when only non-source arguments change between deltas) is skipped without re-running the matcher. Both paths feed their matches through the same trigger-decision handler.
3. Trigger decision and immediate abort path
When one or more rules match and at least one matched rule allows interruption:
- Matched rules are deduplicated into
#pendingTtsrInjections. #ttsrAbortPending = trueand a TTSR resume gate is created.agent.abort()is called immediately.ttsr_triggeredevent is emitted asynchronously (fire-and-forget).- retry work is scheduled via the post-prompt task scheduler with a 50ms delay.
Abort is not blocked on extension callbacks.
4. Retry scheduling, context mode, and reminder injection
After the 50ms timeout:
#ttsrAbortPending = false- read
ttsrManager.getSettings().contextMode - if
contextMode === "discard", drop the targeted partial assistant output withagent.replaceMessages(...slice(0, targetAssistantIndex)) - build injection content from pending rules using
ttsr-interrupt.mdtemplate - append and persist a hidden
custom_message/runtime custom message withcustomType: "ttsr-injection"anddetails.rules - mark those rule names injected, persist a
ttsr_injectionentry, and callagent.continue()to retry generation
Template payload is:
<system-interrupt reason="rule_violation" rule="" path="">
...
<h1 id="tree-命令参考">/tree 命令参考</h1>
<table>
<tbody>
<tr>
<td><a href="/MusePi/docs/tree.html">English</a></td>
<td>中文</td>
</tr>
</tbody>
</table>
<p><code class="language-plaintext highlighter-rouge">/tree</code> 打开交互式<strong>会话树</strong>导航器。它让你跳转到当前会话文件的任意条目,并从该点继续。</p>
<p>这是文件内的叶移动,不是新会话导出。</p>
<h2 id="tree-的作用"><code class="language-plaintext highlighter-rouge">/tree</code> 的作用</h2>
<ul>
<li>从当前会话条目构建树(<code class="language-plaintext highlighter-rouge">SessionManager.getTree()</code>)</li>
<li>打开 <code class="language-plaintext highlighter-rouge">TreeSelectorComponent</code>,支持键盘导航、筛选和搜索</li>
<li>选中后,调用 <code class="language-plaintext highlighter-rouge">AgentSession.navigateTree(targetId, { summarize, customInstructions })</code></li>
<li>从新的叶路径重建可见聊天</li>
<li>选择 user/custom 消息时可预填充编辑器文本</li>
</ul>
<p>主要实现:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">src/slash-commands/builtin-registry.ts</code>(<code class="language-plaintext highlighter-rouge">/tree</code>、<code class="language-plaintext highlighter-rouge">/branch</code> 命令路由)</li>
<li><code class="language-plaintext highlighter-rouge">src/modes/controllers/input-controller.ts</code>(按键绑定、双击 Esc 行为)</li>
<li><code class="language-plaintext highlighter-rouge">src/modes/controllers/selector-controller.ts</code>(树 UI 启动 + 摘要提示流程)</li>
<li><code class="language-plaintext highlighter-rouge">src/modes/components/tree-selector.ts</code>(导航、筛选、搜索、标签、渲染)</li>
<li><code class="language-plaintext highlighter-rouge">src/session/agent-session.ts</code>(<code class="language-plaintext highlighter-rouge">navigateTree</code> 叶切换 + 可选摘要)</li>
<li><code class="language-plaintext highlighter-rouge">src/session/session-manager.ts</code>(<code class="language-plaintext highlighter-rouge">getTree</code>、<code class="language-plaintext highlighter-rouge">branch</code>、<code class="language-plaintext highlighter-rouge">branchWithSummary</code>、<code class="language-plaintext highlighter-rouge">resetLeaf</code>、标签持久化)</li>
</ul>
<h2 id="如何打开">如何打开</h2>
<p>以下任一方式打开同一个选择器:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">/tree</code></li>
<li>为 <code class="language-plaintext highlighter-rouge">app.session.tree</code> action 配置的按键绑定</li>
<li>空编辑器双击 Esc,且 <code class="language-plaintext highlighter-rouge">doubleEscapeAction = "tree"</code>(默认)</li>
<li><code class="language-plaintext highlighter-rouge">/branch</code> 且 <code class="language-plaintext highlighter-rouge">doubleEscapeAction = "tree"</code> 时(路由到树选择器而非仅用户的 branch picker)</li>
</ul>
<h2 id="树-ui-模型">树 UI 模型</h2>
<p>树由会话条目的父指针(<code class="language-plaintext highlighter-rouge">id</code> / <code class="language-plaintext highlighter-rouge">parentId</code>)渲染。</p>
<ul>
<li>子项按时间戳升序排列(旧的在前,新的在下)</li>
<li>活跃分支(从根到当前叶的路径)用圆点标记</li>
<li>标签(如有)在节点文本前渲染为 <code class="language-plaintext highlighter-rouge">[label]</code></li>
<li>若存在多个根(孤立/断链),它们会显示在一个虚拟分支根下</li>
</ul>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Example tree view (active path marked with •):
├─ user: "Start task"
│ └─ assistant: "Plan"
│ ├─ • user: "Try approach A"
│ │ └─ • assistant: "A result"
│ │ └─ • [milestone] user: "Continue A"
│ └─ user: "Try approach B"
│ └─ assistant: "B result"
</code></pre></div></div>
<p>选择器以当前选中项为中心,最多显示:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">max(5, floor(terminalHeight / 2))</code> 行</li>
</ul>
<h2 id="树选择器中的按键绑定">树选择器中的按键绑定</h2>
<ul>
<li><code class="language-plaintext highlighter-rouge">Up</code> / <code class="language-plaintext highlighter-rouge">Down</code>:移动选中项(循环)</li>
<li><code class="language-plaintext highlighter-rouge">Left</code> / <code class="language-plaintext highlighter-rouge">Right</code>:上翻页 / 下翻页</li>
<li><code class="language-plaintext highlighter-rouge">Enter</code>:选中节点</li>
<li><code class="language-plaintext highlighter-rouge">Esc</code>:有搜索时清空搜索;否则关闭选择器</li>
<li><code class="language-plaintext highlighter-rouge">Ctrl+C</code>:关闭选择器</li>
<li><code class="language-plaintext highlighter-rouge">Type</code>:追加到搜索查询</li>
<li><code class="language-plaintext highlighter-rouge">Backspace</code>:删除搜索字符</li>
<li><code class="language-plaintext highlighter-rouge">Shift+L</code>:编辑/清除选中条目的标签</li>
<li><code class="language-plaintext highlighter-rouge">Ctrl+O</code>:向前循环筛选模式</li>
<li><code class="language-plaintext highlighter-rouge">Shift+Ctrl+O</code>:向后循环筛选模式</li>
<li><code class="language-plaintext highlighter-rouge">Alt+D/T/U/L/A</code>:直接跳转到特定筛选模式</li>
</ul>
<h2 id="筛选与搜索语义">筛选与搜索语义</h2>
<p>筛选模式(<code class="language-plaintext highlighter-rouge">TreeList</code>):</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">default</code></li>
<li><code class="language-plaintext highlighter-rouge">no-tools</code></li>
<li><code class="language-plaintext highlighter-rouge">user-only</code></li>
<li><code class="language-plaintext highlighter-rouge">labeled-only</code></li>
<li><code class="language-plaintext highlighter-rouge">all</code></li>
</ol>
<h3 id="default"><code class="language-plaintext highlighter-rouge">default</code></h3>
<p>显示对话节点以及任何未明确抑制的条目类型。它会隐藏这些设置/记账类条目:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">label</code></li>
<li><code class="language-plaintext highlighter-rouge">custom</code></li>
<li><code class="language-plaintext highlighter-rouge">model_change</code></li>
<li><code class="language-plaintext highlighter-rouge">thinking_level_change</code></li>
</ul>
<p>当前代码中,其他未特殊渲染的内部条目类型可能显示为空行。</p>
<h3 id="no-tools"><code class="language-plaintext highlighter-rouge">no-tools</code></h3>
<p>同 <code class="language-plaintext highlighter-rouge">default</code>,额外隐藏 <code class="language-plaintext highlighter-rouge">toolResult</code> 消息。</p>
<h3 id="user-only"><code class="language-plaintext highlighter-rouge">user-only</code></h3>
<p>仅 role 为 <code class="language-plaintext highlighter-rouge">user</code> 的 <code class="language-plaintext highlighter-rouge">message</code> 条目。</p>
<h3 id="labeled-only"><code class="language-plaintext highlighter-rouge">labeled-only</code></h3>
<p>仅当前解析到标签的条目。</p>
<h3 id="all"><code class="language-plaintext highlighter-rouge">all</code></h3>
<p>会话树中的所有内容,包括记账/自定义条目。</p>
<h3 id="仅工具调用的助手节点行为">仅工具调用的助手节点行为</h3>
<p>仅包含<strong>工具调用</strong>(无文本)的助手消息,在所有筛选视图中默认隐藏,除非:</p>
<ul>
<li>消息是 error/aborted(<code class="language-plaintext highlighter-rouge">stopReason</code> 不是 <code class="language-plaintext highlighter-rouge">stop</code>/<code class="language-plaintext highlighter-rouge">toolUse</code>),或</li>
<li>它是当前叶(始终保持可见)</li>
</ul>
<h3 id="搜索行为">搜索行为</h3>
<ul>
<li>查询按空格分词</li>
<li>匹配是模糊的(子序列)且不区分大小写(<code class="language-plaintext highlighter-rouge">fuzzyMatch</code>)</li>
<li>所有 token 都必须匹配(AND 语义)</li>
<li>可搜索文本包括标签、角色和类型特定内容(消息文本、分支摘要文本、自定义类型、工具命令片段等)</li>
</ul>
<h2 id="选中结果重要">选中结果(重要)</h2>
<p><code class="language-plaintext highlighter-rouge">navigateTree</code> 根据选中条目类型计算新叶行为:</p>
<h3 id="选中-user-消息">选中 <code class="language-plaintext highlighter-rouge">user</code> 消息</h3>
<ul>
<li>新叶变为选中条目的 <code class="language-plaintext highlighter-rouge">parentId</code></li>
<li>如果父节点是 <code class="language-plaintext highlighter-rouge">null</code>(根用户消息),叶重置为根(<code class="language-plaintext highlighter-rouge">resetLeaf()</code>)</li>
<li>选中消息文本复制到编辑器以便编辑/重新提交</li>
</ul>
<h3 id="选中-custom_message">选中 <code class="language-plaintext highlighter-rouge">custom_message</code></h3>
<ul>
<li>叶规则同 user 消息(<code class="language-plaintext highlighter-rouge">parentId</code>)</li>
<li>提取文本内容并复制到编辑器</li>
</ul>
<h3 id="选中非用户节点assistanttoolsummarycompactioncustom-bookkeeping-等">选中非用户节点(assistant/tool/summary/compaction/custom bookkeeping 等)</h3>
<ul>
<li>新叶变为选中节点 id</li>
<li>编辑器不预填充</li>
</ul>
<h3 id="选中当前叶">选中当前叶</h3>
<ul>
<li>无操作;选择器关闭并返回“Already at this point”</li>
</ul>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Selection decision (simplified):
selected node
│
├─ is current leaf? ── yes ──> close selector (no-op)
│
├─ is user/custom_message? ── yes ──> leaf := parentId (or resetLeaf for root)
│ + prefill editor text
│
└─ otherwise ──> leaf := selected node id
+ no editor prefill
</code></pre></div></div>
<h2 id="切换时摘要流程">切换时摘要流程</h2>
<p>摘要提示由 <code class="language-plaintext highlighter-rouge">branchSummary.enabled</code> 控制(默认:<code class="language-plaintext highlighter-rouge">false</code>)。</p>
<p>启用后,选中节点后 UI 询问:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">No summary</code></li>
<li><code class="language-plaintext highlighter-rouge">Summarize</code></li>
<li><code class="language-plaintext highlighter-rouge">Summarize with custom prompt</code></li>
</ul>
<p>流程细节:</p>
<ul>
<li>摘要提示中的 Esc 重新打开树选择器</li>
<li>自定义提示取消返回到摘要选择循环</li>
<li>摘要期间,UI 显示 loader 并将 <code class="language-plaintext highlighter-rouge">Esc</code> 绑定到 <code class="language-plaintext highlighter-rouge">abortBranchSummary()</code></li>
<li>如果摘要被中止,树选择器重新打开且不应用移动</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">navigateTree</code> 内部:</p>
<ul>
<li>从旧叶到公共祖先收集已放弃分支的条目</li>
<li>发出 <code class="language-plaintext highlighter-rouge">session_before_tree</code>(扩展可以取消或注入摘要)</li>
<li>仅在请求且需要时使用默认摘要器</li>
<li>使用以下方式应用移动:
<ul>
<li><code class="language-plaintext highlighter-rouge">branchWithSummary(...)</code> 当存在摘要时</li>
<li><code class="language-plaintext highlighter-rouge">branch(newLeafId)</code> 用于无摘要的非根移动</li>
<li><code class="language-plaintext highlighter-rouge">resetLeaf()</code> 用于无摘要的根移动</li>
</ul>
</li>
<li>用重建的会话上下文替换 agent 对话</li>
<li>发出 <code class="language-plaintext highlighter-rouge">session_tree</code></li>
</ul>
<p>注意:如果用户请求摘要但无可摘要内容,导航继续而不创建摘要条目。</p>
<h2 id="标签">标签</h2>
<p>树 UI 中的标签编辑调用 <code class="language-plaintext highlighter-rouge">appendLabelChange(targetId, label)</code>。</p>
<ul>
<li>非空标签设置/更新解析后的标签</li>
<li>空标签清除它</li>
<li>标签作为仅追加的 <code class="language-plaintext highlighter-rouge">label</code> 条目存储</li>
<li>树节点显示解析后的标签状态,而非原始标签条目历史</li>
</ul>
<h2 id="tree-与相邻操作"><code class="language-plaintext highlighter-rouge">/tree</code> 与相邻操作</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>范围</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="language-plaintext highlighter-rouge">/tree</code></td>
<td>当前会话文件</td>
<td>将叶移动到选中位置(同一文件)</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">/branch</code></td>
<td>通常是当前会话文件 -> 新会话文件</td>
<td>默认从选中的<strong>用户</strong>消息分支到新会话文件;如果 <code class="language-plaintext highlighter-rouge">doubleEscapeAction = "tree"</code>,<code class="language-plaintext highlighter-rouge">/branch</code> 打开树导航 UI</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">/fork</code></td>
<td>整个当前会话</td>
<td>将会话复制到新的持久化会话文件</td>
</tr>
<tr>
<td><code class="language-plaintext highlighter-rouge">/resume</code></td>
<td>会话列表</td>
<td>切换到另一个会话文件</td>
</tr>
</tbody>
</table>
<p>关键区别:<code class="language-plaintext highlighter-rouge">/tree</code> 是单个会话文件内的导航/重定位工具。<code class="language-plaintext highlighter-rouge">/branch</code>、<code class="language-plaintext highlighter-rouge">/fork</code> 和 <code class="language-plaintext highlighter-rouge">/resume</code> 都会改变会话文件上下文。</p>
<h2 id="操作者工作流">操作者工作流</h2>
<h3 id="从更早的用户提示重新运行且不丢失当前分支">从更早的用户提示重新运行,且不丢失当前分支</h3>
<ol>
<li><code class="language-plaintext highlighter-rouge">/tree</code></li>
<li>搜索/选中更早的用户消息</li>
<li>选择 <code class="language-plaintext highlighter-rouge">No summary</code>(或根据需要摘要)</li>
<li>在编辑器中编辑预填充的文本</li>
<li>提交</li>
</ol>
<p>效果:新分支在同一会话文件内从选中点开始增长。</p>
<h3 id="带上下文面包屑离开当前分支">带上下文面包屑离开当前分支</h3>
<ol>
<li>启用 <code class="language-plaintext highlighter-rouge">branchSummary.enabled</code></li>
<li><code class="language-plaintext highlighter-rouge">/tree</code> 并选中目标节点</li>
<li>选择 <code class="language-plaintext highlighter-rouge">Summarize</code>(或自定义提示)</li>
</ol>
<p>效果:<code class="language-plaintext highlighter-rouge">branch_summary</code> 条目在继续之前被附加到目标位置。</p>
<h3 id="调查隐藏的记账条目">调查隐藏的记账条目</h3>
<ol>
<li><code class="language-plaintext highlighter-rouge">/tree</code></li>
<li>按 <code class="language-plaintext highlighter-rouge">Alt+A</code>(all)</li>
<li>搜索 <code class="language-plaintext highlighter-rouge">model</code>、<code class="language-plaintext highlighter-rouge">thinking</code>、<code class="language-plaintext highlighter-rouge">custom</code> 或 labels</li>
</ol>
<p>效果:检查完整的内部时间线,而不仅是对话节点。</p>
<h3 id="为后续跳转添加书签">为后续跳转添加书签</h3>
<ol>
<li><code class="language-plaintext highlighter-rouge">/tree</code></li>
<li>移动到条目</li>
<li>按 <code class="language-plaintext highlighter-rouge">Shift+L</code> 并设置标签</li>
<li>之后使用 <code class="language-plaintext highlighter-rouge">Alt+L</code>(<code class="language-plaintext highlighter-rouge">labeled-only</code>)快速跳转</li>
</ol>
<p>效果:在持久分支里程碑之间快速导航。</p>
</system-interrupt>
Pending injections are cleared after content generation.
contextMode behavior on partial output
discard: partial/aborted assistant message is removed before retry.keep: partial assistant output remains in conversation state; reminder is appended after it.
Non-interrupting matches
Non-interrupting matches split by matchContext.source:
-
source === "tool"(tool-source match). The rule is bucketed into#perToolTtsrInjections, keyed by the matched tool call’sid. There is no deferred follow-up turn and the stream is not aborted. When the tool actually produces a result, theafterToolCallhook prepends a renderedttsr-tool-reminder.mdblock toctx.result.content(a singletextblock inserted ahead of the tool’s own content), and persists attsr_injectionentry with the consumed rule names. The template payload is:```xml
... /tree 命令参考
| English | 中文 |
/tree 打开交互式会话树导航器。它让你跳转到当前会话文件的任意条目,并从该点继续。
这是文件内的叶移动,不是新会话导出。
/tree 的作用
- 从当前会话条目构建树(
SessionManager.getTree()) - 打开
TreeSelectorComponent,支持键盘导航、筛选和搜索 - 选中后,调用
AgentSession.navigateTree(targetId, { summarize, customInstructions }) - 从新的叶路径重建可见聊天
- 选择 user/custom 消息时可预填充编辑器文本
主要实现:
src/slash-commands/builtin-registry.ts(/tree、/branch命令路由)src/modes/controllers/input-controller.ts(按键绑定、双击 Esc 行为)src/modes/controllers/selector-controller.ts(树 UI 启动 + 摘要提示流程)src/modes/components/tree-selector.ts(导航、筛选、搜索、标签、渲染)src/session/agent-session.ts(navigateTree叶切换 + 可选摘要)src/session/session-manager.ts(getTree、branch、branchWithSummary、resetLeaf、标签持久化)
如何打开
以下任一方式打开同一个选择器:
/tree- 为
app.session.treeaction 配置的按键绑定 - 空编辑器双击 Esc,且
doubleEscapeAction = "tree"(默认) /branch且doubleEscapeAction = "tree"时(路由到树选择器而非仅用户的 branch picker)
树 UI 模型
树由会话条目的父指针(id / parentId)渲染。
- 子项按时间戳升序排列(旧的在前,新的在下)
- 活跃分支(从根到当前叶的路径)用圆点标记
- 标签(如有)在节点文本前渲染为
[label] - 若存在多个根(孤立/断链),它们会显示在一个虚拟分支根下
Example tree view (active path marked with •):
├─ user: "Start task"
│ └─ assistant: "Plan"
│ ├─ • user: "Try approach A"
│ │ └─ • assistant: "A result"
│ │ └─ • [milestone] user: "Continue A"
│ └─ user: "Try approach B"
│ └─ assistant: "B result"
选择器以当前选中项为中心,最多显示:
max(5, floor(terminalHeight / 2))行
树选择器中的按键绑定
Up/Down:移动选中项(循环)Left/Right:上翻页 / 下翻页Enter:选中节点Esc:有搜索时清空搜索;否则关闭选择器Ctrl+C:关闭选择器Type:追加到搜索查询Backspace:删除搜索字符Shift+L:编辑/清除选中条目的标签Ctrl+O:向前循环筛选模式Shift+Ctrl+O:向后循环筛选模式Alt+D/T/U/L/A:直接跳转到特定筛选模式
筛选与搜索语义
筛选模式(TreeList):
defaultno-toolsuser-onlylabeled-onlyall
default
显示对话节点以及任何未明确抑制的条目类型。它会隐藏这些设置/记账类条目:
labelcustommodel_changethinking_level_change
当前代码中,其他未特殊渲染的内部条目类型可能显示为空行。
no-tools
同 default,额外隐藏 toolResult 消息。
user-only
仅 role 为 user 的 message 条目。
labeled-only
仅当前解析到标签的条目。
all
会话树中的所有内容,包括记账/自定义条目。
仅工具调用的助手节点行为
仅包含工具调用(无文本)的助手消息,在所有筛选视图中默认隐藏,除非:
- 消息是 error/aborted(
stopReason不是stop/toolUse),或 - 它是当前叶(始终保持可见)
搜索行为
- 查询按空格分词
- 匹配是模糊的(子序列)且不区分大小写(
fuzzyMatch) - 所有 token 都必须匹配(AND 语义)
- 可搜索文本包括标签、角色和类型特定内容(消息文本、分支摘要文本、自定义类型、工具命令片段等)
选中结果(重要)
navigateTree 根据选中条目类型计算新叶行为:
选中 user 消息
- 新叶变为选中条目的
parentId - 如果父节点是
null(根用户消息),叶重置为根(resetLeaf()) - 选中消息文本复制到编辑器以便编辑/重新提交
选中 custom_message
- 叶规则同 user 消息(
parentId) - 提取文本内容并复制到编辑器
选中非用户节点(assistant/tool/summary/compaction/custom bookkeeping 等)
- 新叶变为选中节点 id
- 编辑器不预填充
选中当前叶
- 无操作;选择器关闭并返回“Already at this point”
Selection decision (simplified):
selected node
│
├─ is current leaf? ── yes ──> close selector (no-op)
│
├─ is user/custom_message? ── yes ──> leaf := parentId (or resetLeaf for root)
│ + prefill editor text
│
└─ otherwise ──> leaf := selected node id
+ no editor prefill
切换时摘要流程
摘要提示由 branchSummary.enabled 控制(默认:false)。
启用后,选中节点后 UI 询问:
No summarySummarizeSummarize with custom prompt
流程细节:
- 摘要提示中的 Esc 重新打开树选择器
- 自定义提示取消返回到摘要选择循环
- 摘要期间,UI 显示 loader 并将
Esc绑定到abortBranchSummary() - 如果摘要被中止,树选择器重新打开且不应用移动
navigateTree 内部:
- 从旧叶到公共祖先收集已放弃分支的条目
- 发出
session_before_tree(扩展可以取消或注入摘要) - 仅在请求且需要时使用默认摘要器
- 使用以下方式应用移动:
branchWithSummary(...)当存在摘要时branch(newLeafId)用于无摘要的非根移动resetLeaf()用于无摘要的根移动
- 用重建的会话上下文替换 agent 对话
- 发出
session_tree
注意:如果用户请求摘要但无可摘要内容,导航继续而不创建摘要条目。
标签
树 UI 中的标签编辑调用 appendLabelChange(targetId, label)。
- 非空标签设置/更新解析后的标签
- 空标签清除它
- 标签作为仅追加的
label条目存储 - 树节点显示解析后的标签状态,而非原始标签条目历史
/tree 与相邻操作
| 操作 | 范围 | 结果 |
|---|---|---|
/tree |
当前会话文件 | 将叶移动到选中位置(同一文件) |
/branch |
通常是当前会话文件 -> 新会话文件 | 默认从选中的用户消息分支到新会话文件;如果 doubleEscapeAction = "tree",/branch 打开树导航 UI |
/fork |
整个当前会话 | 将会话复制到新的持久化会话文件 |
/resume |
会话列表 | 切换到另一个会话文件 |
关键区别:/tree 是单个会话文件内的导航/重定位工具。/branch、/fork 和 /resume 都会改变会话文件上下文。
操作者工作流
从更早的用户提示重新运行,且不丢失当前分支
/tree- 搜索/选中更早的用户消息
- 选择
No summary(或根据需要摘要) - 在编辑器中编辑预填充的文本
- 提交
效果:新分支在同一会话文件内从选中点开始增长。
带上下文面包屑离开当前分支
- 启用
branchSummary.enabled /tree并选中目标节点- 选择
Summarize(或自定义提示)
效果:branch_summary 条目在继续之前被附加到目标位置。
调查隐藏的记账条目
/tree- 按
Alt+A(all) - 搜索
model、thinking、custom或 labels
效果:检查完整的内部时间线,而不仅是对话节点。
为后续跳转添加书签
/tree- 移动到条目
- 按
Shift+L并设置标签 - 之后使用
Alt+L(labeled-only)快速跳转
效果:在持久分支里程碑之间快速导航。
</system-reminder>
- **`source === "text"` / `"thinking"` (prose-source match).** Behavior is unchanged: the rule is queued in `#pendingTtsrInjections` and, after a successful non-error, non-aborted assistant message, `AgentSession` injects the hidden `ttsr-injection` custom message as a follow-up and schedules continuation.
Within a single matching batch, each rule is attached to exactly one sibling tool call — if multiple sibling tool calls would satisfy the same rule, deduplication picks one and the others are left untouched. Multiple distinct rules can still fold onto the same tool call.
#### Implications for tool authors and transcript readers
- The tool's own `toolResult` content is preserved verbatim; the reminder is **prepended** as an additional leading text block. Renderers that assume `content[0]` is the tool's primary output must scan past any block whose text begins with `<system-reminder reason="rule_violation"` (or filter on the wrapper tag) to find the real payload.
- The reminder is in-band on the tool result, not a separate `custom_message`/`ttsr-injection` entry. Transcript readers looking for non-interrupting TTSR activity on tool-source rules MUST inspect tool results (and the persisted `ttsr_injection` entry list), not just synthetic injection entries.
- A single tool result may carry reminders for several rules concatenated with a blank line between rendered templates.
- If the assistant message ends with `stopReason === "aborted"` or `"error"` before the matched tools run, the pending per-tool buckets are cleared — those rules are **not** persisted as injected and remain eligible to re-trigger on a future turn (subject to repeat policy).
## 5. Repeat policy and gap logic
`TtsrManager` tracks `#messageCount` and per-rule `lastInjectedAt`.
### `repeatMode: "once"`
A rule can trigger only once after it has an injection record.
### `repeatMode: "after-gap"`
A rule can re-trigger only when:
- `messageCount - lastInjectedAt >= repeatGap`
`messageCount` increments on `turn_end`, so gap is measured in completed turns, not stream chunks.
## 6. Event emission and extension/hook surfaces
### Session event
`AgentSessionEvent` includes:
```ts
{ type: "ttsr_triggered"; rules: Rule[] }
Extension runner
#emitSessionEvent() routes the event to:
- extension listeners (
ExtensionRunner.emit({ type: "ttsr_triggered", rules })) - local session subscribers
Hook and custom-tool typing
- extension API exposes
on("ttsr_triggered", ...) - hook API exposes
on("ttsr_triggered", ...) - custom tools receive
onSession({ reason: "ttsr_triggered", rules })
Interactive-mode rendering difference
Interactive mode uses session.isTtsrAbortPending to suppress showing the aborted assistant stop reason as a visible failure during TTSR interruption, and renders a TtsrNotificationComponent when the event arrives.
7. Persistence and resume state (current implementation)
SessionManager persists injected-rule state:
- entry type:
ttsr_injection - append API:
appendTtsrInjection(ruleNames) - query API:
getInjectedTtsrRules() - context reconstruction includes
SessionContext.injectedTtsrRules
TtsrManager supports restoration via restoreInjected(ruleNames).
Current wiring status
In the current runtime path:
- interrupted injections append a hidden
custom_messagewithcustomType: "ttsr-injection"and append attsr_injectionentry viaappendTtsrInjection(...) - deferred non-interrupting prose-source injections are marked/persisted when their queued custom message reaches
message_end - non-interrupting tool-source injections are marked at match time and persisted via
appendTtsrInjection(...)from theafterToolCallhook when the matched tool’s result is produced createAgentSession()restoresexistingSession.injectedTtsrRulesintottsrManager
Net effect: injected-rule suppression is persisted/restored across session reload/resume for the current branch path.
8. Race boundaries and ordering guarantees
Abort vs retry callback
- abort is synchronous from TTSR handler perspective (
agent.abort()called immediately) - retry is deferred by timer (
50ms) - extension notification is asynchronous and intentionally not awaited before abort/retry scheduling
Multiple matches in same stream window
checkDelta() returns all currently matching eligible rules for that scoped buffer. Pending injections are deduplicated by rule name before injection.
Between abort and continue
During the timer window, state can change (user interruption, mode actions, additional events). The retry call is best-effort: agent.continue() is awaited in a try/catch; on failure the error is swallowed and the TTSR resume gate is resolved.
9. Edge cases summary
- Invalid
conditionregex: skipped with warning; other conditions/rules continue. - Duplicate rule names at capability layer: lower-priority duplicates are shadowed before registration.
- Duplicate names at manager layer: second registration is ignored.
ttsr.disabledRules: listed names are dropped before TTSR registration and are not surfaced through always-apply/rulebook buckets.ttsr.builtinRules: false: embeddedbuiltin-defaultsrules are dropped before TTSR registration; user/project rules still load.globson a TTSR rule require the stream match context to include at least one matching file path.contextMode: "keep": partial violating output can remain in context before reminder retry.interruptMode: "never": prose-source matches queue a deferred hidden injection after a successful assistant message; tool-source matches fold an in-band<system-reminder>into the matched tool call’stoolResultcontent via theafterToolCallhook (no mid-stream abort, no separate follow-up turn).- Tool-source non-interrupting buckets are cleared when the parent assistant message ends with
stopReason === "aborted"or"error", so rules whose target tool never produced a result remain eligible to re-trigger. - Repeat-after-gap depends on turn count increments at
turn_end; mid-turn chunks do not advance gap counters.