# 检查点、回退与分叉

> Write 与 Edit 每次成功都留一份文件快照；回退对话只移动会话上的分支游标，不删消息；回退文件分“按检查点直接恢复”和“先预览、按哈希校验再应用”两条路；分叉把前缀复制进子会话，稳定分叉在回合结束时就钉好边界；旁支会话则是一个只继承上下文的隐藏分叉。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.org/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.org/manual/zcode/rewind-fork
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

会话库只追加不删除，这一篇讲建在它上面的三件事：**检查点**（改文件前留一份快照）、**回退**（把对话或文件退回某个点）和**分叉**（从某个点复制出一个新会话）。协议与载荷定义在 `apps/zcode-cli/packages/contracts/src/rewind/index.ts`，实现集中在 `apps/zcode-cli/packages/core/src/runtime/methods/` 下的 `rewind.ts`、`rewind-message.ts`、`file-rewind.ts`、`workspace-checkpoints.ts`、`workspace-checkpoint-persistence.ts`、`workspace-fork.ts`、`session-fork.ts`、`stable-fork-boundary.ts` 八个文件，共约 4400 行；落库部分见上一篇[SQLite 会话库](https://daiw.org/manual/zcode/session-store)。

先说清一件容易误会的事：Agent 的检查点不是 Git 快照，而是工具层记下的“改动前后全文”。回退作用于三种范围：`conversation`、`workspace`、`both`（`apps/zcode-cli/packages/contracts/src/rewind/index.ts:21`）。

## 怎么用

TUI 里的 `/rewind` 与 `/fork` 不带参数时都先弹出最近 50 个检查点的选择列表（`apps/zcode-cli/packages/cli/src/command-center/create.ts:229`、`create.ts:242`）；带参数的 `/rewind` 当作提示词交给运行时，在回合入口解析（`apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:106`）。解析器认识这些写法（`apps/zcode-cli/packages/core/src/runtime/helpers/commands.ts:17`）：

| 写法 | 效果 |
| --- | --- |
| `/rewind status`（或 `help`、`-h`） | 显示最近一个检查点；TUI 里不带参数时改为弹列表 |
| `/rewind latest`、`/rewind <checkpointId>` | 把该检查点记下的文件恢复到改动前 |
| `/rewind conversation <messageId>`（或 `message`） | 回退对话到这条用户消息之前 |
| `/rewind code <messageId>`（或 `workspace`） | 恢复与这条消息相关的最近一个检查点 |
| `/rewind both <messageId>` | 先恢复文件，成功后再回退对话 |
| `/rewind cascade <范围> <messageId>` | 从这条消息往后的所有检查点倒序恢复；范围含对话时再回退对话 |
| `/fork`、`/fork latest`、`/fork <checkpointId>` | 从检查点分叉出新会话；TUI 会随即切到新会话（`apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:239`） |

桌面与 Web 端不走斜杠命令，而是 ZCode Protocol V4 的命令（见[ZCode Protocol V4](https://daiw.org/manual/zcode/zcode-protocol)）：消息上的“编辑”按钮发 `editUserQuery`，重试发 `retryTurn`，“分叉”按钮发 `forkAssistant`（`packages/ui/src/i18n/locales/zh-CN.ts:3977`、`zh-CN.ts:3986`），本轮文件改动摘要上的“撤销”发 `applyFileRewind`（`zh-CN.ts:1540`）；选中回复文字后点“在辅助对话中提问”，或输入 `/side <问题>`、`/btw <问题>`，创建旁支会话（`zh-CN.ts:645`、`packages/ui/src/v4/slashCommands.ts:110`、`packages/ui/src/v4/MarkdownSelectionTooltip.tsx:104`）。配置里有个 `features.rewind`，默认 `true`（`apps/zcode-cli/packages/contracts/src/config/index.ts:310`），但全仓库没有任何代码读取它，从代码看设成 `false` 也关不掉这些功能。

## 检查点：每次成功的 Write 或 Edit 记一份

检查点不是按回合或定时拍的。每个工具结果闭合后，回合循环都会调一次 `emitFileMutationCheckpoint`，把本回合用户消息的 ID 当作锚点传进去（`apps/zcode-cli/packages/core/src/runtime/methods/turn-tools.ts:370`）；只有输出里同时带 `filePath`、`structuredPatch` 和 `originalFile` 的成功结果才算数（`apps/zcode-cli/packages/core/src/runtime/helpers/rewind.ts:38`），目前产出这种结构的只有 Write 与 Edit 两个工具，新建文件的 `originalFile` 为 `null`（`apps/zcode-cli/packages/core/src/tool/handlers/write.ts:177`）。落盘的部分（`apps/zcode-cli/packages/core/src/runtime/methods/tools.ts:187`）：

```ts
    const artifact = await this.artifactStore.writeToolResultArtifact(
      {
        sessionId: this.sessionId,
        turnId: options.traceContext.turnId,
        toolCallId: options.result.toolCallId,
        toolName: options.result.toolName,
        content: stringifyWorkspaceCheckpointArtifact(candidate, options.result),
        contentType: WORKSPACE_CHECKPOINT_CONTENT_TYPE,
        retention: "session",
        trace: options.traceContext,
      },
      { signal: options.abortSignal },
    );
    throwIfTurnAborted(options.abortSignal);

    const event = this.createEvent(
      SessionEventType.CheckpointCreated,
      {
        checkpointId: `checkpoint_${crypto.randomUUID()}`,
        messageId: options.messageId,
        targetMessageId: options.messageId,
        toolMessageId: options.toolMessageId,
        scope: RewindScope.Workspace,
        snapshotRef: artifact.uri,
        diffRef: artifact.uri,
        fileCount: 1,
      },
```

一个检查点只管一个文件。快照是一份 `workspace_file_before_change` JSON，存改动前全文、改动后全文、是否原本存在和结构化补丁（`helpers/rewind.ts:58`，schema 见 `rewind/index.ts:100`），写在 `~/.zcode/cli/artifacts/<会话 ID>/` 下，以 `zcode-artifact://` 地址引用（`apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:354`、`apps/zcode-cli/packages/adapters/src/storage/index.ts:71`）。写快照失败只记警告，不影响工具结果（`tools.ts:240`）。

事件流在内存里，进程一退，`CheckpointCreated` 就没了。所以事件管线把它另存一份到 `session_entry`，类型是 `runtime/workspace_checkpoint`（`apps/zcode-cli/packages/core/src/runtime/methods/events.ts:270`），注释说只留 artifact 不留关联载荷的话，冷恢复后预览与应用都找不到快照（`apps/zcode-cli/packages/core/src/runtime/methods/workspace-checkpoint-persistence.ts:42`）。恢复会话时这些条目按原序号回放进内存事件流（`workspace-checkpoint-persistence.ts:101`，调用在 `apps/zcode-cli/packages/core/src/runtime/methods/resume.ts:191`）。同一次写入还会记进本回合的文件变更表，回合最后一次模型调用把它汇总成增删行数挂在 `ModelComplete` 事件上，桌面端“本轮改动”和撤销入口就来自这里（`apps/zcode-cli/packages/core/src/runtime/helpers/turn-file-changes.ts:7`、`apps/zcode-cli/packages/core/src/runtime/methods/turn-model-step.ts:593`）。

这套机制有几个边界：

- **Bash 改的文件没有检查点**。Bash 的输出里没有那三个字段，不会产生快照。
- **快照不会被清理**。写入时传了 `retention: "session"`，但 Node 实现 `NodeToolArtifactStore` 从头到尾没读这个字段，也没有找到清理 `cli/artifacts` 目录的代码；桌面资源管理器还把它归为不可清理的工具输出（`packages/services/src/storage/domain/storageCatalog.ts:29`、`storageCatalog.ts:80`）。
- **桌面端另有一套 Git 检查点**。`packages/services` 里有个 `IGitCheckpointService`，用临时 `GIT_INDEX_FILE` 把工作区状态做成隐藏提交，挂在 `refs/zcode/checkpoints/...` 下，不碰用户的 index 与分支（`packages/services/src/git/repo/gitCheckpointRepo.ts:240`、`packages/services/src/git/repo/gitCheckpointHelpers.ts:28`）。它作为 RPC 服务注册了（`packages/services/src/git/gitCheckpoint.ts:20`），但本仓库的界面代码里没有找到调用它创建检查点的地方，和上面的 Agent 检查点也不相通。

NOTICE 把话说在前面（`NOTICE.md:15`）：

> 任务快照、Git 检查点和会话恢复不能替代对全部本地文件、数据库及外部服务的备份。

## 回退对话：只移游标，不删消息

消息与 part 表只追加。回退对话不删任何一行，只在会话行的 `revert` 列写一个游标（`apps/zcode-cli/packages/core/src/runtime/methods/rewind-message.ts:566`）：

```ts
  await this.sessionStore!.setRevert({
    sessionID: this.sessionId,
    revert: {
      // 只保存 target/created 无法在连续编辑或重启后还原旧 rewind 前缀。
      // keptMessageIDs 保存的是本次 rewind 前 active branch 的保留前缀，避免旧分支重新浮出。
      keptMessageIDs: keptMessages.map((message) => message.info.id as MessageId),
      branchCutAfterMessageID: branchCutAfterMessageId,
      branchGeneration,
      messageID: keptMessages.at(-1)?.info.id ?? options.targetMessageId,
      kind: "conversation_rewind",
      scope: RewindScope.Conversation,
      targetMessageID: options.targetMessageId,
    },
  });
```

`keptMessageIDs` 是目标之前要保留的前缀，`branchCutAfterMessageID` 取回退这一刻库里最后一条消息（`rewind-message.ts:528`）。之后读历史时，活跃分支等于“保留的前缀”加上“游标之后新追加的消息”，中间那段旧分支还在库里，只是不再被选中（`rewind/index.ts:193`）。恢复、冷启动投影和分叉复制共用这个裁剪函数（`rewind/index.ts:175`）。

`branchGeneration` 每回退一次加一（`rewind-message.ts:529`），同步给后台任务注册表，旧分支上后台任务的迟到结果据此作废（`rewind-message.ts:581`，见[后台任务与通知](https://daiw.org/manual/zcode/background-tasks)）。提交游标之前，还要先停掉被砍掉的那几个回合里仍在运行的后台任务，停不掉就整体失败，“不提交 branch cut，也不碰 workspace”（`rewind-message.ts:646`）。游标写好后，内存里的模型历史、读文件状态、上下文前缀、缓存统计、回合计数、本回合文件变更表全部按新分支重建（`rewind-message.ts:675`）。

目标必须是一条用户消息（摘要消息不算），也就是把这次提问连同之后的回复一起撤掉。重试传来的是助手消息的 ID，纯对话范围（包括 TUI 的 `/rewind conversation`）会往前找到所属回合的用户消息（`rewind-message.ts:476`、`rewind-message.ts:418`）；`both` 与 `cascade` 不做这种映射，目标不是用户消息就直接拒绝。对话回退失败时不发 `RewindTriggered` 事件，免得界面先裁掉而库里仍是旧分支（`rewind-message.ts:663`）；文件回退失败则照发一条策略为 `unavailable` 的事件（`apps/zcode-cli/packages/core/src/runtime/methods/rewind.ts:359`）。

能不能回退由 `evaluateRewindTarget` 裁定（`rewind/index.ts:229`）。消息被压缩覆盖时，对话回退照样可行：先剪分支，再在新分支上重建压缩范围（`rewind/index.ts:316`）：

| 策略 | 何时 | 可用范围 |
| --- | --- | --- |
| `active_chain` | 目标在活跃链上，或被压缩覆盖但回退的是对话 | 有检查点时三种都行，否则只有 `conversation` |
| `file_only` | 目标被压缩覆盖、只回退文件、且有检查点 | 只恢复文件，对话停在压缩后的上下文 |
| `unavailable` | 找不到目标，或要回退文件却没有检查点 | 无 |

契约里还有第四种 `fork_required`（`rewind/index.ts:32`），`evaluateRewindTarget` 从不返回它，只作为 `SessionForked` 事件上的策略标记出现。

## 回退文件：两条路

**按检查点直接恢复**。`/rewind <checkpointId>`、`/rewind code <messageId>` 走 `rewindWorkspaceToCheckpoint`（`apps/zcode-cli/packages/core/src/runtime/methods/rewind.ts:202`）：读出快照，把文件写回改动前的内容，原本不存在的文件直接删掉（`apps/zcode-cli/packages/core/src/runtime/methods/workspace-fork.ts:47`），再写一条合成的用户提示并作为 `rewind_notice` 附件告诉模型（`methods/rewind.ts:308`）。这条路不检查文件在快照之后有没有被改过。另外要注意“一个检查点一个文件”：`/rewind <id>` 只恢复那一个文件，之后别的文件上的改动原样保留。要撤掉某条消息以来的全部改动得用 `cascade`，它先把所有快照都读出来，任何一份缺失就整体放弃，再按创建顺序倒着写，避免停在半回退状态（`rewind-message.ts:310`）。

**先预览、再应用**。桌面端的撤销用 `previewWorkspaceFileRewind` 与 `applyWorkspaceFileRewind`（`apps/zcode-cli/packages/core/src/runtime/methods/file-rewind.ts:79`、`file-rewind.ts:94`），按消息 ID 或回合找出相关检查点，先在内存里从新到旧“模拟”一遍（`file-rewind.ts:406`）：

```mermaid
flowchart TD
  A["按消息 ID 或回合找检查点"] --> B["读全部快照"]
  B -->|"读不出"| U["checkpoint_unreadable"]
  B --> C{"工具是 Bash 一类？"}
  C -->|"是"| I["列入 ignored，不回滚"]
  C -->|"否"| D["从新到旧逐个比对当前内容哈希"]
  D -->|"与改后内容不符"| E["external_modified"]
  D -->|"相符"| F["模拟写回改前内容"]
  F --> G{"有不安全文件？"}
  G -->|"有"| N["整体不应用，返回预览"]
  G -->|"没有"| H["逐个写入，边写边记 journal"]
  H -->|"中途失败"| R["按 journal 倒序补偿"]
  H --> K["commitAfterApply，发 RewindTriggered"]
```

冲突的判定就是一次哈希比较（`file-rewind.ts:417`）：

```ts
    const currentState =
      simulatedByPath.get(operation.path) ??
      (await readCurrentFileState.call(this, operation.path, traceContext, options.abortSignal));
    if ("reason" in currentState) {
      markUnsafe(unsafeByPath, {
        action: operation.action,
        message: currentState.message,
        path: operation.path,
        reason: currentState.reason,
        toolName: operation.toolName,
      });
      continue;
    }

    const expectedHash = hashContent(operation.afterContent);
    if (currentState.hash !== expectedHash) {
      markUnsafe(unsafeByPath, {
        action: operation.action,
        currentHash: currentState.hash ?? "missing",
        expectedHash,
        path: operation.path,
        reason: "external_modified",
        toolName: operation.toolName,
      });
      continue;
    }
```

同一个文件被改过多次时，后一次模拟的“当前内容”取前一次模拟写回的结果，所以多次改动能层层剥回去。快照里没存改后全文的旧数据，用结构化补丁以零模糊度重放出来（`file-rewind.ts:543`）。只要有一个文件不安全，`canApply` 就是假，整批不动（`file-rewind.ts:468`）；不安全的原因有 `checkpoint_unreadable`、`external_modified`、`file_read_failed`、`unsupported_checkpoint`，类型里还有一个 `checkpoint_missing` 但 core 从不产生（`apps/zcode-cli/packages/core/src/runtime/types.ts:566`）。应用阶段每写一个文件前先读下当前内容记进 journal，第 N 个写失败就把前面的倒序恢复，补偿也失败才升级为不可恢复的错误（`file-rewind.ts:184`）。成功后发一条原因为 `file_summary_rewind` 的 `RewindTriggered`，它同样另存为 `runtime/workspace_file_rewind` 条目，重启后撤销按钮不会复活（`workspace-checkpoint-persistence.ts:80`）。

## 编辑重发与重试

两条回退可以组合。桌面端编辑一条用户消息时，`editUserQuery` 的 `workspaceMode` 缺省是 `preserve`，只回退对话；选 `rewind` 就先预览该回合的文件撤销，有不安全文件、有 Bash 一类改动或者压根没有可撤销文件，都以 `guard.workspaceRewind*` 拒绝并把预览交回界面；通过后应用文件撤销，在 `commitAfterApply` 这道提交闸里才写对话游标，最后用新文本重新起一个回合（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/commands/handlers/fork-edit-retry.ts:150`）。只有最后一轮的用户提问能编辑、最后一轮的回复能重试（`fork-edit-retry.ts:68`、`fork-edit-retry.ts:78`）。重试等于回退对话加重发原输入，原输入要在截断之前解析好，截断后就拿不到了（`fork-edit-retry.ts:241`）。注释还记录了一个踩过的坑：组合回退曾在文件事务回调里再提交一条 `/rewind` 提示词，嵌套回退等着当前命令释放队列，界面永远停在编辑态，所以现在直接调用 core 的回退原语（`fork-edit-retry.ts:88`）。

## 分叉

分叉从父会话的某个点复制出一个子会话。子会话的 `parent_id` 指向父会话，`trace_id` 沿用父会话的根 trace，`task_type` 为 `fork`，标题是 `Fork of` 加父标题，工作目录与父会话相同（`apps/zcode-cli/packages/core/src/runtime/methods/session-fork.ts:147`）。按入口分三条路：

| 入口 | 实现 | 动不动文件 | 提交方式 |
| --- | --- | --- | --- |
| TUI `/fork`、协议旧命令 `session/fork`（`packages/shared/src/zcode-protocol/index.ts:3585`） | `forkWorkspaceFromCheckpoint`（`apps/zcode-cli/packages/core/src/runtime/methods/workspace-checkpoints.ts:133`） | 恢复检查点，改的是共享工作目录 | 先建会话再逐条复制消息 |
| 桌面端回合末尾的分叉 `forkAssistant` | `forkStableConversationAtMessage`（`session-fork.ts:1049`） | 不动 | 一个 `commitForkBundle` 事务 |
| 旁支会话 `createSelectionSideSession` | `createSelectionSideConversation`（`session-fork.ts:804`） | 不动 | 同上 |

第一条路需要格外小心：子会话与父会话共用一个工作目录，恢复文件就是改父会话的文件。代码为按消息分叉专门改过语义（`workspace-checkpoints.ts:145`）：

```ts
  // message 目标的 fork 语义是"历史包含目标回合，工作区停在 fork 点时刻"。
  // 恢复目标回合自身 checkpoint 的 beforeContent 会把该回合刚产出的文件回退/删除——
  // fork 与父会话共用工作区目录，父会话的产物也随之丢失（在最新回复上分叉时最明显）。
  // message fork 只撤销 fork 点之后的 checkpoint；显式 checkpoint 目标（/fork latest、
  // targetCheckpointId）仍保留"回到该 checkpoint 修改前"的 rewind 式语义。
```

按消息分叉（协议旧命令给的是消息目标时）只撤销分叉点之后的检查点，同一文件取分叉点后第一次改动前的内容；分叉点之后没有改动就退化成纯对话分叉（`workspace-fork.ts:149`、`workspace-fork.ts:188`）。显式指定检查点（包括 TUI 的 `/fork` 与 `/fork latest`）则仍是“回到这个检查点改动之前”，父会话里那个文件也会被写回。反过来，TUI 的 `/fork` 只认检查点，会话里还没改过任何文件时直接报 `No workspace checkpoint is available yet.`（`workspace-checkpoints.ts:162`）。协议旧命令要求父会话当时没有运行中的回合（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:2231`）。

**稳定分叉边界**。桌面端的分叉按钮只挂在一轮最后一段完成的助手回复上（`fork-edit-retry.ts:46`）。为了分叉时不必再猜“这一轮到哪条消息为止”，回合结束、`TurnComplete` 对订阅者可见之前，运行时先把边界写进最终那条助手消息的锚点（`apps/zcode-cli/packages/core/src/runtime/methods/stable-fork-boundary.ts:83`）：

```ts
  const anchor: MessageProjectionAnchor = {
    ...boundary.info.anchor,
    ...(input.traceContext.turnId ? { turnId: input.traceContext.turnId } : {}),
    historyRoundCount: input.historyRoundCount,
    orderedMessageIds,
    boundaryMessageId: input.boundaryMessageId,
    goalBoundary,
  };
  await store.saveMessage({ ...boundary.info, anchor });
```

`orderedMessageIds` 是这一轮从起点到边界的连续消息，`goalBoundary` 是此刻目标的快照（清掉只属于父会话的活跃运行字段）加上边界之前的完成验证记录 ID（`stable-fork-boundary.ts:94`、`stable-fork-boundary.ts:106`）。回合循环先结算目标用量再固定边界，两者都落库后 `TurnComplete` 才开放分叉（`turn.ts:623`）。分叉时要求这段 ID 在活跃分支里严格连续，终点必须是一条没出错的助手消息（`session-fork.ts:1132`）。父会话此时可以正在跑下一轮，分叉不会打断它（`fork-edit-retry.ts:275`）。

**一个事务提交**。稳定分叉与旁支会话把要写的东西都在内存里算好：所有消息、part、回合、目标与验证记录都换成子会话本地的新 ID，再整包交给 `commitForkBundle`（`session-fork.ts:642`、`session-fork.ts:737`）。存储层在一个 `begin immediate` 事务里写完，写之前逐项核对包里每个引用都指向子会话自己（`apps/zcode-cli/packages/adapters/src/storage/session-store/sqlite-session-store.ts:115`、`sqlite-session-store.ts:384`）；父会话里的命令事实行保证重放同一条命令只会得到同一个子会话。复制与不复制的内容：

| 复制进子会话 | 不复制 |
| --- | --- |
| 边界之前的活跃分支消息与 part（ID 重映射，元数据记 `forkOrigin`） | 被回退掉的旧分支 |
| 模型选择与执行状态两条条目；执行状态取复制范围内最后一条助手消息上记录的，而不是父会话当前的 | Todo 列表、排队中的输入 |
| 目标快照与边界前的验证记录（旁支会话除外） | 目标的活跃运行字段 |
| 一对分叉提示：一条只给模型的隐藏用户消息，一条界面上的分隔时间线（`session-fork.ts:377`） | 检查点记录，文件也不动；父会话的内存事件 |

执行状态的取法见 `session-fork.ts:599`，目标运行字段清空见 `stable-fork-boundary.ts:94`。另一条“压缩覆盖的编辑改为分叉”路径 `forkConversationBeforeMessage` 仍在 core 里（`session-fork.ts:1080`），但宿主接口上它已标注“新 editUserQuery 永不调用”（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/commands/types.ts:261`）。

把回退和分叉放在同一张图里：

```mermaid
flowchart LR
  subgraph P["父会话（message 表只追加）"]
    U1["用户 1"] --> A1["助手 1"] --> U2["用户 2"] --> A2["助手 2"] --> U3["用户 2 改写版"] --> A3["助手 3"]
  end
  R["session.revert<br/>kept = 用户 1、助手 1<br/>cutAfter = 助手 2"] -.->|"活跃分支跳过"| U2
  A1 -->|"forkAssistant 复制前缀"| C["子会话 task_type=fork<br/>新 ID 的用户 1、助手 1<br/>加分叉提示"]
  A3 -->|"在辅助对话中提问"| S["旁支会话<br/>继承上下文但界面隐藏"]
```

## 旁支会话

选中一段回复问个题外话，又不想打扰正在干活的主会话，就开一个旁支会话（selection side chat，界面上叫“辅助对话”，`zh-CN.ts:610`）。它是一次特殊的分叉：`task_type` 为 `selection_side_chat`，标题固定为 `Selection side chat`（`session-fork.ts:165`）。复制父会话的活跃分支，如果父会话正在跑，只取到本轮真实用户输入为止，不带正在生成的回复与工具调用（`session-fork.ts:833`）；复制来的消息全部标成只给模型看、界面隐藏，副屏从空白开始（`session-fork.ts:677`）；不复制目标与验证记录，连消息锚点上的目标边界也剥掉（`session-fork.ts:607`）。末尾追加一条隐藏的系统提醒（`session-fork.ts:499`）：

```ts
const SELECTION_SIDE_CHAT_BOUNDARY = [
  "The preceding conversation was inherited from the parent task for reference only.",
  "Do not continue the parent's active work automatically; answer only new questions sent in this side chat.",
  "Modify the workspace only when the user explicitly asks you to do so in this side chat.",
].join(" ");
```

注意它和父会话同样共用工作目录，这段提醒只是请模型别主动改文件，并不是隔离。TUI 没有这个入口，只有桌面与 Web 端的界面调用它。

还有一个名字容易混淆的文件：`embedded-search-branch.ts` 跟对话分支无关，它决定 Bash 可用时是否隐藏 Glob 与 Grep、改由 Bash 里的搜索前置脚本承担检索（`apps/zcode-cli/packages/core/src/runtime/methods/embedded-search-branch.ts:21`），属于[读、写、改、搜](https://daiw.org/manual/zcode/file-tools)的范畴。

下一篇：[遥测、调试与提示词轨迹](https://daiw.org/manual/zcode/telemetry-debug)——traceId 怎样一层层往下传，OpenTelemetry 默认为什么是关的，模型请求又在本地留下了哪些记录。
