AIGC标识 Thinkrail 实测:worktree 隔离了改动,没隔离 stash

JetBrains 把 Thinkrail 开源了(Apache-2.0,写这篇时 1914 星):它是套在 pi 编码 agent 外面的一个轻量 IDE,主打「一个任务切一个 git worktree,多个 agent 并行干活,互不碰你的主目录」。Hacker News 上那帖我看的时候是 190 分、141 条评论,很大一部分在吵 README 是不是 AI 写的,我翻到的评论里几乎没人贴自己跑出来的结果。

我在 Windows 10 上下了 10 月 25 日的 0.3.0-nightly.6,接上 DeepSeek,开了两个任务同时改同一个文件,再开一个项目目录模式的对话,中间重启一次,最后翻 agent 的会话记录。这篇讲三件事:隔离到底隔住了什么、哪几处没隔住(有一处我用纯 git 复现了),以及面板上的 token 和费用能不能直接当账单看。

先说结论

想知道的 实测结果
两个任务同时改同一文件 ✅ 各自建 worktree + 分支,分别提交,main 没动,合并无冲突
每个对话都自动隔离吗 ❌ 不是。「Work in project folder」模式直接改主目录,界面写明了 no isolation
spec 存在哪 任务 spec 在 worktree 的 .thinkrail/context/,被 gitignore;项目 spec 是仓库里的 SPEC.md,要你自己提交
重启后还在吗 工作区、对话、token 读数都在;但项目目录里没提交的 SPEC.md,新 worktree 看不到
会互相踩吗 文件不会;git stash 会——所有 worktree 共用一个 stash 栈,agent 自己就用了裸 git stash
token / 费用面板 和 pi 会话记录逐项一致;但单价是写死的美元价,DeepSeek 空闲时段半价它不知道

三步上手建议:

  1. 想要隔离就走「Start building」,别在「Work in project folder」里干并行活;
  2. 想让后续任务读到的 spec,先 commit 到 main;
  3. 在项目规则里写一句「禁止裸 git stash」,要暂存就做临时 WIP 提交。

Thinkrail 的 worktree 隔离:隔离了什么,没隔离什么

背景(一句话版)

按 README 的说法,Thinkrail 是「a thin host that runs pi in-process」:pi 管模型、技能、上下文压缩和费用,Thinkrail 管工作区、编辑器和前后端通信;每个任务可以「cut workspaces from it as git worktrees, each with its own branch and working directory」,也可以「explicitly work in the project folder itself」。发布形式有两种:桌面安装包和一个自带完整引擎的 thinkrail 命令行,两者内嵌的是同一个引擎。

实测

1. 下载、校验、启动

Releases 页面上 nightly.6 一共 19 个文件,Windows 相关的是桌面包 thinkrail-desktop-windows-x64.zip(55.9 MB)和命令行 thinkrail-windows-x64.exe(231 MB)。先对哈希、看签名:

$ sha256sum --ignore-missing -c SHA256SUMS
thinkrail-desktop-windows-x64.zip: OK
thinkrail-windows-x64.exe: OK

PS> Get-AuthenticodeSignature .\thinkrail-windows-x64.exe
Status : Valid
Signer : CN=JetBrains s.r.o., O=JetBrains s.r.o., L=Prague, C=CZ, ...

桌面 zip 解开只有一个 1.3 MB 的 ThinkRail-Setup-canary.exe,加上 .installer\ 目录里 55 MB 的 .tar.zst 载荷。README 特意说要「run its setup executable in place, next to its payload」,所以别把 setup.exe 单独拖出来运行。另外这个 zip 的路径分隔符是反斜杠,Git Bash 的 unzip 会报一句 appears to use backslashes as path separators,好在能正常解开。

为了能脚本化地观察,这次我用的是命令行版(它会起一个本地服务,用浏览器打开界面)。启动时把 HOME 指到一个空目录,这样它写的所有文件都在眼皮底下:

$ thinkrail-windows-x64.exe --version
0.3.0-nightly.6

$ thinkrail-windows-x64.exe --no-open --no-analytics --port 24242 ./repo
[2026-10-26T03:30:33.388Z] INFO: [log] logging initialized (thinkrail 0.3.0-nightly.6, pid 28640, level info)
[2026-10-26T03:30:33.456Z] INFO: [host] listening on port 24242
thinkrail → http://localhost:24242

从初始化日志到开始监听只用了 68 ms。启动时踩到两个小坑:

$ netstat -ano | grep 24242
  TCP    [::1]:24242            [::]:0                 LISTENING       28640

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:24242/
502
$ curl -s --noproxy '*' -o /dev/null -w "%{http_code}\n" http://localhost:24242/
200
$ curl -s --noproxy '*' -o /dev/null -w "%{http_code}\n" http://127.0.0.1:24242/
000
  • 默认只监听 IPv6 的 [::1],用 127.0.0.1 访问连不上;
  • 本机设了 HTTP_PROXY(开着代理软件)的话,curl 或脚本访问 localhost 会被代理转走,返回 502,要加 --noproxy。

