# pi-collision-guard 中文快速上手

`pi-collision-guard` 通过本机 lease（租约）阻止**参与该扩展的 PI
进程**同时用 built-in `edit` / `write` 操作同一个 canonical path。它是窄范围的协作保护，
不是“防止所有文件冲突”。

## 安装与一次性试用

要求 Node.js 22+ 和支持 package 的 PI。

```bash
# 当前 OS 用户安装
pi install npm:pi-collision-guard

# 只在本次 PI 运行中加载，不写入安装配置
pi -e npm:pi-collision-guard
```

项目级安装可用：

```bash
pi install npm:pi-collision-guard -l
```

## 它实际保护什么

只有同时满足以下条件才参与互斥：

- 同一台主机；
- 同一个 OS 用户；
- PI 进程都已加载本扩展；
- 操作是 PI built-in `edit` 或 `write`；
- 工具执行前看到的是同一个 canonical path。

现有文件和现有父目录会经过 `realpath`，因此常见相对路径和 symlink
别名会收敛。不同路径互不影响。

以下内容**不在保护范围内**：

- agent bash、user bash；
- custom tools、built-in tool override；
- Git；
- IDE、外部编辑器、formatter、build tool；
- 未安装或未加载扩展的 PI；
- 不同 OS 用户、不同主机；
- NFS/网络文件系统；
- hardlink 别名；
- preflight 之后的 TOCTOU 变化；
- 后续 extension handler 再次改写 `event.input.path`。

## 生命周期与默认值

某个路径的 claim 从该 agent run 第一次 `edit` / `write` 开始，持续到
`agent_settled`，随后当前 runtime 持有的全部 claim 会释放。
`session_shutdown` 会停止 heartbeat，并做一次幂等兜底释放。

- heartbeat：每 **5 秒**
- stale threshold：**30 秒**
- corrupt grace：**30 秒**

PID 已确认存活时，旧 heartbeat 不会让 claim 自动过期；PID 已确认死亡时，
下一次 acquire 可立即接管；进程状态未知时，heartbeat 超过 30 秒后才可接管。
损坏的 claim 在文件修改时间后的 30 秒内 fail-closed，之后由下一次 acquire
原子替换。系统没有后台清理线程。

## 冲突示例

```text
PI A edit  src/main/java/example/App.java  -> 获取 claim，允许执行
PI B write src/main/java/example/App.java  -> 检测到 PI A，阻止执行
PI A agent_settled                         -> 释放 claim
PI B 重试 write                            -> 获取 claim，允许执行
```

## 本地验证（无需模型）

开发与 CI 可以完全在本地验证，不需要让 PI 接入大模型：

```bash
npm install --ignore-scripts
npm run test:local
```

该命令会先构建并运行自动化测试，再启动多个真实 Node 进程，共用临时 claim
目录并依次验证：

1. 进程 A 获取目标路径；
2. A 存活时，进程 B 获取同一路径会收到 conflict；
3. A 释放 claim；
4. 进程 C 随后可以获取同一路径。

只有最终验证“PI 收到模型响应并实际触发 built-in `edit` / `write`”时才需要配置
模型。扩展本身的互斥逻辑、文件存储和 PI event adapter 均可纯本地测试。

## 真实 PI + 模型验收

已配置 PI provider 后，执行：

```bash
PI_E2E_MODEL=provider/model npm run test:pi-e2e
```

例如当前模型为 `omlx/Qwen3.5-9B-MLX-4bit` 时：

```bash
omlx start
PI_E2E_MODEL=omlx/Qwen3.5-9B-MLX-4bit npm run test:pi-e2e
```

该场景会启动三个真实 PI 进程并解析 JSON event stream：

1. PI A 用 built-in `edit` 修改文件，然后停在受控 bash 等待中并持续持有 claim；
2. PI B 用 built-in `edit` 修改同一文件，必须收到 `Collision guard blocked`；
3. 测试允许 PI A 继续执行，等待其 `agent_settled` 释放 claim；
4. PI C 再次修改同一文件，必须成功。

测试仅允许指定的 built-in tools，使用临时目标文件，完成后会清理目标 claim 和
临时目录。该测试会真实调用所选模型，产生相应的本地算力或 API token 消耗。

## 命令

```text
/collision status
/collision force-release <path>
```

`status` 会列出 `[self]` / `[other]`、路径、短 session ID、占用时长和
active/stale 状态；损坏记录会显示 claim ID 和 grace 状态。直接输入
`/collision` 等同于 `status`。

`force-release` 会直接删除单路径 claim，可能让两个仍在运行的 PI 同时写文件。
因此仅当 PI 提供 UI 且用户在确认框中明确同意时才允许执行；非 UI 模式会拒绝，
agent 也不能把它当作 tool 调用。

## 故障与恢复

路径规范化、状态存储、operation lock 或 acquire 失败时，built-in
`edit` / `write` 会 fail-closed：阻止工具，而不是冒险放行。新近损坏的 claim
也会阻止接管。

恢复步骤：

1. 执行 `/collision status`；
2. 等原 agent 到达 `agent_settled`，或正常退出原 PI；
3. 重试操作；死亡 PID 可立即接管，未知进程需等待 heartbeat 超过 30 秒；
4. 损坏 claim 需等待 30 秒 corrupt grace 后重试；
5. 只有确认没有活动 writer 时，才执行
   `/collision force-release <path>` 并在 UI 中确认。

## 状态、卸载与隐私

默认状态目录：

```text
~/.pi/agent/collision-guard/
├── claims/
└── locks/
```

新建目录/lock 目录权限为 `0700`，新建 claim 和 owner-token 文件为 `0600`。
claim 文件名是 canonical path 的 SHA-256；JSON 内容仍包含原始 canonical path、
session ID、随机 owner token、PID 和时间戳。已有目录不会被重新 `chmod`。

先让所有 agent settle 并关闭仍加载扩展的 PI，再卸载：

```bash
pi remove npm:pi-collision-guard
rm -rf ~/.pi/agent/collision-guard
```

`pi remove` 不会自动删除状态目录。不要在仍有参与进程运行时删除该目录。

扩展自身不调用模型、不消耗 token、不访问网络、不发送 telemetry。它只进行
本地 path/进程探测和文件系统 I/O。PI extension 以当前用户权限运行，安装前应先
审查源码。

架构说明见 [architecture-zh.md](architecture-zh.md)。
