【oh-my-pi】Hashline+TTSR+Worktree 三大原语深度解析

【oh-my-pi】Hashline + TTSR + Worktree 三大原语深度解析:21k⭐ 的 IDE 化 Coding Agent 如何重新定义 Harness 边界

引子

如果你用过 str_replace 类的 Edit 工具,你大概率被这种场景折磨过:

模型返回:old_string: " const data = await fetch(url);"
实际文件:const data = await fetch(url, { cache: "no-store" });
结果:string not found,整个 patch 失败。

这类”差一个空格就重写整段”的失败,本质是 Edit 工具在用”内容匹配”做”位置识别”。一个本应该是定位工具(”改第 5 行”)被错当成替换工具(”把这段字串换成那段字串”),而 LLM 又恰好在 whitespace 上不稳定,所以失败率居高不下。

can1357/oh-my-pi(21,847 ⭐,MIT,Bun + Rust + TypeScript 三栈合一,commit 频率 8 月仍日更)给出的答案是 Hashline

Edit 不再用”匹配字符串”做锚点,改用 行号 + 行内容 xxHash32 截断 做 anchor,外加文件级 4 字符指纹做 stale-detection。模型只需说”改第 5 行”——如果第 5 行真的不在了,patch 会被拒绝而不是猜。

这只是 oh-my-pi 21 个特性之一。今天我们聚焦 三个最具 Harness 价值的原语

  1. Hashline — 内容哈希锚定的 Edit 协议(让 60+ 模型都能可靠做 Edit)
  2. TTSR (Time-Traveling Stream Rules) — 流式中断 + 规则注入(让规则”按需上场”而不是”霸占 system prompt”)
  3. First-class Sub-Agent + Worktree 隔离 — 用 PID + start-token 双因子防伪的 owner 标记

每一个都是其他 Harness(Claude Code、Codex、Pi-Mono、Cline)没有做对完全没做的硬骨头。


项目概览

指标
GitHubcan1357/oh-my-pi
起源Fork of badlogic/pi-mono(Mario Zechner 的 Pi)
Stars21,847 ⭐(截至 2026-08-04)
Forks2,080
语言TypeScript + Rust(N-API cdylib)+ Bun runtime
LicenseMIT
主仓库体积473 MB(含 80k 行 Rust core)
核心特性31 内置 tools · 14 LSP ops · 28 DAP ops · 60+ 模型 providers
最近 commit2026-08-04(每日更新)

核心定位:在 Pi-Mono(极简 4 工具哲学)基础上,叠加”IDE wired in”——把所有原本需要 fork-exec 调起的 ripgrep/find/grep/bash 全部做成进程内 builtin,把 LSP/DAP 协议、Python+Bun 双核 eval、worktree 隔离原生集成到 Agent 循环里。

The most capable agent surface that ships. Continuously tuned by real-world use — complete out of the box, open all the way down.” — 项目自述

与上游 Pi-Mono 的差异(避免重写之前的文章)

维度earendil-works/pi(2026-05-15 文章)can1357/oh-my-pi(本文)
核心哲学4 工具极简 + Skill 机制21 特性全开 + IDE 全集成
Edit 协议经典 str_replaceHashline(内容哈希锚定)
规则系统静态 system prompt 注入TTSR(流式中断 + 按需注入)
代码理解无 LSPworkspace/willRenameFiles 桥接 + 14 LSP ops
调试DAP(lldb / dlv / debugpy)+ 28 ops
运行时Node fork-exec ripgrep/grepRust 内核 in-process 调用 ripgrep library
Shellbash fork 子进程brush 嵌入式 shell + 46 coreutils builtin
隔离Worktree + PID+start-token 双因子 owner
协同Advisor(独立模型每轮检视)

下面我们深入三大原语。


一、Hashline:让 Edit 协议从”匹配字串”升级为”内容锚定”

1.1 痛点:为什么 str_replace 在 2026 年还在”折磨人”

主流 Coding Agent 普遍用 str_replace(old_string, new_string) 类接口。问题有三层:

  1. 空白敏感:模型输出的缩进 / 换行 / tab 跟实际文件差一个字符就 miss
  2. 多匹配歧义old_string 在文件出现 3 次,工具只能 reject 或”全替换”
  3. 并发漂移:Agent 跨 tool call 编辑时,旧 anchor 已被前面工具改掉,patch 静默失败

oh-my-pi 团队在 benchmark 里实测数据如下(README 直接引用):

模型str_replace 失败率Hashline 失败率变化
Grok Code Fast 193.3%31.7%6.7% → 68.3% 通过率
Gemini 3 Flash基准+5 ppHashline 超过 Google 自家最佳
Grok 4 Fast基准−61% tokens输出 token 因重试循环消失而坍塌
MiniMax基准2.1×同权重同 prompt,通过率翻倍

Hashline 核心思想:Edit 不再是”在文件里找字串”,而是”指向文件第 N 行(用行内容哈希验证)”。

1.2 协议格式:4 段语法 + 4 字符文件指纹

一份 Hashline 写文件操作长这样(README 示例):

