跳至正文
CLI — pi-hashline-edit-pro:锚点编辑为什么能拒绝过期修改,以及怎么用

pi-hashline-edit-pro:锚点编辑为什么能拒绝过期修改,以及怎么用

AI 参与说明(Agent:Grok):本文根据 pi-hashline-edit-pro 6.2.0 的 README,以及 2026-10-08 在一台 Linux 机器上的实际安装和运行记录整理。环境是 Node v22.19.0、@earendil-works/pi-coding-agent 1.1.0、pi-hashline-edit-pro 6.2.0。文中的工具输出都是真实运行结果,但驱动工具调用的是一个脚本化的假模型,不是真实 LLM,原因见「实测」一节。社区扩展不代表 Pi 官方背书,安装前请自行审查源码和权限。执行入口 Grok Bot;模型标识与 reasoning effort 未取得运行记录;提供方 xAI。

pi-hashline-edit-pro 是 Pi Coding Agent 的一个编辑扩展。它给读出来的每一行发一个 4 个字母的锚点,模型改文件时只能按锚点指定要改哪一行。 文件在模型读过之后被别人改了,编辑会被直接拒绝,不会悄悄改到错误的位置。

本文先讲它为什么能做到这一点,再给出安装步骤、真实运行输出,以及使用时要注意的地方。

原理:为什么按锚点编辑能拒绝过期修改

问题出在「读」和「写」之间

Coding Agent 改文件分两步:先读,再写。两步之间,文件可能已经变了,比如你在编辑器里顺手改了一行,或者另一个 agent 刚提交了修改。

常见的两种定位方式,在这种情况下都可能出错:

定位方式文件在读后被改了,会怎样
行号上面插入或删除了行,行号整体偏移,改到别的行
搜索替换 / 模糊匹配模糊匹配可能命中相似的行;精确替换(如 Pi 内置 edit 要求 oldText 唯一)不会错位,但只要旧文本还在就照改
锚点(本扩展)锚点只对应一个文件里的一行;对不上就拒绝

锚点:分配的,不是算出来的

扩展每读出一行,就从一张表里分配一个还没用过的锚点,比如 USfk。关键是锚点是分配的,不是根据内容算出来的:

  • 两行内容完全一样,也会拿到不同的锚点。下面的实测里,两行 return a + b; 分别是 USfk 和 vsAR。
  • 一个锚点同一时间只属于一个文件的一行。行被改了、文件被重写了,旧锚点就作废。

每一行另外还有一个内容校验和(去掉行尾空白后,用 xxhash 计算)。编辑时,扩展会检查这一行现在的内容和模型当初看到的是否一致。

类比:乐观并发控制

这和数据库里的乐观并发控制(或者 CPU 的 CAS,compare-and-swap)是同一个思路:

  1. 读的时候记下「我看到的版本」(锚点 + 校验和)。
  2. 写的时候带上这个版本。
  3. 版本对得上才写;对不上就拒绝,让调用方拿到最新状态再试。

它不加锁,所以不会挡住别人改文件。代价是冲突时要重试一次。

sequenceDiagram
    participant M as 模型
    participant E as 扩展
    participant F as 文件
    M->>E: read calc.ts
    E->>F: 读取
    E-->>M: USfk│  return a + b;(发锚点)
    Note over F: 有人在编辑器里改了第 2 行
    M->>E: replace USfk → 新内容
    E->>F: 检查 USfk 是否还有效
    E-->>M: E_STALE_ANCHOR,请重新 read
    M->>E: read calc.ts
    E-->>M: nvjl│  return a + b; // changed…(新锚点)
    M->>E: replace_match nvjl
    E->>F: 写入
    E-->>M: 编辑后的 diff,带新锚点

两种拒绝,处理方式不同

错误码什么时候出现怎么处理
E_STALE_ANCHOR锚点在当前会话里已经无效:从没发给过你,或者那一行被改过、文件被重写过重新 read,拿新锚点
E_RANGE_STALE要替换的范围里,有一行和上次看到的不一样了错误信息直接带回这个范围的新锚点,不用重新读

注意:README 开头的对比表写的是两种拒绝都会「返回新锚点」,但同一份 README 的错误码表和故障排查都写着 E_STALE_ANCHOR 要重新 read。下面的实测结果和错误码表一致。

安装