还有一点值得知道:我加了 --no-analytics,首次启动弹出的「Share additional usage data」开关仍然是默认打开的,~/.thinkrail/config.json 里写的是 "analyticsEnabled": true,手动关掉之后才变成 false。README 也说了,基础使用事件和错误报告是「always sent」的,这个开关只管附加数据。

模型这块我什么都没配:它直接读到了环境变量里的 DEEPSEEK_API_KEY,默认选中「DeepSeek V4.1 Flash」,实际请求的模型名是 deepseek-flash。

2. 两个任务同时改同一个文件

测试仓库只有一个 calc.js,里面放了两个互不相关的函数 add 和 formatPrice。两个任务前后差 17 秒提交:

  • 任务 A(11:32:31):给 add 加参数校验,非数字就抛 TypeError,只改 add;
  • 任务 B(11:32:48):把 formatPrice 改成 ¥3.00 这种格式,只改 formatPrice。

刚提交 A,worktree 就有了;B 一提交,又多出一个:

$ git worktree list
…\src\repo                                     257f6d5 [main]
…\src\home\.thinkrail\worktrees\repo\workspace-1  257f6d5 [add-typeparam-validation]
…\src\home\.thinkrail\worktrees\repo\workspace-2  257f6d5 [format-price-rmb]

注意 worktree 不在仓库旁边,而是统一放在 ~/.thinkrail/worktrees/<项目>/workspace-N。分支名是 agent 读完任务后自己起的(它调用了一个 set_title 工具,顺手把工作区也重命名了)。

两个任务都在 30 秒左右提交完:

$ git log --all --oneline --graph --format='%h %ad %d %s' --date=format:%T
* 8ed0628 11:33:13  (format-price-rmb) Format prices with a ¥ prefix and two decimals
| * c13270a 11:33:03  (add-typeparam-validation) Throw TypeError in add when arguments are not numbers
|/
* 257f6d5 11:30:22  (HEAD -> main) init

main 上的 calc.js 一个字没变。我另外 clone 了一份,把两个分支先后合并进来,没有冲突:

$ git merge --no-edit origin/add-typeparam-validation
$ git merge --no-edit origin/format-price-rmb
 calc.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

到这一步,「文件层面互不相踩」这个说法成立。

3. agent 在 worktree 里实际做了什么

pi 把每个对话存成一份 jsonl,在 ~/.pi/agent/sessions/<worktree 路径>/ 下面。我写了个小脚本把工具调用一条条列出来,有三个发现。

第一,每个任务开工前都会先读两份内置技能(choosing-a-workflow、brainstorming 的 SKILL.md),再写一份任务 spec,最后才改代码。改一行代码的任务也是这个流程,所以 token 主要花在了读上下文,后面讲费用时会看到。

第二,它发现了一个跟 worktree 位置有关的问题。 任务 A 跑 node test.js 时报错了:

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and
'F:\desk_code\articles\package.json' contains "type": "module".

原因是我把测试用的 HOME 放在了一个上级目录有 package.json("type": "module")的地方,Node 往上找配置时找到了它。agent 自己看出了原因,把文件复制到临时目录里重新验证,然后在 spec 里写明「改动前同样报错,与本变更无关」。这次主目录和 worktree 都在这个目录下面,所以两边结果一样。但这暴露了一个更普遍的问题:worktree 的上级目录是 ~/.thinkrail/...,不是你仓库原来的上级目录。凡是「从当前目录往上找」的配置(package.json 的 type、.npmrc、.editorconfig、某些 lint/格式化配置),在 worktree 里都可能跟主目录不一样。

第三,agent 用了裸 git stash。 任务 A 为了确认「改动前也报错」,跑了这样一条命令:

git stash -q && node test.js; echo "exit=$?"; git stash pop -q && git status --porcelain

这次没出事,因为任务 B 那段时间没碰 stash。但 stash 是存在 .git 里、所有 worktree 共用的,隔离对它不起作用。我用纯 git 把「运气不好」的情况复现了一遍:

# wt1、wt2 是同一仓库的两个 worktree
cd wt1 && echo '// A 的改动' >> calc.js && git stash -q   # A:stash 一下,想跑原版
cd wt2 && echo '// B 的改动' >> calc.js && git stash -q   # B:恰好这时也 stash
cd wt1 && git stash pop -q                                 # A:pop 回来

== wt1 的 calc.js 末行:
// B 的改动
== stash list:
stash@{0}: WIP on t1: 257f6d5 init

A pop 回来的是 B 的改动,A 自己的改动还压在栈里。两个 agent 并行跑的时候,这种事只要时间点碰上就会发生,而且很难看出来。

4. spec 存在哪,重启后还能不能读到

任务 A 写的任务 spec 长这样(节选):

.thinkrail/context/.gitignore                    内容只有一个 *
.thinkrail/context/TASK-add-arg-validation.md    2518 字节,带 id / type: task-spec / status: done

$ git status --short --ignored
!! .thinkrail/

内容写得挺扎实:需求、设计取舍(「用 typeof 而非 Number.isFinite」)、明确不做的事、验证结果和 commit 号都有。但它被 * 整体 gitignore 了,只活在这个 worktree 里,删掉工作区就没了,也不会跟着分支走。