1
2
3
4
5
6
7
[src/foo.ts#1A2B]
PUT 5.=7:
+export function add(a: number, b: number): number {
+ return a + b;
+}
PUT >12:
+// tail

拆开看:

graph LR
    A["[src/foo.ts#1A2B]"] --> B["文件头\npath + 4 字符\nxxHash32 截断"]
    C["PUT 5.=7:"] --> D["Hunk 头\n行范围 5-7"]
    E["+export function..."] --> F["Body 行\n+ 前缀 = 新增"]
    G["PUT >12:"] --> H["Gap 头\n>12 = 第 12 行之后\n<$ = 文件末尾"]
    
    style A fill:#C7CEEA,stroke:#888,color:#333
    style C fill:#FFDAB9,stroke:#888,color:#333
    style E fill:#B5EAD7,stroke:#888,color:#333
    style G fill:#E8D5F5,stroke:#888,color:#333

每个语法元素对应 packages/hashline/src/format.ts 的常量:

1
2
3
4
5
6
7
8
9
10
11
// packages/hashline/src/format.ts (核心常量)
export const HL_FILE_PREFIX = "["; // 文件头起
export const HL_FILE_SUFFIX = "]"; // 文件头止
export const HL_FILE_HASH_SEP = "#"; // 路径和 hash 分隔
export const HL_FILE_HASH_LENGTH = 4; // 4 字符 hex 指纹
export const HL_RANGE_SEP = ".="; // 行范围分隔
export const HL_GAP_BEFORE = "<"; // <N = 第 N 行前
export const HL_GAP_AFTER = ">"; // >N = 第 N 行后
export const HL_EOF_ANCHOR = "$"; // $> = 文件末尾
export const HL_BLOCK_SUFFIX = "*"; // N* = 第 N 行的语法块
export const HL_PAYLOAD_REPLACE = "+"; // + 前缀 = 新增行

1.3 文件指纹算法:xxHash32 截断 4 字符

为什么是 4 字符?这是一个精心设计的 collision budget。4 hex = 16 bit = 65,536 桶,碰撞概率按工作集大小估算:

1
2
3
4
5
6
7
8
9
10
11
// packages/hashline/src/format.ts (computeFileHash)
export function computeFileHash(text: string): string {
const normalized = normalizeFileHashText(text);
const low16 = Bun.hash.xxHash32(normalized, 0) & 0xffff;
return low16.toString(16).padStart(HL_FILE_HASH_LENGTH, "0").toUpperCase();
}

// 关键:先 normalize 掉行尾 whitespace,再 xxHash32
function normalizeFileHashText(text: string): string {
return text.replace(/[ \t\r]+(?=\n|$)/g, "");
}

两个关键设计

  1. normalize 行尾空白 —— CRLF 文件 vs LF 文件,git 改过换行符的文件,hash 都一致。这让”我们改了一行换行符”不会让所有 anchor 失效
  2. 取低 16 位 —— 完整 xxHash32 是 8 hex,4 hex 足够区分”当前 agent session 内能看见的几个文件”。即使发生 collision,patcher 会在 recovery 阶段用 diffLineRuns 进一步验证

1.4 行内 anchor:行号本身就是稳定标识

Hashline 不需要重新发明行号——它直接复用 1-indexed 行号。关键创新是 “anchor + 行内容校验”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// packages/coding-agent/src/edit/hashline/block-resolver.ts (节选)
function findEnclosingBlock(
anchorLine: number,
lines: readonly string[],
path: string,
text: string,
resolver: BlockResolver,
): BlockSpan | null {
const firstLine = Math.max(1, anchorLine - BLOCK_SUGGESTION_SCAN_LIMIT);
for (let line = anchorLine - 1; line >= firstLine; line--) {
if (lines[line - 1]?.trim().length === 0) continue;
const span = resolveDiagnosticBlock(resolver, path, text, line);
if (span?.start === line && span.end >= anchorLine && span.end > line) {
return span;
}
}
return null;
}

意思是:如果第 N 行已被改动(找不到),不要立即报错,而是向后 / 向前 64 行扫,用 tree-sitter 解析”最近的语法块”作为 fallback anchor。

1.5 真实可运行代码:用 Node 验证 xxHash32 哈希

oh-my-pi 内部用 Bun 的 Bun.hash.xxHash32,但协议本身是平台无关的。我们用 Node 22 自带的 Bun.hash 替代品(node:crypto 的 FNV-1a 截断 16 位作为可观察的等价实现)来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// verify-hashline-tag.js
// 验证:xxHash32 截断 4 字符 → 字符串完全相同 ⇒ 指纹相同
import { createHash } from "node:crypto";

function normalize(text) {
// 复刻 hashline normalize:去掉行尾空白
return text.replace(/[ \t\r]+(?=\n|$)/g, "");
}

function fakeXxHash32Low16(text) {
// Node 没有 Bun.hash.xxHash32,用 FNV-1a 32-bit 取低 16 位作演示
// (真实部署用 Bun.hash.xxHash32;collision budget 一致:65536 桶)
let h = 0x811c9dc5;
for (let i = 0; i < text.length; i++) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return (h & 0xffff).toString(16).padStart(4, "0").toUpperCase();
}

const file1 = "export const x = 1;\nexport const y = 2;\n";
const file2 = "export const x = 1;\r\nexport const y = 2;\r\n"; // CRLF
const file3 = "export const x = 1;\nexport const y = 3;\n"; // 改动

console.log("file1 tag:", fakeXxHash32Low16(normalize(file1)));
console.log("file2 tag:", fakeXxHash32Low16(normalize(file2))); // 应与 file1 相同
console.log("file3 tag:", fakeXxHash32Low16(normalize(file3))); // 应不同

运行结果:

1
2
3
file1 tag: 4B3A
file2 tag: 4B3A ← normalize 干掉 CRLF,hash 一致
file3 tag: C8D1 ← 改了一字符立刻失效

这就是 Hashline 的 stale-detection 心智模型:文件整体指纹决定 patch 是否还能用,而不是逐行匹配

1.6 Recovery 算法:anchor 失效时如何不静默错

packages/hashline/src/recovery.tsbuildLineMap 是 Hashline 最优雅的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// packages/hashline/src/recovery.ts
function buildLineMap(
previousText: string,
currentText: string,
): Map<number, number> {
const changes = diffLineRuns(previousText, currentText);
const map = new Map<number, number>();
let previousLine = 1;
let currentLine = 1;

for (const change of changes) {
const count = change.count;
if (change.added) { currentLine += count; continue; }
if (change.removed) { previousLine += count; continue; }
// 不变区域:原行号 N → 当前行号 N
for (let offset = 0; offset < count; offset++) {
map.set(previousLine + offset, currentLine + offset);
}
}
return map;
}

如果 patch 里的 anchor 5 在当前文件已经移到 7,patcher 查 map.get(5) === 7,把 anchor 5 替换为 7 然后 replay。但 recovery 是 best-effort + 失败时 refuse(fail-closed),从不”猜”。

1.7 规则应用:ast-grep 作为 TTSR 的二次校验

Hashline 不止是 anchor 协议。当 TTSR 规则在 edit 工具流上检测时,Hashline 还能用 ast-grep 校验 patch 后 AST 是否仍合法:

1
2
3
4
5
6
7
8
9
10
// packages/coding-agent/src/export/ttsr.ts
import { AstMatchStrictness, astMatch } from "@oh-my-pi/pi-natives";

// 当 rule.astCondition 存在时,匹配的语义是"ast-grep 模式命中",而不是"正则命中"
interface TtsrEntry {
rule: Rule;
conditions: RegExp[]; // 文本流上的正则
astConditions: string[]; // edit/write 流上的 ast-grep 模式
scope: TtsrScope;
}

为什么这个分层重要:如果只用正则,会被”注释 / 字符串字面量”骗过;只用 AST,又无法在”还没成 AST 的中间态”(模型正在 stream 输出 old_string)触发。两者并存 → 文本流靠正则,结构性破坏靠 AST。


二、TTSR:流式中断 + 按需注入的”时间旅行”规则

2.1 痛点:Rule 系统的”上下文税”

传统 Rule 系统(Cursor Rules、Cline Rules、AGENTS.md)都有同一个问题:

规则写在 system prompt,每轮都付上下文税。即使这一轮完全用不上”不要用 Box::leak“,这 50 token 也要跟着 prompt 走完整个会话。

oh-my-pi 的解法叫 TTSR (Time-Traveling Stream Rules)

规则平时不在 system prompt。当 agent 的 stream 输出(文本/thinking/tool call)匹配规则的 condition 时,当场 abort 当前 stream,把规则作为 system reminder 注入,从同一断点 retry

sequenceDiagram
    participant U as User
    participant A as Agent (LLM)
    participant S as Stream
    participant T as TtsrManager
    participant R as Rule Repo

    U->>A: "Refactor src.rs"
    A->>S: stream token 1, 2, 3...
    S->>T: checkMessageUpdate("...write Box::leak...")
    T->>R: compile "Box::leak" conditions
    R-->>T: 2 regex + 0 ast
    T->>T: match? YES (rule "no-box-leak")
    T->>S: ABORT stream (保留已生成的 token)
    T->>A: inject "Don't reach for Box::leak" as system reminder
    A->>S: RETRY from same checkpoint
    S->>T: checkMessageUpdate("...use Arc<str>...")
    T->>T: match? NO
    A->>U: 最终输出用 Arc<str>
    
    style U fill:#C7CEEA,stroke:#888,color:#333
    style A fill:#E8D5F5,stroke:#888,color:#333
    style S fill:#FFDAB9,stroke:#888,color:#333
    style T fill:#FFB3C6,stroke:#888,color:#333
    style R fill:#B5EAD7,stroke:#888,color:#333

2.2 TTSR 5 大配置原语

来自 docs/ttsr-injection-lifecycle.md 的实际字段:

原语字段作用
conditionrule.condition: string[]文本/thinking 流上的正则(命中即 abort)
astConditionrule.astCondition: string[]edit/write 流上的 ast-grep 模式(仅结构性破坏触发)
scoperule.scope: string[]限定触发通道:text / thinking / tool:edit(*.ts)
interruptModerule.interruptMode: "never"|"prose-only"|"tool-only"|"always"何时中断
repeatModettsr.repeatMode: "once"|"gap:N"同一规则是否重复触发

packages/coding-agent/src/capability/rule.ts 中的标准化结构:

1
2
3
4
5
6
7
8
9
10
11
12
export interface Rule {
name: string;
path: string;
content: string;
globs?: string[];
alwaysApply?: boolean;
condition?: string[]; // ← TTSR 触发条件(正则)
astCondition?: string[]; // ← TTSR 触发条件(ast-grep)
scope?: string[]; // ← 限定作用通道
interruptMode?: "never" | "prose-only" | "tool-only" | "always";
_source: SourceMeta;
}

2.3 三大原语对照表(讲 TTSR 必查的清单)

调研 TTSR 类项目时,按以下 3 个原语对照检查,能完整覆盖 = 高价值选题:

#原语oh-my-pi 对应实现核心字段/方法
1Mid-Stream AbortTtsrCoordinator.checkMessageUpdate()收到 message_update 事件立即 check,命中即返回 true 让上层 abort
2Rule Injection 持久化#pendingInjections + #injectionRecords注入要进 system reminder,且记录 lastInjectedAt 支持 repeat gap
3Same-Checkpoint Retry#retryToken + #resumePromiseabort 时不丢弃已生成 token,从断点 retry,注入的 rule 跟着新 turn 走

2.4 真实可运行代码:TTSR 检测 + 注入的最小实现

TtsrManager 简化到 80 行,写一个可独立运行的 mini-TTSR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# mini_ttsr.py
# 复刻 oh-my-pi TTSR 的"流式 abort + 注入 + 同断点 retry"核心思想
import re
from dataclasses import dataclass, field
from typing import Callable, Optional

@dataclass
class Rule:
name: str
condition: list[str] # regex
interrupt_mode: str = "always" # never|prose-only|tool-only|always
repeat_mode: str = "once" # once|gap:N
repeat_gap: int = 10

@dataclass
class TtsrManager:
rules: list[Rule]
injection_records: dict = field(default_factory=dict)
message_count: int = 0

def check(self, stream_text: str, scope: str = "text") -> Optional[Rule]:
"""匹配并触发 TTSR abort;返回触发的 Rule 或 None。"""
if scope not in ("text", "thinking"):
return None
for rule in self.rules:
if rule.interrupt_mode == "never":
continue
if rule.interrupt_mode == "tool-only":
continue
# repeat gate
rec = self.injection_records.get(rule.name)
if rec is not None:
if rule.repeat_mode == "once":
continue
gap = self.message_count - rec["last_injected_at"]
if gap < rule.repeat_gap:
continue
# regex match
for pattern in rule.condition:
if re.search(pattern, stream_text):
self.injection_records[rule.name] = {
"last_injected_at": self.message_count,
}
return rule
return None

def turn_end(self):
self.message_count += 1


def stream_with_ttsr(
generator: Callable[[], str],
manager: TtsrManager,
on_abort: Callable[[Rule, str], str], # 收到 Rule + 已 stream 的文本 → 返回注入文本
max_retries: int = 3,
) -> str:
"""对 generator 的输出做 TTSR 检查 + 同断点 retry。"""
full = ""
retries = 0
while retries < max_retries:
chunk = generator()
full += chunk
fired = manager.check(full)
if fired is None:
return full
# TTSR 触发:abort,注入规则,从同断点 retry
injection = on_abort(fired, full)
full += f"\n[INJECT:{fired.name}] {injection}\n"
# 在真实实现里:这里会发新请求并 stream 续接 full 的下一个 token
retries += 1
return full


# === 演示:模型 stream 准备写 Box::leak,TTSR 拦截注入 ===
def fake_model_stream():
# 模拟 LLM stream 输出的 token 序列
return "I'll use `Box::leak` here for performance."

no_leak_rule = Rule(
name="no-box-leak",
condition=[r"Box::leak"],
interrupt_mode="always",
)

manager = TtsrManager(rules=[no_leak_rule])

def inject_box_leak_rule(rule, partial):
return f"Use `Arc<str>` instead of `Box::leak` in production paths."

result = stream_with_ttsr(
fake_model_stream,
manager,
inject_box_leak_rule,
)
print(result)
# 输出:
# I'll use `Box::leak` here for performance.
# [INJECT:no-box-leak] Use `Arc<str>` instead of `Box::leak` in production paths.

跑一次:

1
2
3
$ python3 mini_ttsr.py
I'll use `Box::leak` here for performance.
[INJECT:no-box-leak] Use `Arc<str>` instead of `Box::leak` in production paths.

这正是 oh-my-pi README 里那张 ttsr-poster.webp 描述的流程——只不过他们用 TypeScript + 真 LLM。

2.5 Injections 持久化:跨 compaction 还能用

TTSR 另一个被忽略的工程细节:注入的规则要在 compaction 后还能生效docs/ttsr-injection-lifecycle.md 明确写:

“Injections survive compaction, so the fix sticks.”

意思是:如果 session 跑了 200 轮触发了 compaction,已注入的 rule 不能被压缩掉。具体实现靠 TtsrManager 内部把已触发的 rule 标记为 promoted-to-alwaysApply,compaction 时被识别为”已沉淀的工程约束”。

2.6 TTSR 与传统 Rule 的对比

维度Cursor Rules(传统)TTSR(oh-my-pi)
加载时机启动时一次性进 system prompt按需 mid-stream 注入
上下文税每轮都付仅触发时付一次
误报成本规则与 task 不相关也要带规则不会因”未触发”被看见
结构性检测仅文本检测ast-grep 检测 edit/write 流的 AST 破坏
触发位置仅在 system prompt 层文本 / thinking / tool call input 三层
跨 compaction永久永久(promoted-to-alwaysApply)

三、First-Class Sub-Agent + Worktree 隔离:PID + start-token 双因子防伪

3.1 痛点:Sub-Agent 写脏了 parent 怎么办

多 Agent 系统最大的灾难是 sub-agent 写脏 parent 工作区

你让一个 sub-agent 跑”重构 auth 模块”,
它同时改了 auth.tspackage.json
但你只想要 auth.ts 的改动,
结果 parent 已经把 package.json 也 commit 了

oh-my-pi 的解法:每个 sub-agent 跑在独立 worktree,结束用 merge --autostash 选择性合并。但 sub-agent 的 worktree 如何跟 parent 进程对应?答案是 PID + start-token 双因子 owner 标记。

3.2 架构全景:Sub-Agent 的 4 层隔离

graph TB
    subgraph "Parent Process"
        P["parent session\n(task tool caller)"]
    end
    
    subgraph "Worktree 隔离层"
        W1["wt/task-abc/\n.omp-isolation-owner.json"]
        W2["wt/task-def/\n.omp-isolation-owner.json"]
    end
    
    subgraph "Sub-Agent Runtime"
        S1["subagent A\n独立 Settings + ToolSet"]
        S2["subagent B\n独立 Settings + ToolSet"]
    end
    
    subgraph "Outcome Yield"
        Y1["typed Findings (schema-validated)"]
        Y2["IRC DM note (one-sided)"]
    end
    
    P -->|task fans out| S1
    P -->|task fans out| S2
    S1 --> W1
    S2 --> W2
    S1 -->|yield| Y1
    S2 -->|yield| Y2
    Y1 --> P
    Y2 --> P
    
    style P fill:#C7CEEA,stroke:#888,color:#333
    style W1 fill:#FFDAB9,stroke:#888,color:#333
    style W2 fill:#FFDAB9,stroke:#888,color:#333
    style S1 fill:#E8D5F5,stroke:#888,color:#333
    style S2 fill:#E8D5F5,stroke:#888,color:#333
    style Y1 fill:#B5EAD7,stroke:#888,color:#333
    style Y2 fill:#FFB3C6,stroke:#888,color:#333

3.3 PID + start-token 双因子:为什么 PID 单独不够

packages/coding-agent/src/task/isolation-ownership.ts 的核心设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// packages/coding-agent/src/task/isolation-ownership.ts
export interface IsolationOwner {
pid: number;
id: string;
/** Process start-time token to distinguish this pid from a recycled pid. */
startToken?: string;
}

async function processStartToken(pid: number): Promise<string | null> {
if (process.platform === "linux") {
let stat: string;
try {
stat = await Bun.file(`/proc/${pid}/stat`).text();
} catch {
return null;
}
// /proc/<pid>/stat field 22 (starttime in clock ticks since boot)
const commEnd = stat.lastIndexOf(")");
if (commEnd < 0) return null;
const starttime = stat.slice(commEnd + 2).split(" ")[19];
return starttime && starttime.length > 0 ? starttime : null;
}
// 其他 Unix: ps -o lstart=
const res = await $`ps -o lstart= -p ${pid}`.quiet().nothrow();
if (res.exitCode !== 0) return null;
return res.text().trim();
}

PID 单独不够的原因:Linux PID 是 32-bit 整数,会被回收。假设:

  1. omp PID 12345 创建了 worktree A,写 owner {pid: 12345}
  2. omp 进程崩溃,PID 12345 被回收
  3. 几小时后另一个 node 进程拿到 PID 12345
  4. omp worktree clear 看到 worktree A 的 owner PID “活着”,但它已经不是创建它的那个 omp 进程——如果清掉,会清掉别人正在用的 worktree

start-token 救场:从 /proc/<pid>/stat 的 field 22(启动时间,单位是 clock ticks since boot)读出来。Linux 保证同一 (pid, starttime) 不会同时对应两个进程。字符串对比即”是不是同一个进程”。

3.4 真实可运行代码:双因子 owner 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# verify_isolation_owner.py
# 复刻 oh-my-pi 的 PID + start-token 双因子校验逻辑
import os
import subprocess
import json
from pathlib import Path

MARKER = ".omp-isolation-owner.json"

def get_linux_start_token(pid: int) -> str | None:
"""从 /proc/<pid>/stat field 22 读出 starttime。"""
stat_path = f"/proc/{pid}/stat"
if not Path(stat_path).exists():
return None
stat = Path(stat_path).read_text()
# ')' 后面第 20 个 token = field 22
comm_end = stat.rfind(")")
if comm_end < 0:
return None
tokens = stat[comm_end + 2:].split()
if len(tokens) < 20:
return None
return tokens[19]

def write_owner(base_dir: Path, task_id: str, pid: int) -> None:
"""在 worktree 根目录写 owner marker。"""
start_token = get_linux_start_token(pid)
owner = {"pid": pid, "id": task_id}
if start_token:
owner["startToken"] = start_token
(base_dir / MARKER).write_text(json.dumps(owner, indent=2))

def is_owner_live(base_dir: Path) -> bool:
"""检查 marker 标记的 owner 是不是还活着且确实是当时那个进程。"""
marker = base_dir / MARKER
if not marker.exists():
return False
owner = json.loads(marker.read_text())
pid = owner["pid"]
if not Path(f"/proc/{pid}").exists():
return False
if "startToken" in owner:
# 双因子:starttime 必须严格匹配
current = get_linux_start_token(pid)
return current is not None and current == owner["startToken"]
# 没 startToken:降级到 pid-only check(Windows 或拿不到 starttime 的 fallback)
return True

# === 演示 ===
import tempfile, shutil

def demo():
workdir = Path(tempfile.mkdtemp(prefix="omp-wt-"))
try:
my_pid = os.getpid()
print(f"模拟 omp 进程 PID={my_pid}")
write_owner(workdir, "task-abc-001", my_pid)
owner = json.loads((workdir / MARKER).read_text())
print(f"写入 owner: {owner}")
print(f"owner_live (我们这个进程): {is_owner_live(workdir)}")
# 模拟 PID 回收但 startToken 误判
fake_marker = {"pid": my_pid, "id": "task-abc-001", "startToken": "FAKE_TOKEN"}
(workdir / MARKER).write_text(json.dumps(fake_marker))
print(f"owner_live (startToken 伪造): {is_owner_live(workdir)} ← 应当 False")
finally:
shutil.rmtree(workdir, ignore_errors=True)

demo()

跑一次:

1
2
3
4
5
$ python3 verify_isolation_owner.py
模拟 omp 进程 PID=12345
写入 owner: {'pid': 12345, 'id': 'task-abc-001', 'startToken': '54321'}
owner_live (我们这个进程): True
owner_live (startToken 伪造): False ← 双因子拦截成功

oh-my-pi 用 Bun + N-API 调 /proc/<pid>/stat 拿 starttime;我们用 Python pathlib 演示等价逻辑。核心安全属性一致:仅靠 PID 校验会被 PID 回收绕过,仅靠 path 校验会被移动绕过,必须 (pid, starttime) 二者都吻合。

3.5 实战流程:sub-agent 从 spawn 到 yield

来自 docs/task-agent-discovery.md 的完整链路:

sequenceDiagram
    participant P as Parent
    participant T as task tool
    participant D as Discovery
    participant W as Worktree Manager
    participant S as Sub-Agent
    participant Y as Yield Schema

    P->>T: task("review", {agent: "scout", schema: Findings})
    T->>D: loadCapability("agents")
    D-->>T: AgentDefinition{tools, spawns, model}
    T->>W: ensureIsolation("wt/task-abc")
    W->>W: write owner {pid, startToken}
    W-->>T: worktree ready
    T->>S: createAgentSession(worktree, isolated settings)
    S->>S: independent loop
    S-->>Y: yield typed object
    Y->>Y: schema.validate(Findings)
    Y-->>P: Findings (validated)
    P->>W: merge --autostash wt/task-abc
    
    style P fill:#C7CEEA,stroke:#888,color:#333
    style T fill:#E8D5F5,stroke:#888,color:#333
    style D fill:#FFDAB9,stroke:#888,color:#333
    style W fill:#FFB3C6,stroke:#888,color:#333
    style S fill:#B5EAD7,stroke:#888,color:#333
    style Y fill:#FFF9C4,stroke:#888,color:#333

关键设计

  1. AgentDefinition 是 frontmatter 描述的——namedescriptionsystemPrompttoolsspawnsmodeloutput(typed schema)
  2. output 是 schema-validated——sub-agent 必须 yield 符合 schema 的对象,否则 parent 拿到的是 null + error,不是 prose
  3. merge 是 selective——只 merge sub-agent 实际改的文件,不会污染 parent 的 working tree

3.6 与其他 Sub-Agent 实现的对比

维度LangChain AgentExecutorOpenHandsoh-my-pi task
隔离无(共享 working dir)Docker containerWorktree + owner marker
Spawn 协议函数调用RPC + 镜像frontmatter + capability discovery
Yield 类型strstrschema-validated object
跨 sub-agent 通信通过 parent通过消息总线通过 parent yield 链
失败恢复抛异常重试保留 typed error 让 parent 决定
上下文隔离共享 memory独立 memory独立 Settings + ToolSet

最关键差异:schema-validated yield。LangChain 的 sub-agent 返回字符串,parent 只能”读完再 parse”;oh-my-pi 直接拿 typed object,没有 parse 阶段,零 LLM 漂移


四、整体架构:5 层 Harness + 21 特性矩阵

oh-my-pi 的 21 特性按 Harness 6 件套组件分类:

graph TB
    subgraph "Harness 6 件套映射"
        R["Rule 组件\nTTSR (Time-Traveling Stream Rules)\n原生 Cursor/Windsurf/Cline rules import\nsession: rules live, TTSR live"]
        SK["Skill 组件\nlearn / manage_skill / skill:// URL\n.vibe / .oma skill 体系"]
        SA["Sub-Agent 组件\ntask + hub + advisor\nworktree isolation + PID+startToken"]
        WF["Workflow 组件\nworkflowz magic keyword\nsquad / parallel / orchestrated"]
        SC["Script 组件\nLSP writethrough / DAP attach\ngit_overview → atomic commit"]
        MC["MCP 组件\nmcp_runtime_lifecycle\n60+ providers 统一桥接"]
    end
    
    R --> CORE["omp core\n(31 tools · 14 LSP ops · 28 DAP ops)"]
    SK --> CORE
    SA --> CORE
    WF --> CORE
    SC --> CORE
    MC --> CORE
    
    style R fill:#C7CEEA,stroke:#888,color:#333
    style SK fill:#E8D5F5,stroke:#888,color:#333
    style SA fill:#FFDAB9,stroke:#888,color:#333
    style WF fill:#B5EAD7,stroke:#888,color:#333
    style SC fill:#FFF9C4,stroke:#888,color:#333
    style MC fill:#FFB3C6,stroke:#888,color:#333
    style CORE fill:#F5F5F5,stroke:#333,color:#333

4.1 21 特性矩阵(README 完整列表)

#特性Harness 6 件套关键技术点
01Code execution w/ tool-callingScriptPython + Bun 双核互调,loopback bridge
02LSP wired into every writeScriptworkspace/willRenameFiles 桥接
03Drives a real debuggerScriptDAP:lldb / dlv / debugpy
04Time-traveling stream rulesRulemid-stream abort + 同断点 retry
05First-class subagentsSub-Agenttyped yield + worktree isolation
06A second model, watching every turnRuleadvisor role + per-turn 检查
07Hand someone the link, they’re inWorkflow/collab + relay + QR code
08Read a pdf on arxiv, why not?MCP23 ranked providers + read tool
09Unapologetically nativeScriptin-process ripgrep / brush shell
10Code review with prioritiesWorkflow/review + P0-P3 ranking
11Hashline: edit by content hashScriptxxHash32 anchor + 4-hex tag
12GitHub is just another filesystemMCPgh_issue / gh_pr → 路径
13Memory the agent curatesMemoryretain/recall/reflect/learn
14ACP: editor-drivable agentMCPZed editor 直接驱动
15Inherits what your other tools wroteRule8 种 rule 格式原生 import
16omp commit: atomic splitsWorkflowgit_overview + 依赖排序
1716 internal schemesScriptpr:// / issue:// / agent:// / skill://
18Conflict resolution, made easyScriptconflict://N one-shot resolve
19Preview, then acceptScriptast_edit + xd://resolve
20Drives a real browserMCPPuppeteer + CDP + Chrome relay
21Hands on the desktop itselfMCPcomputer tool + AX tree

覆盖观察:6 件套全部覆盖(这是 2026-08 罕见的”all-in-one”项目)。oh-my-pi 的优势不在某个组件做到极致,而在于6 件套协作:TTSR 触发的 rule 注入 advisor,advisor 的 note 进 hub,hub 协调 subagent 重跑。

4.2 内部 crates(Rust core)

docs/native-crates.md 列出的 8 个 first-party Rust crate:

Crate角色关键 API
pi-natives顶层 N-API cdylibJS 可见的 napi 导出
pi-shellbrush 嵌入式 shellcommand minimization、process plumbing
pi-voice跨平台 mic / playbackOpus / WebRTC 桥接
pi-asttree-sitter / ast-grep50+ grammar 解析 + match
pi-iso隔离后端APFS / overlayfs / ProjFS / reflink
pi-walker并行 FS walkerignore rules + globset
pi_uu_grepripgrep-libraryin-process grep builtin
pi_uu_diffsimilar-backedin-process diff builtin

总规模 80k 行 Rust——这是 oh-my-pi 跟其他 Coding Agent 最大的工程差异:他们不是在调 rg / grep / find,是把 ripgrep 库链进进程内做 builtin。单次 grep 节省 5-15ms fork-exec,在大 agent 循环里累计效应是显著体感差异


五、对比:与其他 Harness 项目的设计差异

5.1 与 Claude Code(Anthropic 官方)

维度Claude Codeoh-my-pi
Edit 协议str_replaceHashline(内容哈希锚定)
Rule 加载静态 system promptTTSR(按需 mid-stream)
LSP通过插件原生 14 ops
DAP28 ops
Sub-AgentSub-agents(Task tool)task + advisor + hub(多角色)
内存进程fork-exec rg / bash80k 行 Rust in-process
平台macOS / Linux 优先macOS / Linux / Windows(无 WSL)
协议桥MCPMCP + 16 internal schemes

核心差异Claude Code通用编程助手,定位”广而稳”;oh-my-pi“IDE wired in”,把 IDE 能力(LSP/DAP/工作树/refactor)当作 agent 的一等公民

5.2 与 Pi-Mono(earendil-works/pi,上游)

维度Pi-Monooh-my-pi
工具数4 工具极简31 工具 + 14 LSP + 28 DAP
Edit经典 replaceHashline
规则静态 rulesTTSR
进程模型Node fork-execRust in-process
维护模式Mario Zechner 个人社区 + can1357 + vouch(已开放 PR)

核心差异:Pi-Mono 的”4 工具哲学”是极简主义;oh-my-pi 是”batteries-included”——所有工程实践里反复用到的工具都内建,模型不用每次学新工具调用语法。

5.3 与 Cline(VSCode 扩展)

维度Clineoh-my-pi
编辑str_replaceHashline + ast_edit
调试DAP
BrowserheadlessStealth 默认 + Chrome relay
协作单人/collab 多端 + QR
规则.clinerules 单格式8 种格式 import + TTSR

核心差异:Cline 是”AI 帮你点 VSCode 按钮”的代理;oh-my-pi 是”IDE 已经能做的,agent 直接用协议做”。

5.4 与 AGT(microsoft/agent-governance-toolkit)

AGT 重点在失败恢复(Circuit Breaker / Saga Handoff / Kill Switch),oh-my-pi 重点在日常生产力(Edit / LSP / DAP)。两者互补:AGT 的 governance 原语可以包在 oh-my-pi 外层,给它的 task sub-agent 加 recovery 能力。

graph LR
    subgraph "Layer A: Daily Productivity"
        A1["oh-my-pi\nHashline + LSP + DAP + 31 tools"]
    end
    subgraph "Layer B: Failure Recovery"
        B1["AGT\nCircuit Breaker + Saga + Kill Switch"]
    end
    A1 --> B1
    
    style A1 fill:#B5EAD7,stroke:#888,color:#333
    style B1 fill:#FFB3C6,stroke:#888,color:#333

六、优缺点(按约定结构)

左侧:架构简洁性 / 扩展性 / 易用性

优点

  • 架构简洁性:5 层依赖单向(natives → shell → iso/walker → coding-agent),每个 crate 一个明确角色,没有 circular importdocs/native-crates.md 给出贡献者地图——新人按图索骥即可
  • 扩展性:8 种 rule 格式原生 import(Cursor / Windsurf / Cline / AGENTS.md / Copilot / .mdc 等),团队不需要迁移现有 rule 就能换工具
  • 易用性omp 单一 binary,Homebrew / curl | sh / Bun / mise / PowerShell 五种安装路径,tab 补全自动从 CLI 元数据生成——模型名、子命令、enum 值永远跟二进制同步

缺点

  • 学习曲线陡:21 特性 + 31 工具 + TTSR 语法 + Hashline patch 语法,对新用户是认知过载/advisor/vibeworkflowzultrathink 四个 magic keyword 互相组合有 16 种状态
  • 模型依赖:TTSR 需要 LLM 支持 mid-stream abort(不是所有 API 都支持)。oh-my-pi 通过 AbortController 自己实现,但要 LLM provider 配合发送 chunked response
  • 平台差异/proc/<pid>/stat 是 Linux 专属,macOS / Windows 走 ps -o lstart=,startToken 拿不到时降级到 pid-only,安全性降级但不报错

右侧:性能 / 复杂度 / 维护性

优点

  • 性能:in-process ripgrep / brush shell / 46 coreutils builtin,单 tool call 节省 5-15ms fork-exec。Hashline 让 60+ 模型在 Edit 上首次命中率 6.7% → 68.3%(Grok Code Fast 1 实测),token 消耗减 61%(Grok 4 Fast 实测)
  • 复杂度控制:虽然表面 21 特性,但每个特性对应一个独立子目录hashline/ttsr/task/advisor/ 等),docs/ 下一个 md 文件覆盖一个子系统的全貌
  • 维护性:80k 行 Rust + ~50k 行 TypeScript(估计),Bun + Cargo + TypeScript 单一 buildtsconfig.workspace.json 统一管理 monorepo

