> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 文件快照与回滚

文件 checkpoint 用来记录一次会话里被工具修改过的本地文件状态。启用后，宿主应用可以调用 `QoderSDKClient.rewind_files(user_message_id, ...)`，把被追踪的文件恢复到某条用户消息开始处理时的状态。

这两个能力需要配合使用：没有启用 `enable_file_checkpointing=True` 时，`rewind_files()` 没有可用的文件快照。

<div id="启用文件-checkpoint" />

<div id="启用文件checkpoint" />

## 启用文件 checkpoint

`enable_file_checkpointing` 是 `QoderAgentOptions` 的字段。需要后续回滚时，使用 `QoderSDKClient` 保持同一个活跃会话，并在 options 里打开 checkpoint：

```python theme={null}
from qoder_agent_sdk import QoderAgentOptions, QoderSDKClient


options = QoderAgentOptions(
    cwd="/path/to/project",
    enable_file_checkpointing=True,
    extra_args={"replay-user-messages": None},
    allowed_tools=["Read", "Edit", "Write"],
    permission_mode="acceptEdits",
)

async with QoderSDKClient(options=options) as client:
    await client.query("Refactor src/foo.py into a cleaner implementation.")
    async for message in client.receive_response():
        ...
```

`extra_args={"replay-user-messages": None}` 不是启用 checkpoint 的开关。它的作用是让响应流里回放 `UserMessage`，并带上可作为回滚锚点的 `uuid`。如果你的应用需要让用户点选「回到这一轮之前」，通常应该同时设置它。

<div id="获取-checkpoint-id" />

<div id="获取checkpointid" />

## 获取 checkpoint ID

`rewind_files()` 以用户消息 ID 为锚点。Python SDK 中常见做法是从响应流里的 `UserMessage.uuid` 捕获这个 ID：

```python theme={null}
from qoder_agent_sdk import (
    QoderAgentOptions,
    QoderSDKClient,
    ResultMessage,
    UserMessage,
)


options = QoderAgentOptions(
    cwd="/path/to/project",
    enable_file_checkpointing=True,
    extra_args={"replay-user-messages": None},
    allowed_tools=["Write"],
    permission_mode="acceptEdits",
)

checkpoint_id: str | None = None

async with QoderSDKClient(options=options) as client:
    await client.query("Rewrite notes.txt as a two-line summary.")

    async for message in client.receive_response():
        if (
            isinstance(message, UserMessage)
            and message.uuid
            and message.parent_tool_use_id is None
        ):
            checkpoint_id = message.uuid

        if isinstance(message, ResultMessage) and message.is_error:
            raise RuntimeError(message.result or "Query failed.")

    if checkpoint_id is None:
        raise RuntimeError("No user message UUID was returned.")
```

`checkpoint_id` 是用户消息的 `uuid`，不是 `session_id`，也不是 `ResultMessage` 的 ID。它只在产生该 checkpoint 的会话上下文中有效；其他会话不能拿这个 ID 直接回滚。

<div id="dry-run-预览" />

<div id="dryrun预览" />

## Dry run 预览

执行回滚前，建议先用 `dry_run=True` 预览影响范围。Dry run 不会修改文件，适合做确认弹窗或审计日志。

```python theme={null}
preview = await client.rewind_files(checkpoint_id, dry_run=True)

if not preview["canRewind"]:
    print(preview.get("error", "Unable to rewind files."))
else:
    print(
        {
            "files": len(preview.get("filesChanged", [])),
            "insertions": preview.get("insertions", 0),
            "deletions": preview.get("deletions", 0),
        }
    )

    for file_path in preview.get("filesChanged", []):
        print(f"will be reverted: {file_path}")
```

`RewindFilesResult` 包含这些字段：