项目级 spec 是另一回事。我切到「Work in project folder」开了一个对话,让它「用 spec 工具建立项目级 SPEC.md」。它在主目录写了一份 SPEC.md,右侧 Specs 面板也显示出了「calc 计算模块」:

$ git -C repo status --short
?? SPEC.md

两个现象:

  • 这个对话直接改的是主目录,没有建 worktree,跟界面上写的「no isolation」一致;
  • 这份 spec 是照 main 上的旧代码写的:「formatPrice 不输出货币符号」「add 不做类型校验」。可此时另外两个分支已经把这两条都改掉了。spec 只能反映它所在那份 checkout 的状态。

然后我按 PID 结束进程、重新启动。三个工作区、各自的对话、token 读数都还在。接着从 main 新开了一个只读任务,问它「这个项目目前有哪些 spec,formatPrice 的契约是什么」:

TOOL spec_grep {"pattern":"formatPrice"}
  -> No matches.
TOOL spec_validate {}
  -> Spec-graph is valid: no issues found.

## 结论
**这个项目目前没有任何 spec 文件。**

原因很简单:新 worktree 是从 main 的提交切出来的,主目录里没提交的 SPEC.md 不会带过去。「spec 是事实依据」这句话的前提,是 spec 已经提交了。

5. token 和费用面板准不准

面板读数和 pi 会话 jsonl 里 usage 的累加值逐项对得上,重启后也没变:

对话 面板(↑输入 · ↓输出 · R 缓存读 · 费用) 会话记录累加
任务 A ↑18k · ↓5.5k · R236k · $0.014 18473 / 5502 / 235520 / $0.01356
任务 B ↑14k · ↓3.7k · R153k · $0.009 13805 / 3672 / 153344 / $0.00947

问题出在单价上。我在程序里找到了 pi 内置的 deepseek-flash 价格表:

"deepseek-flash", name: "DeepSeek V4.1 Flash", ... cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0

单位是美元/百万 token。DeepSeek 官方价格页上的 Flash 用人民币计价,还分时段:

每百万 token 高峰(工作日 9–12、14–18 点) 空闲时段
输入(缓存命中) 0.04 元 0.02 元
输入(缓存未命中) 2 元 1 元
输出 8 元 4 元

按 1 美元约 7 元粗算,pi 的写死价大致等于高峰价。我这次是周一 11 点多跑的,正好在高峰,所以面板偏差不大。按官方高峰价算,任务 A 约 0.09 元。但如果在晚上或周末跑,实际扣费只有一半,面板会高估一倍左右。另外有两点要注意:

  • 缓存读 token 是输入的十几倍(任务 A:236k 对 18k),只看「↑」会低估上下文规模;
  • 我本想用 DeepSeek 的余额接口对一下账,但这个 key 同时在别处用着:11:34:35 到 11:35:21 之间 Thinkrail 没有任务在跑,余额却从 11.34 元变成了 11.24 元,扣费没法单独归到这次测试。所以「面板和账单一致」这个结论我没能验证。要对账,最好给它单独开一个 key。

深入:哪些情况会互相踩

把上面几项合起来,「一任务一 worktree」能隔住的是工作目录里的文件,隔不住的是所有 worktree 共用的 .git,以及路径变了带来的环境差异:

  • stash:共用一个栈,agent 自己就会用;
  • hooks、config:.git/hooks 和仓库 config 是同一份,一个任务改了 hook,所有任务都受影响;
  • 未提交文件:主目录里的草稿、spec、.env 都不会进新 worktree,而 .env 这类通常本来就不提交,新 worktree 里跑测试可能缺配置;
  • 上级目录配置:worktree 在 ~/.thinkrail/worktrees/ 下面,往上找配置的工具看到的是另一套上级目录。

也有人觉得这类新 IDE 没必要。HN 上一条评论说它们「mostly just worktrees+sandbox+tabs+prompts, which is just git+docker+tmux」。从实测看,Thinkrail 确实没做 sandbox,它的隔离就是 git worktree 本身,git worktree 有的边界它都有。

适用边界

  • 值得用:同一仓库同时跑 2–4 个改动面不重叠的小任务,想要「每个任务一个分支、一键开 PR」,而且习惯先 spec 后编码的人;
  • 先别用:依赖上级目录配置的仓库(monorepo 里的子目录、放在带 package.json 的大目录里的项目);需要强隔离(进程、网络、数据库)的任务,worktree 管不了这些;
  • 版本提示:这次测的是 nightly,10 月 19 日到 25 日一共发了 6 个 nightly,表现可能很快就会变;macOS 只有 Apple Silicon 版。

清单

这次的整个流程,是在 SpectrAI 里让一个 Agent 通过内置浏览器操作 Thinkrail 界面、在终端里逐条核对 git 状态完成的,命令和输出都留在会话记录里。

参考

posted @ 2026-10-26 12:39  SpectrAI光谱AI  阅读(4)  评论(0)    收藏  举报