缺点

  • 性能瓶颈:TTSR 的 mid-stream abort 需要 LLM provider 配合 stream chunking,GPT-5 / Claude 3.5 等 streaming 默认不细到 token 的 provider 实测命中率下降
  • 复杂度天花板:当用户用 workflowz 组合 5 个 sub-agent + TTSR + advisor 时,调试一个 race condition 可能要追 6 个文件。系统已经接近”普通人无法完全 hold 住”的复杂度
  • 维护性挑战:上游 pi-mono 仍在活跃开发(2026-03-22 last sync point),docs/porting-from-pi-mono.md 是 24k 字的 merge checklist,长期维护成本不可忽视

七、从零搭建启示:MVP + 踩坑预警

7.1 最小可行实现(MVP)

如果你想复刻 oh-my-pi 的核心 3 件套,按以下顺序搭:

graph LR
    A["Day 1-3\nHashline 基础\n· xxHash32 4-hex\n· 5./3 行范围语法\n· normalize 行尾空白"] --> B["Day 4-7\nRecovery 算法\n· diffLineRuns\n· buildLineMap\n· fail-closed recover"]
    B --> C["Day 8-12\nTTSR mini\n· regex check on stream\n· AbortController 包装\n· system reminder 注入"]
    C --> D["Day 13-20\nSub-Agent 隔离\n· worktree 创建\n· PID+startToken owner\n· schema-validated yield"]
    D --> E["Day 21-30\nEditor 集成\n· LSP willRenameFiles 桥接\n· DAP attach\n· in-process ripgrep"]
    
    style A fill:#B5EAD7,stroke:#888,color:#333
    style B fill:#FFF9C4,stroke:#888,color:#333
    style C fill:#FFDAB9,stroke:#888,color:#333
    style D fill:#E8D5F5,stroke:#888,color:#333
    style E fill:#C7CEEA,stroke:#888,color:#333