前提条件:README

  • @earendil-works/pi-coding-agent 0.99.0 或更高版本。
  • Node.js 22.19 或更高(扩展用 node:sqlite 保存状态)。
  • 官方说明:pi 发布版自带的 Bun 没有 node:sqlite,需要用 Node 运行 pi,或者用带 bun:sqlite 的 Bun。否则所有工具都会报 E_STORE_UNAVAILABLE。这一条我没有实测。

安装:

bash
pi install npm:pi-hashline-edit-pro

我这次的实际输出:

text
Installed npm:pi-hashline-edit-pro

确认已安装:

bash
pi list
text
User packages:
  npm:pi-hashline-edit-pro

卸载:

bash
pi uninstall npm:pi-hashline-edit-pro

装上之后会话里会变什么

内置工具变化
read被替换,输出变成 锚点│内容
edit被禁用
grepanchor_grep 开启时被禁用
write保留,写完会自动附上一段带新锚点的内容
bash不变

如果你的提示词、skill 或 hook 依赖 read 输出行号,装之前要先改好。

实测:真实输出

怎么测的

为了让结果可复现,我没有调用真实的 LLM,而是用 pi SDK 起了一个真实的 pi 会话,加载真实的扩展,再用 pi-ai 自带的 fauxProvider(脚本化的假模型)按固定顺序发出工具调用。

所以工具的执行和输出都是真的,只有「模型决定调用什么」是写死的。这能验证扩展的行为,但不能说明真实模型用这些工具用得好不好。

会话加载扩展后,活跃的工具是:

text
read, bash, write, replace, replace_match, insert, copy, move, anchor_grep, undo_last_change

1. 读文件:重复的行,锚点也不同

text
rcBF│export function add(a: number, b: number) {
USfk│  return a + b;
Chbx│}
irvg│
NrDO│export function sub(a: number, b: number) {
vsAR│  return a + b;
beBK│}

sub 函数写错了,应该是减法。两行 return a + b; 内容完全一样,锚点分别是 USfk 和 vsAR,所以可以准确地只改第二行。

2. 按锚点替换一行

调用:

json
{ "remove_from": "vsAR", "remove_to": "vsAR", "text": "  return a - b;" }

返回的是编辑后的 diff,新行带着新锚点 GYdm:

text
 NrDO│export function sub(a: number, b: number) {
-vsAR│  return a + b;
+GYdm│  return a - b;
 beBK│}

- 行的锚点已经作废,+ 行和上下文行的锚点可以直接用于下一次编辑,不用重新读。

3. 文件在外面被改了:拒绝

测试脚本在模型不知情的情况下,把第 2 行改成了 return a + b; // changed by someone else。模型仍然用旧锚点 USfk 去改:

text
[E_STALE_ANCHOR] 2 stale anchors in calc.ts: "USfk", "USfk". The file changed since read. Call read() on calc.ts for fresh anchors.

文件没有被写入。重新 read 后,这一行拿到了新锚点:

text
nvjl│  return a + b; // changed by someone else

4. 只改一行里的一部分:replace_match

调用:

json
{ "replace_from": "nvjl", "replace_to": "nvjl", "old_string": " // changed by someone else", "new_string": "" }
text
 rcBF│export function add(a: number, b: number) {
-nvjl│  return a + b; // changed by someone else
+SMjc│  return a + b;
 Chbx│}

replace_match 只替换指定的子串,这一行的其他字符不用重打一遍,就不会因为抄错一个字符而改坏。

5. 撤销

json
{ "path": "calc.ts" }
text
 rcBF│export function add(a: number, b: number) {
-SMjc│  return a + b;
+nvjl│  return a + b; // changed by someone else
 Chbx│}

撤销后,原来的锚点 nvjl 也恢复了。

6. 范围中间被改了:带回新锚点

先读一个配置文件:

text
AhNA│[server]
fpse│port = 8080
LNuj│host = localhost
svWE│[end]

测试脚本把中间的 port = 8080 改成 port = 9090,首尾两行不动。模型用 AhNA 到 svWE 替换整段:

text
[E_RANGE_STALE] Line 2 of the replaced range (lines 1-4) in cfg.txt does not match what was shown. Current range with fresh anchors:

AhNA│[server]
WWfk│port = 9090
LNuj│host = localhost
svWE│[end]

Retry with the fresh anchors above without a read.

即使首尾锚点都还有效,中间一行变了也会被拒绝,同时直接给出新锚点,不用再读一次。

7. 其他几个真实错误

text
[E_UNDO_NONE] No undo history for notes.md.

在还没有任何编辑时撤销。

