MusePi

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

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:

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:

During stream (message_update)

When assistant updates arrive and rules exist:

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:

  1. Matched rules are deduplicated into #pendingTtsrInjections.
  2. #ttsrAbortPending = true and a TTSR resume gate is created.
  3. agent.abort() is called immediately.
  4. ttsr_triggered event is emitted asynchronously (fire-and-forget).
  5. 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:

  1. #ttsrAbortPending = false
  2. read ttsrManager.getSettings().contextMode
  3. if contextMode === "discard", drop the targeted partial assistant output with agent.replaceMessages(...slice(0, targetAssistantIndex))
  4. build injection content from pending rules using ttsr-interrupt.md template
  5. append and persist a hidden custom_message/runtime custom message with customType: "ttsr-injection" and details.rules
  6. mark those rule names injected, persist a ttsr_injection entry, and call agent.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 ──&gt; close selector (no-op)
   │
   ├─ is user/custom_message? ── yes ──&gt; leaf := parentId (or resetLeaf for root)
   │                                     + prefill editor text
   │
   └─ otherwise ──&gt; 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>通常是当前会话文件 -&gt; 新会话文件</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

Non-interrupting matches

Non-interrupting matches split by matchContext.source:

English 中文

/tree 打开交互式会话树导航器。它让你跳转到当前会话文件的任意条目,并从该点继续。

这是文件内的叶移动,不是新会话导出。

/tree 的作用

主要实现:

如何打开

以下任一方式打开同一个选择器:

树 UI 模型

树由会话条目的父指针(id / parentId)渲染。

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"

选择器以当前选中项为中心,最多显示:

树选择器中的按键绑定

筛选与搜索语义

筛选模式(TreeList):

  1. default
  2. no-tools
  3. user-only
  4. labeled-only
  5. all

default

显示对话节点以及任何未明确抑制的条目类型。它会隐藏这些设置/记账类条目:

当前代码中,其他未特殊渲染的内部条目类型可能显示为空行。

no-tools

default,额外隐藏 toolResult 消息。

user-only

仅 role 为 usermessage 条目。

labeled-only

仅当前解析到标签的条目。

all

会话树中的所有内容,包括记账/自定义条目。

仅工具调用的助手节点行为

仅包含工具调用(无文本)的助手消息,在所有筛选视图中默认隐藏,除非:

搜索行为

选中结果(重要)

navigateTree 根据选中条目类型计算新叶行为:

选中 user 消息

选中 custom_message

选中非用户节点(assistant/tool/summary/compaction/custom bookkeeping 等)

选中当前叶

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 询问:

流程细节:

navigateTree 内部:

注意:如果用户请求摘要但无可摘要内容,导航继续而不创建摘要条目。

标签

树 UI 中的标签编辑调用 appendLabelChange(targetId, label)

/tree 与相邻操作

操作 范围 结果
/tree 当前会话文件 将叶移动到选中位置(同一文件)
/branch 通常是当前会话文件 -> 新会话文件 默认从选中的用户消息分支到新会话文件;如果 doubleEscapeAction = "tree"/branch 打开树导航 UI
/fork 整个当前会话 将会话复制到新的持久化会话文件
/resume 会话列表 切换到另一个会话文件

关键区别:/tree 是单个会话文件内的导航/重定位工具。/branch/fork/resume 都会改变会话文件上下文。

操作者工作流

从更早的用户提示重新运行,且不丢失当前分支

  1. /tree
  2. 搜索/选中更早的用户消息
  3. 选择 No summary(或根据需要摘要)
  4. 在编辑器中编辑预填充的文本
  5. 提交

效果:新分支在同一会话文件内从选中点开始增长。

带上下文面包屑离开当前分支

  1. 启用 branchSummary.enabled
  2. /tree 并选中目标节点
  3. 选择 Summarize(或自定义提示)

效果:branch_summary 条目在继续之前被附加到目标位置。

调查隐藏的记账条目

  1. /tree
  2. Alt+A(all)
  3. 搜索 modelthinkingcustom 或 labels

效果:检查完整的内部时间线,而不仅是对话节点。

为后续跳转添加书签

  1. /tree
  2. 移动到条目
  3. Shift+L 并设置标签
  4. 之后使用 Alt+Llabeled-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:

Hook and custom-tool typing

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:

TtsrManager supports restoration via restoreInjected(ruleNames).

Current wiring status

In the current runtime path:

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

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