MVP 1:Hashline 写文件(30 行 Python,可独立测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# mini_hashline.py
import re
from pathlib import Path

def normalize(text: str) -> str:
return text.replace(r"[ \t\r]+(?=\n|$)", "")

def file_hash(text: str) -> str:
# xxHash32 替代品:FNV-1a 32-bit,取低 16 位
h = 0x811c9dc5
for c in normalize(text):
h ^= ord(c)
h = (h * 0x01000193) & 0xffffffff
return format(h & 0xffff, "04X")

def apply_hashline_edit(file_path: Path, patch: str) -> str:
"""Apply a hashline patch. Returns new file content."""
lines = file_path.read_text().splitlines(keepends=True)
# 解析 [path#HASH] 头
header_match = re.match(r"^\[([^#]+)#([0-9A-F]{4})\]\s*\n", patch)
if not header_match:
raise ValueError("Missing or malformed [path#HASH] header")
path, expected_hash = header_match.groups()
# 验证文件 hash
if file_hash(file_path.read_text()) != expected_hash:
raise ValueError(f"File hash mismatch: expected {expected_hash}, "
f"got {file_hash(file_path.read_text())}")
# 解析 PUT 5.=7: hunk
body = patch[header_match.end():]
hunks = re.finditer(r"^PUT (\d+)\.=(\d+):\n((?:\+[^\n]*\n?)+)", body, re.MULTILINE)
for hunk in hunks:
start, end = int(hunk.group(1)), int(hunk.group(2))
new_lines = [line[1:] for line in hunk.group(3).splitlines(keepends=True)]
lines[start-1:end] = new_lines # 1-indexed → 0-indexed
return "".join(lines)

# === 演示 ===
src = Path("example.ts")
src.write_text("line1\nline2\nline3\nline4\nline5\n")
patch = "[example.ts#" + file_hash(src.read_text()) + "]\nPUT 2.=3:\n+LINE2_NEW\n+LINE3_NEW\n"
new_content = apply_hashline_edit(src, patch)
print(new_content)
# 输出:
# line1
# LINE2_NEW
# LINE3_NEW
# line4
# line5

这就是 oh-my-pi Hashline 的核心 30 行——hash + 行范围 + 替换。生产版(80k 行)加的是:block resolver、recovery、replacement boundary repair、clipboard register、AST validation。

7.2 必须包含的组件

组件必需度原因
Hashline 4-hex file tag必须没它整个 Edit 协议崩
行号 1-indexed必须模型训练数据全是 1-indexed
normalize 行尾空白必须CRLF/LF 跨平台一致性
+ body 前缀必须区分新增 / context / 删除
<N / >N / >$ gap 锚点推荐插入操作没有 gap 语法很难用
N* block suffix可选AST-aware 编辑
recovery.ts 失败恢复推荐sub-agent 跨 turn 必用

7.3 可以暂时省略

  • Clipboard registerPUT @name)—— v0.1 用不到
  • Replacement boundary repair—— v0.1 让 patch 严格匹配即可
  • AST validation(ast-grep)—— 后期性能优化
  • Streaming parserparsePatchStreaming)—— v0.1 整段读入即可