| 字段             | 类型          | 说明                             |
| -------------- | ----------- | ------------------------------ |
| `canRewind`    | `bool`      | 是否可以执行回滚。dry run 失败时不抛错，由该字段标记 |
| `error`        | `str`       | `canRewind=False` 时的诊断文案       |
| `filesChanged` | `list[str]` | 将要恢复或已经恢复的文件路径列表               |
| `insertions`   | `int`       | 回滚动作会撤销的新增行数汇总                 |
| `deletions`    | `int`       | 回滚动作会撤销的删除行数汇总                 |

当前返回值只包含受影响文件列表和汇总的行级统计，不返回每个文件的具体 diff。需要展示逐文件差异时，可以先 dry run 获取 `filesChanged`，再结合自己的工作区 diff 逻辑展示。

<div id="执行回滚" />

## 执行回滚

确认影响范围后，不传 `dry_run` 即可执行回滚：

```python theme={null}
result = await client.rewind_files(checkpoint_id)

if result["canRewind"]:
    print(result.get("filesChanged", []))
```

回滚只恢复被 checkpoint 追踪到的本地文件状态，不会回滚对话历史。也就是说，模型仍然保留之前会话里的上下文；UI 需要根据 `filesChanged` 自行刷新编辑器、文件树或 diff 视图。

<div id="失败语义" />

## 失败语义

| 调用形式                                          | 不可回滚时的表现                                         |
| --------------------------------------------- | ------------------------------------------------ |
| `await client.rewind_files(id, dry_run=True)` | 返回 `{"canRewind": False, "error": ...}`，适合直接展示诊断 |
| `await client.rewind_files(id)`               | 抛出异常，调用方应捕获并展示失败原因                               |

```python theme={null}
try:
    await client.rewind_files(checkpoint_id)
except Exception as exc:
    print(str(exc))
```

常见失败原因包括：没有启用 `enable_file_checkpointing`、传入的 ID 不是有效用户消息 UUID、该 ID 不属于当前会话、目标消息没有可回滚的文件快照。

<div id="settings-关系" />

<div id="settings关系" />

## Settings 关系

`QoderAgentOptions.settings` 可以和 `enable_file_checkpointing` 同时使用：

```python theme={null}
options = QoderAgentOptions(
    cwd="/path/to/project",
    settings={"theme": "dark"},
    enable_file_checkpointing=True,
)
```

当 `enable_file_checkpointing=True` 时，SDK 会在传给 CLI 的 settings 中合并 `general.fileCheckpointing.enabled = True`。如果已有其他 settings 字段，它们会被保留；如果已有 `fileCheckpointing` 配置，`enabled` 会以 SDK 选项为准。

<div id="边界" />

## 边界

* 只回滚本地文件 checkpoint；MCP 工具、远程服务或数据库等外部副作用不会被撤销。
* 通过 `Bash` 直接写文件的变更不作为可回滚文件快照处理。
* 文件内容可以恢复；目录创建这类目录级副作用不一定会被撤销。
* checkpoint ID 与会话绑定。恢复同一会话后可以继续使用对应 ID；不同会话之间不可混用。

<div id="字段速查" />

## 字段速查

| 入口                                                            | 类型               | 说明        |                                                              |
| ------------------------------------------------------------- | ---------------- | --------- | ------------------------------------------------------------ |
| `QoderAgentOptions.enable_file_checkpointing`                 | \`bool           | None\`    | 启用文件 checkpoint，供 `rewind_files()` 使用                        |
| `QoderAgentOptions.extra_args`                                | \`dict\[str, str | None]\`   | 传 `{"replay-user-messages": None}` 可在流里拿到 `UserMessage.uuid` |
| `QoderSDKClient.rewind_files(user_message_id, dry_run=False)` | async method     | 预览或执行文件回滚 |                                                              |

<div id="最佳实践" />

## 最佳实践

* **保存 user message UUID**：把 `UserMessage.uuid` 和你的 UI 消息记录绑定起来，避免靠文本内容反查。
* **先 dry run 再执行**：先展示影响文件和统计，再让用户确认回滚。
* **回滚后刷新 UI**：根据 `filesChanged` 重新加载相关文件状态。
* **失败时展示 `error`**：dry run 返回的 `error` 通常可以直接作为用户可见诊断。