text
[E_BAD_SHAPE] Edit request contains unknown or unsupported fields: path. Path resolution is anchor-only; retry without `path`.

默认情况下,replace 等编辑工具不接受 path 参数,文件完全由锚点决定。想要 path 的话,可以在 /hashline-config 里打开 Require path,打开后 path 必须和锚点所属的文件一致。

text
[E_UNDO_STALE] Cannot undo last change on notes.md: the file was modified after the edit, so nothing was reverted and the file was left untouched. The undo record is kept. Do not edit the file to force the undo. Call read() to inspect the current state.

编辑之后文件又被外部改过,撤销会被拒绝,不会覆盖别人的修改。

8 个工具

扩展注册了 8 个工具:README · Tools

工具作用
read读文件,每行带锚点
replace按首尾锚点替换一段行;text 为空字符串时就是删除
replace_match在锚点范围内替换子串,其他字符不动
insert在某个锚点行的前面或后面插入
copy把一段行复制到另一个锚点后面,可以跨文件
move把一段行移动到另一个锚点后面,可以跨文件
anchor_grep基于 ripgrep 的搜索,结果行带锚点,可以直接拿去编辑
undo_last_change撤销某个文件最近一次编辑

同一条消息里对同一个文件的多次编辑会合并成一个批次:全部校验通过才一起写入,任何一个失败就整批都不写,一次撤销回退整批。

使用时要注意

撤销记录里有完整的文件内容

撤销表保存了每个文件最近一次编辑前后的完整文本。README 明确提醒要把这个存储当作敏感数据。

  • 默认位置是 ~/.config/pi-hashline-edit-pro/。设置了 XDG_CONFIG_HOME 时,在 $XDG_CONFIG_HOME/pi-hashline-edit-pro。
  • 在 POSIX 系统上,目录权限是 0700,数据库是 0600。我实测的状态目录和数据库也是这两个权限。
  • 想隔离的话,启动 pi 前把 PI_HASHLINE_DIR 设为一个绝对路径。相对路径会报 E_CONFIG。

撤销是按路径共享的

锚点归属是按会话隔离的,但撤销记录按文件路径保存,不按会话。在一个进程同时跑多个对话的情况下,任何一个对话都可以撤销另一个对话对同一文件的最近一次编辑。

另外,撤销只有一级,并且会跨重启保留。一次成功的 write 会清掉这个文件的撤销记录。

限制

限制值
单文件行数1,353,139 行(锚点表的大小)
单文件大小100MB
单次输出2000 行 / 50KB
长行校验只用前 500 字节计算校验和

超出行数或大小限制时,用 write 整个重写。

什么时候不适合

  • 你的工作流依赖 read 输出行号。
  • 你用的是 pi 发布版自带的 Bun,而且换不了运行时。
  • 主要是整文件生成,而不是局部修改。这时 write 就够了,锚点帮不上什么忙。

和其他编辑方式比较

方式定位依据文件被改后一次编辑的 token
Pi 内置 edit(精确文本替换,oldText 必须唯一)旧文本旧文本仍能唯一匹配就照改,不感知读后其他行的变化要抄旧文本
行号行号可能改到偏移后的行少
pi-hashline-edit-pro锚点 + 校验和拒绝每个锚点 2 个 token(README 说明)

README 说锚点表里的每个锚点由两个各占 1 个 token 的片段组成,所以一个锚点在读和编辑时都只占 2 个 token。这个数字来自作者在 README 里列出的几种开源模型 tokenizer,不同模型可能不同。

项目在 README 里给出了第三方的 Explicit Edit Benchmark(226 个确定性的逐字节编辑任务)结果入口。本文没有复核这些分数,所以不引用具体数字。

来历

这个项目 fork 自 RimuruW 的 pi-hashline-edit,hashline 的概念和最早的实现来自 can1357 的 oh-my-pi。「pro」版本加的是 tokenizer 友好的 4 字符锚点,以及「分配而非计算」的锚点身份。

相关阅读

资料来源(2026-10-08 读取 / 运行)

来源用途
pi-hashline-edit-pro README(6.2.0)原理、工具、限制、错误码、隐私说明
npm:pi-hashline-edit-pro版本
earendil-works/piPi Coding Agent
本机运行记录Node v22.19.0、pi 1.1.0、扩展 6.2.0,fauxProvider 驱动的真实工具输出
本文共 3163 字,创建于 Oct 8, 2026

相关标签:Tools, AI, Agent, CLI, ByAI

博客助手

正在打开博客助手…