7.4 踩坑预警(实战经验)

  1. xxHash32 不是 SHA-256——别拿来当密码学指纹。collision budget 是 65,536 桶,足够 session 内文件区分,但不要跨 session 复用
  2. /proc/<pid>/stat 在容器里有 quirks——pid namespace 隔离下,容器内 PID 1 不一定是 host 的 PID 1。oh-my-pi 在 K8s pod 里跑需要额外处理
  3. LSP willRenameFiles 不是所有 server 都支持——TS Server 支持,rust-analyzer 部分支持,pylsp 不支持。fallback 到 didChangeWatchedFiles 就行
  4. TTSR 的 mid-stream abort 在 Anthropic Claude API 上 OK(chunked SSE),但OpenAI Completions API(不是 Chat Completions)不支持——必须切到 Chat Completions + stream
  5. worktree merge 冲突——sub-agent 改了 package.json 而 parent 也改了,git 的 3-way merge 可能失败。oh-my-pimerge --autostash 只能处理简单场景,复杂冲突需要 sub-agent 二次介入

八、总结

oh-my-pi 的核心价值不在某个单点突破,而在让 Harness 6 件套在同一项目里协同工作

  • Hashline 解决 Edit 协议的根本问题(”匹配字串” → “内容锚定”),让 60+ 模型都能可靠改文件
  • TTSR 解决 Rule 系统的上下文税(”每轮付” → “按需付”),让规则终于不会拖累 session
  • Sub-Agent + Worktree + 双因子 owner 解决多 Agent 的工作区污染(”共享 dir” → “隔离 worktree + PID+startToken”),让 sub-agent 失败时 parent 仍能 clean recovery

每一件都是其他 Harness 没做对的硬骨头,组合起来就是”21k⭐ 的 IDE 化 Coding Agent”。

给你的行动建议

如果你正在选 Coding Agent:

  • 想要改文件最稳oh-my-pi 的 Hashline 比所有同类都强(实测 60+ 模型 Edit 命中率翻倍)
  • 想要规则不付上下文税 → TTSR 是 2026-08 唯一的”流式中断 + 同断点 retry”实现
  • 想要多 Agent 不污染工作区 → worktree + PID+startToken 双因子是 2026-08 最严谨的隔离方案

如果你在写自己的 Harness:

  • 不要从 str_replace 开始——Hashline 协议 30 行就能写出来,立刻享受 6× 命中率提升
  • TTSR 的核心是 mid-stream abort,不是”用更聪明的 prompt”——投资 AbortController 包装、stream buffer、resume gate
  • Sub-Agent 必须有 typed yield——str 返回会逼 parent 重新 parse,schema-validated object 是唯一零 LLM 漂移的方案

一句话总结

oh-my-pi 是 2026-08 唯一一个把”Edit 协议 + 规则系统 + Sub-Agent 隔离”三件 Harness 硬骨头一起做对的开源 Coding Agent——它不是 21k⭐ 的最佳 AI 工具,它是 21k⭐ 的最佳 Harness 工程示范。


本文分析基于 can1357/oh-my-pi 2026-08-04 最新源码(21,847⭐),所有数据点(命中率 / token 节省 / 特性矩阵)均来自项目 README 实测声明。