跳到主要内容

原生核心:进程内、无 fork-exec 的 Rust 层

30 秒导读: 大多数编码 agent 想搜代码就 spawn("rg", ...)、想跑命令就 spawn("bash", ...)。oh-my-pi 反其道:把这些活写成一层约六万行的 Rust,通过 N-API(Node/Bun 的原生扩展接口)直接在 agent 进程内调用——搜索、发现、结构摘要、分词、语法高亮全程不起子进程;连 bash 都是内嵌的解释器。本章讲这层怎么搭、跨 macOS/Linux/Windows 三平台怎么取舍、JS 和 Rust 之间的绑定契约长什么样。


1. 这是什么(零基础也能懂)

一句话定义: 原生核心是 oh-my-pi 的"手脚下沉层"——把编码 agent 每天要用的重活(文件搜索、内容 grep、目录遍历、AST 结构摘要、token 计数、shell 执行、工作区克隆)用 Rust 实现,编译成一个 .node 动态库,让 JavaScript 侧像调普通函数一样直接调用。

它替谁干活: 一个 agent 主循环(见 主循环与回合模型)在一次回合里可能要 grep 上万个文件、给上下文算 token 预算、把长文件折叠成结构摘要。别的 harness 通常这么干:

别的 harness: Node 进程 ──spawn──▶ rg 子进程 (读文件、匹配、把结果打回 stdout)
──spawn──▶ fd 子进程
──spawn──▶ bash 子进程
──spawn──▶ tiktoken(Python)

每个 spawn 都要:找到二进制、拉起进程、串行化参数、等 stdout、解析文本、清理僵尸进程。启动开销、跨进程拷贝、平台差异(Windows 没有 fork)全砸在你脸上。

oh-my-pi 的做法: 把这些做进同一个进程。

oh-my-pi: Node/Bun 进程
└─ N-API ─▶ Rust: grep() (grep-searcher 库,直接读内存/mmap)
glob()/fuzzyFind()
summarizeCode() (tree-sitter)
countTokens() (tiktoken-rs)
Shell.run() (brush:内嵌 bash 解释器)

为什么值得: 三个直接好处——

好处原因
没有进程启动开销;结果通过 N-API 直接变成 JS 对象,不经过"文本序列化→再解析"这一圈
跨平台一致Rust 一份代码编译到三平台;不用担心用户机器上装没装 rg/fd,也不用为 Windows 缺 fork 单开逻辑
可控取消、超时、并发上限、性能采样都是自己的代码说了算,不是"祈祷子进程听 SIGTERM"

一句话直觉: 把"外包给命令行工具"改成"自己长出这些器官"。ripgrep 不再是一个你 exec 的程序,而是一个你 use 的库(grep-searchergrep-regex);bash 不再是 /bin/bash,而是一个用 Rust 写的、活在你进程里的 bash 解释器(brush)。

一处诚实的边界(先说,后面 §6 展开): "无 fork-exec" 是这层搜索/发现/编辑/分词类工具的性质,不是全部。真正绕不开子进程的地方它照样 spawn:PTY 交互式命令要拉起真实进程(pty.rs),工作区隔离的兜底后端要调 git worktree(rcopy.rs)。内嵌的 bash 解释器本身在进程内,但你脚本里写 git status,那个 git 仍然是外部子进程。本章会把"哪些真进程内、哪些仍 spawn"标清楚。


2. 顶层全景(它大概怎么转)

2.1 一张结构图

packages/natives/native/ (JS 加载侧)
┌──────────────────────────────────────┐
agent 工具层 ──────▶ │ index.js → loader-state.js │
(第3章工具封装) │ ·选 CPU 变体(AVX2?) ·版本哨兵校验 │
│ ·装 Tokio 运行时 ·require .node │
└───────────────┬────────────────────────┘
│ N-API (napi-rs)
════════════════╪════════════════ JS ↔ Rust 边界

crates/pi-natives (N-API 导出聚合 = lib.rs)
┌──────────────┬──────────────┬──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
发现/搜索 结构/文本 调度/进程 shell 隔离
grep glob fd ast summary task prof shell(napi) iso(napi)
workspace tokens ps pty │ │
fs_cache highlight ────┬─── ▼ ▼
│ │ │ crates/pi-shell crates/pi-iso
└── ignore 并行 └── tree-sitter │ (brush 内嵌bash) (CoW 克隆 PAL)
walker / tiktoken └── libuv 线程池 + Tokio

怎么读:上半是 JS 加载器(把对的 .node 挑出来、验对、装好);中间横线是 N-API 边界;下半是 Rust,按职责分五组。发现/搜索组都靠底层的 ignore 并行遍历器 + fs_cache;shell 和 iso 各自委托给独立 crate。

2.2 部件一句话职责

部件干什么在哪
lib.rsN-API 导出总聚合;装崩溃处理、装 Tokio 运行时、发版本哨兵crates/pi-natives/src/lib.rs
task.rs把阻塞 Rust 活丢到 libuv 线程池;取消/超时/采样crates/pi-natives/src/task.rs
fs_cache.rsignore 并行遍历 + TTL 键控的扫描缓存,glob/fd 共享crates/pi-natives/src/fs_cache.rs
grep.rs进程内 ripgrep:grep-searcher/grep-regex + mmapcrates/pi-natives/src/grep.rs
glob.rs / fd.rsglob 匹配、模糊找文件(@ 提及补全)crates/pi-natives/src/{glob,fd}.rs
workspace.rs启动时一遍扫工作区树 + 发现 AGENTS.mdcrates/pi-natives/src/workspace.rs
summary.rs/ast.rstree-sitter 结构摘要、ast-grep 结构搜索/改写crates/pi-natives/src/{summary,ast}.rs
tokens.rs/highlight.rsBPE token 计数、syntect 语法高亮crates/pi-natives/src/{tokens,highlight}.rs
shell.rs+pi-shell内嵌 bash(brush)、跨调用存活的会话crates/pi-natives/src/shell.rscrates/pi-shell
iso.rs+pi-iso工作区隔离:CoW clone / reflink / overlayfs / projfs / rcopycrates/pi-natives/src/iso.rscrates/pi-iso

规模参照:整个 workspace 的 Rust 约 6.2 万行(不含 vendor),其中 pi-natives(N-API 薄壳 + 大部分逻辑)约 1.5 万行,余下在 pi-shellpi-astpi-iso 等独立 crate。

2.3 主线走一遍(以一次 grep 为例,不进代码)

JS: await native.grep({pattern, path, signal})

▼ N-API 把参数 marshal 成 Rust 结构体 GrepOptions
task::blocking("grep", CancelToken, work) ← 立刻返回一个 JS Promise
│ work 被排到 libuv 线程池的一个 worker 上跑,主线程不阻塞

ignore 并行遍历器走目录(尊重 .gitignore,跳过 .git/node_modules)
│ 每个文件:grep-searcher 在内存/mmap 上匹配 grep-regex
│ 每 128 个条目 check 一次 CancelToken::heartbeat()(超时/中止就退)

命中通过 on_match 回调流式回 JS(可选),最终结果 resolve 回 Promise

关键:整趟没有子进程;取消是协作式的(靠 heartbeat);Promise 的兑现发生在 JS 主线程,重活发生在 worker 线程。


3. 核心机制(逐个,由浅入深)

3.1 JS ↔ Rust 绑定契约:一个 .node 怎么被安全装进来

要解决的小问题: 一个预编译的原生库,在用户五花八门的机器上,得挑对 CPU 变体、验明"没装错版本"、再把异步运行时装好——任何一步错都会变成难懂的崩溃。

思路: 把加载做成一条纯管线,每一步都有兜底。JS 侧(loader-state.js)负责选文件和验证,Rust 侧(lib.rs)负责在恰当时机装运行时。

四件必须做对的事:

  1. 选 CPU 变体。 x64 上分 baseline / modern 两个 .node,后者用 AVX2。加载器探测 /proc/cpuinfosysctl 或 PowerShell 判断 AVX2,结果缓存进一个私有环境变量,让 worker/子进程继承、不必重探(selectCpuVariant,detectAvx2Support)。

  2. 版本哨兵。 Rust 导出一个名字里编了版本号的空函数 __piNativesV16_2_5(pi_natives_version_sentinel,lib.rs:176);发版脚本把这名字和 package.json 版本一起 bump。加载器算出期望名字,.node 不暴露它就拒载(validateLoadedBindings)。这把 Windows 上"锁文件更新留下旧 .node"导致的 <sym> is not a function 静默崩溃,变成一句能看懂的报错。

  3. 崩溃处理器在最早时机装。 #[module_init] 标注的 install_native_crash_handler(lib.rs:191)在 .nodedlopen 时就跑——此时动态加载器锁还握着,绝不能起线程

  4. Tokio 运行时延迟装。 多线程 Tokio 会 eager 起 worker 线程;在模块初始化里起线程会和加载器锁死锁。所以运行时不在 #[module_init] 装,而是加载器在 dlopen 返回后单独调一次 __ompInstallTokioRuntime(omp_install_tokio_runtime,lib.rs:218)。

Windows 的额外小心(巧妙): 内存紧张的 Windows 主机上,eager 起满 worker 会直接 os error 1455 把进程干掉。所以 create_windows_napi_tokio_runtime(lib.rs:140)先用 std::thread::Builder::spawn 预探能稳定并存几个线程(probe_spawnable_workers,lib.rs:109),按探到的数目建运行时,建不出就退化到单线程运行时——永不 panic。这套探测被刻意限定 Windows-only,因为 Linux 上在加载期起探测线程反而会和 Bun 加载 .node 死锁。

dlopen(.node)
│ #[module_init] → install_native_crash_handler() (不起线程!)

loader-state.js: validateLoadedBindings() (版本哨兵对不对?)


loader-state.js: installNativeTokioRuntime()
│ → Rust omp_install_tokio_runtime()
│ (Windows: 预探线程数 → 建 bounded 运行时 / 退化单线程)

第一个 async 原生调用 (napi-rs 的 LazyLock 此刻才真正物化运行时)

3.2 任务调度:把阻塞的 Rust 活丢到 libuv,而不冻住主线程

要解决的小问题: grep 一个大仓可能跑几百毫秒。如果在 JS 主线程同步跑,UI 就卡死。得让它在后台线程跑,又要能取消、能超时、能被采样。

思路: napi-rs 提供 Task trait——compute() 在 libuv 线程池的 worker 上跑,resolve() 回到 JS 主线程。task.rs 把它包成一个极简的 blocking(tag, cancel_token, work)(task.rs:209),返回值直接就是 JS 侧的 Promise<T>

原理演示(示意,非源码):

// 一个 #[napi] 函数怎么用这套调度
#[napi]
fn glob(options: GlobOptions) -> Promise<GlobResult> {
let ct = CancelToken::new(options.timeout_ms, options.signal); // 超时 + AbortSignal
task::blocking("glob", ct, move |ct| { // 这个闭包在 libuv worker 上跑
// ... 重活 ...
ct.heartbeat()?; // 定期问:该停了吗?
Ok(result)
}) // 立刻返回 Promise,主线程不阻塞
}

取消是协作式的,不是抢占式的。 CancelToken(task.rs:87)统一两个取消源:JS 的 AbortSignal 和毫秒超时。工作方必须在循环里周期性调 heartbeat()(task.rs:102),它在被中止时返回 Err。遍历器的惯例是每 128 个条目探一次(见 fs_cache.rsEntryVisitor::visit),既不漏检也不让 heartbeat 本身变成热点。

两种调度,别混用:

场景用哪个跑在哪
CPU 密集 / 阻塞 syscall(grep、walk、token 计数)task::blocking(task.rs:209)libuv 线程池 worker
需要 .await 的异步 I/O(shell、进程、ISO)task::future(task.rs:245)Tokio 运行时

always-on 采样(巧妙): compute() 一进门就挂一个 profile_region(tag) 守卫(Blocking::compute,task.rs:169;prof::profile_region,prof.rs:107)。采样持续写进一个固定大小的环形缓冲区,get_work_profile(last_seconds)(prof.rs:225)随时取最近 N 秒——生产环境常开的火焰图,零配置。

3.3 进程内文件发现:一个遍历器 + 一个 TTL 缓存,喂饱 glob/fd/workspace

要解决的小问题: glob、模糊找文件、启动扫工作区,底层都是"遵守 gitignore 地走一遍目录树"。走一次不便宜,而且短时间内经常重复走同一棵树。

思路: 共用一套并行遍历器(ignore crate 的 WalkBuilder,build_walker,fs_cache.rs:246),外面套一层 TTL 缓存。

遍历器统一约定: 总是跳过 .git;默认跳过 node_modules(除非模式显式提到);尊重 .gitignore/.git/exclude/全局 ignore,且即便目录不是 git 仓库也认 .gitignore(require_git(false))。并行度由 PI_GREP_WORKERS 控(默认 4)。

缓存的键是什么(一个要澄清的点): 缓存不是按文件 mtime 键控的,而是按 (root + 扫描选项) 键控、按存活时长(TTL) 判新鲜——

CacheKey = { root, include_hidden, use_gitignore, skip_node_modules, detail }
新鲜判定: now - created_at < TTL(默认 1000ms) ← 靠时间,不是靠 mtime

GlobMatch 里确实带每个文件的 mtime 字段(fs_cache.rs:46),但那是给"按修改时间排序"用的,不是缓存键。get_or_scan(fs_cache.rs:451)命中未过期就返回克隆;过期就重扫并 LRU 淘汰(上限 FS_SCAN_CACHE_MAX_ENTRIES,默认 16)。

空结果快速复查(巧妙,防陈旧假阴性): 你刚建了个文件立刻 glob,缓存里还是旧的空结果怎么办?get_or_scan 会连带返回缓存年龄 cache_age_ms;调用方(glob/fd)一看"这次零命中、而缓存已老过 EMPTY_RECHECK_MS(默认 200ms)",就 force_rescan(fs_cache.rs:495)再试一次才返回空(见 run_glob,glob.rs:407)。写/改/删文件后,agent 侧显式调 invalidate_fs_scan_cache(fs_cache.rs:551)按路径前缀失效相关条目。

谁用这个缓存、谁只借遍历器:

模块用 TTL 缓存吗说明
glob.rs / fd.rs(fuzzyFind)是(可选 cache: true)get_or_scan / force_rescan
workspace.rs(listWorkspace)复用遍历器,但自己一次性走完,不进缓存
grep.rs复用 build_walker,边走边流式匹配

fd 的模糊打分(细节): @ 提及补全靠子序列打分(fuzzy_subsequence_score,fd.rs:62)——查询字符按序命中即算,命中间有间隔就扣分;纯查询只比 basename,带 / 的查询才比全路径(否则 @plan 会把所有祖先目录含 plan 的文件全捞出来)。

glob 的容错(巧妙): LLM 常吐出没闭合的花括号 *.{ts,jsfix_unclosed_braces(glob_util.rs:53)不报错,而是补上缺的 } 再编译。

3.4 进程内 grep:ripgrep 拆成库来用

要解决的小问题: 内容搜索是 agent 用得最多的工具。既要 ripgrep 的速度和正确性(二进制检测、编码、上下文行),又不想 exec 一个 rg 进程、再解析它的文本输出。

思路: 直接依赖 ripgrep 的底层 crate——grep-regex(匹配器)、grep-searcher(搜索引擎)——在 Rust 里组装。grep.rs 分两层:

函数干什么
内存搜索search(grep.rs:1737)、has_match(grep.rs:1761)对已在内存里的字符串/Uint8Array 搜(zero-copy)
文件系统搜索grep(grep.rs:1806)走目录树 + glob/类型过滤 + 全局偏移/限额 + 每文件摘要

搜索器配置(细节): build_searcher(grep.rs:591)开二进制检测 BinaryDetection::quit(b'\x00')(遇 NUL 即停,不把二进制当文本喷),按需开多行、前后上下文行。

读文件分档(巧妙): read_file_bytes(grep.rs:607)按大小选策略——超过 4MiB(MAX_FILE_BYTES)直接跳过;≤128KiB 一次读进 Vec;更大的走 mmap(memmap2::Mmap,只读、搜完即 drop)。小文件避免 mmap 开销,大文件避免整体拷进堆。

并发与公平(细节): 文件系统 grep 用 ignore 的并行遍历器,每文件搜索独立;有个 per-file 上限,防一个巨热文件把全局 max_count 预算吃光、别的文件还没轮到。命中可通过 on_match 线程安全回调流式回 JS(ThreadsafeFunction)。

3.5 结构摘要与文本原语:tree-sitter / tiktoken / syntect

这几个都是"别人 exec 外部工具、这里当库用"的典型,各占一模块。

结构摘要(summarizeCode,summary.rs:76): 用 tree-sitter 把源码解析成语法树,按规则把过长的函数体/字面量/块注释折叠成"省略段",保留骨架。给的是"kept/elided 段序列",让上下文治理层(见 长程上下文治理)能把一个大文件压成结构轮廓而不是截断。它还支持 BFS 渐进展开到目标可见行数(unfold_until_lines)。真正的解析逻辑在独立 crate pi-ast(summary.rs 只是 N-API 转接壳)。

AST 结构搜索/改写(astGrep/astMatch/astEdit,ast.rs:577/738/844): 基于 ast-grep,按语法结构而非文本匹配/重写代码,支持多档匹配严格度(cst/smart/ast/relaxed/…)。

Token 计数(countTokens,tokens.rs:56):tiktoken-rs,BPE 表编进二进制、首次用时建一次编码器。默认 o200k_base(GPT-4o/o1/GPT-5 那档),也可选 cl100k_base。输入可以是单串或字符串数组;数组用 rayon 并行编码求和,一次 N-API 过界算完整个预算,不为每个元素付一次跨界成本。注释诚实标注:Anthropic 不公开分词器,这对 Claude 是 ~5–10% 的近似。

语法高亮(highlightCode,highlight.rs): 用 syntect;在自带默认语法集之外,vendor 了 Julia/Nix/Mermaid 三个 .sublime-syntax(build_syntax_set,highlight.rs:39),把 syntect scope 映射到 11 个语义类别输出 ANSI 色。

3.6 内嵌 bash:brush,一个活在进程里的 shell

要解决的小问题: agent 要跑 shell 命令。spawn("/bin/bash") 在 Windows 上就没有;而且一次一个进程,export/cd 这类会话状态跨命令不保留。

思路: 用 brush——一个 Rust 写的 bash 兼容解释器(brush-core/brush-builtins)——在进程内解析并执行 bash 脚本。crates/pi-shell 封装它,crates/pi-natives/src/shell.rs 再套 N-API 壳。

两种入口:

入口语义符号
Shell 类(有状态会话)一个跨多次 run 存活的会话:环境变量、cwd、export 都保留shell.rs:200
executeShell(一次性)每次调用起一个全新会话shell.rs:264

持久会话怎么存活(细节): Shell 内部持一个 Arc<TokioMutex<Option<ShellSessionCore>>>(pi-shell/src/shell.rs:121),ShellSessionCore 裹着一个 brush Shell。命令通过 run 在 Tokio 上异步执行(task::future),流式 stdout/stderr 经 on_chunk 回调回 JS。会话还能数活着的后台任务(live_background_job_count,shell.rs:253)——host 据此决定"这个会话还有 & 起的后台进程在跑,别把它 drop 掉"(drop 会 kill-on-drop 掉那些子进程)。

输出合流(巧妙): 子进程逐字节写(printf 进度条、token 流)会产生海量小 chunk。bridge_chunks(shell.rs:294)贪婪地把已排队的 chunk 合批,单批硬上限 64KiB(MAX_BATCH_BYTES),避免 JS 主线程被一个多 MB 的 napi 回调噎住。

这里的 fork-exec 边界: bash 解释器在进程内;但脚本里调的外部程序(gitnodepython)仍是真子进程,由 pi-shell::process 管理进程树、ps.rs 暴露 Process 句柄(kill_tree/terminate,ps.rs:116/125)。交互式命令走 PtySession(pty.rs),它用 portable_pty 拉起带伪终端的真实进程——这类故意 spawn。

3.7 工作区隔离(pi-iso):给任务一份可写副本,还不用深拷贝

要解决的小问题: 让 agent 在一份工作区上放手改,又不弄脏原始树,还不能每次都把整个仓库深拷贝一遍(慢、占空间)。

思路: 一个跨平台隔离 PAL(平台抽象层):给"只读 lower 树"生成一个"可写 merged 视图",尽量用写时复制(CoW)/reflink,只在真绕不开时才落回递归拷贝。所有后端实现同一个 trait IsolationBackend(pi-iso/src/lib.rs:225):probe(能不能用)/start(建视图)/stop(拆)/diff(算改了啥)。

八个后端,按平台原生优先:

后端机制平台
Apfsclonefile(2) 递归 reflink 整棵树(apfs.rs:77)macOS 默认
Btrfs / Zfs子卷快照 / 数据集快照+克隆Linux(+对应文件系统)
LinuxReflink逐文件 FICLONE reflink(XFS/bcachefs 等)Linux
Overlayfs内核 overlay 挂载,拒绝时退 fuse-overlayfs(overlayfs.rs:111)Linux 默认
WindowsBlockCloneFSCTL_DUPLICATE_EXTENTS_TO_FILE 块克隆Windows(NTFS/ReFS)
Projfs投影文件系统Windows 默认
Rcopylower 是 git 仓就 git worktree add,否则递归拷贝(rcopy.rs:36)全平台兜底

选后端是两段式(细节): BackendKind::native()(lib.rs:106)给出本构建目标的默认;resolve()(lib.rs:338)按 auto_order()(lib.rs:289)逐个 probe,挑第一个"主机级可用"的,并把整条候选链回传,好让调用方在某后端对具体路径 start 失败(跨设备 reflink、非子卷路径)时按链重试。Rcopy 永远是最后一个候选,保证有兜底

diff 统一走 git(巧妙): 不管用哪个生命周期后端建的视图,只要 merged 是 git 工作树,IsolationBackend::diff 的默认实现就委托 git diff——输出和下游 git apply 消费的字节完全一致。非 git 树(只有 Rcopy 会遇到)才走双树遍历,用 (size, mtime) 短路跳过相同文件再做内容比对。

N-API 侧(iso.rs): start/stop 是阻塞 syscall,包在 tokio::task::spawn_blocking 里给 JS 一个正常 Promise(iso_start,iso.rs:132);IsoError::Unavailable 序列化时带 ISO_UNAVAILABLE: 前缀,让 JS 侧能区分"这后端没装"和"硬失败"(iso_is_unavailable_error)。


4. 跨三平台的取舍(把差异摊开)

原生层的大半复杂度来自"同一个能力在三平台上机制完全不同"。归纳三条主线:

1. 线程与运行时:Windows 是特例。 macOS/Linux 直接用 napi-rs 默认运行时;只有 Windows 走自定义 bounded 运行时 + 加载期线程预探(§3.1),因为它内存受限时 eager 起线程会 os error 1455 直接 abort,而 Linux 上加载期起探测线程反而死锁。取舍:为一个平台的失败模式写专门代码,而不是所有平台用同一套"最坏假设"。

2. 进程模型:没有 fork 也要一致的进程树管理。 kill_tree/terminate_tree 在 Linux/macOS 转发真实信号(先子后父),Windows 没有信号抽象,signal 参数被忽略、整树 TerminateProcess 硬杀(ps.rs:116)。对上层暴露同一个 Process 接口,平台差异吞在实现里。

3. 隔离后端:能力天差地别,接口收敛成一个 trait。 每种 BackendKind每个构建里都可 dispatch;没编进当前目标的后端(Linux/macOS 上的 Apfs、非 Windows 的 Projfs)返回平台桩,probeavailable=falsestartUnavailable(pi-iso/src/lib.rs:266backend())。这样 N-API 壳可以照抄用户的 task.isolation.mode 设置,不必自己判"这平台支不支持"。

跨平台共同暗线:能力可以缺,接口不能缺。 每个平台桩都实现完整 trait 只是"永远不可用",于是上层永远面对同一张接口表。


5. 巧妙之处(可借鉴的技术)

  • 版本哨兵把静默崩溃变成可读报错。 一个名字编了版本号的空导出函数(pi_natives_version_sentinel,lib.rs:176),就把"锁文件更新留下旧 .node"这种 Windows 经典失败,从 <sym> is not a function 变成一句"reinstall to re-sync"。

  • 加载期绝不起线程。 崩溃处理器在 #[module_init] 装,Tokio 运行时挪到 dlopen 之后单独装(lib.rs:191 vs lib.rs:218)——绕开了"worker 线程抢加载器锁 vs 初始化线程持锁"的死锁。

  • 预探线程数、永不 panic。 Windows 上用 std::thread::spawn 先探能并存几个线程,再按数目建运行时、建不出就退化单线程(create_windows_napi_tokio_runtime,lib.rs:140)——把一个会 abort 进程的 panic 变成优雅降级。

  • 空结果快速复查防陈旧假阴性。 TTL 缓存返回年龄,零命中且缓存够老就强制重扫再返回空(glob.rs:407 + fs_cache.rs:495)——缓存加速的同时不牺牲"刚建的文件立刻能被找到"。

  • grep 按文件大小分档读。 小文件直接读、大文件 mmap、超大跳过(read_file_bytes,grep.rs:607)——用对的策略应对每一档。

  • token 计数一次过界算完数组。 数组输入 rayon 并行 + 单次返回(count_tokens,tokens.rs:56),把 N-API 跨界成本从 O(元素数) 压到 O(1)。

  • diff 永远和 git apply 字节对齐。 隔离后端不管怎么建视图,diff 都委托 git diff(pi-iso/src/lib.rs:243),消除"我算的 diff 下游打不上"的一整类 bug。


6. 边界与局限(诚实)

  • "无 fork-exec" 有明确范围。 成立于:grep/glob/fd/workspace/ast/summary/tokens/highlight,以及 bash 解释器本身。成立于:PTY 交互式命令(pty.rsportable_pty 起真实进程)、Rcopy 隔离后端(调 git worktree)、以及内嵌 bash 脚本里调用的任何外部程序(git/node/… 仍是子进程)。本章不把这些说成"进程内"。

  • 缓存是 TTL 的,不是内容感知的。 fs_cache 按"存活 <1s"判新鲜,不比对文件 mtime/hash;并发写场景靠调用方显式 invalidate_fs_scan_cache 兜底,否则最长 1s 窗口内可能看到旧扫描。这是速度换强一致性的自觉取舍。

  • grep 有硬上限。 单文件 >4MiB 直接跳过(MAX_FILE_BYTES,grep.rs:38);二进制文件遇 NUL 即停。这些对"搜代码"是对的默认,但不是通用 grep。

  • 平台后端能力不齐。 隔离后端在缺乏对应文件系统/权限时会 probe 失败并逐级回退,最坏落到全量 Rcopy——正确但最慢、最占空间。跨设备 reflink、非子卷 btrfs 路径等只有在 start 时才暴露不可用,需调用方按候选链重试。

  • 分词只是近似。 对 Claude 用的是 OpenAI 的 o200k_base,官方注明 ~5–10% 误差(tokens.rs 模块注释);预算估算够用,精确计费不行。

  • 本章不含工具的 agent 层封装。 这些原生函数如何被包装成 agent 可调用的工具、参数校验、:// 内部 URL 等,属于 工具宇宙;编辑语言见 hashline


7. 横向对比(同 shelf 兄弟)

多数编码 agent(shelf 内其它子库)把搜索/执行外包给系统命令行工具或 Node 生态包(ripgrep 二进制、fast-globnode-ptytiktoken 的 WASM/Python 绑定),靠 child_process 或胶水层拼起来。oh-my-pi 的取舍是把这整条外包链内化成一个自控的原生 crate 集:换来更低延迟、更强的取消/超时/采样控制、以及"不依赖用户机器装了什么"的跨平台一致性;代价是要维护一层庞大的 Rust + 三平台构建矩阵 + N-API 绑定契约。想读它上层怎么被用,回到 架构总览 的阅读地图。


8. 代码地图(导航索引)

主题文件关键符号
N-API 导出聚合 / 模块注册crates/pi-natives/src/lib.rspi_natives_version_sentinelinstall_native_crash_handleromp_install_tokio_runtime
Windows 运行时预探crates/pi-natives/src/lib.rscreate_windows_napi_tokio_runtimeprobe_spawnable_workersdesired_worker_threads
JS 加载器 / 版本哨兵 / 变体选择packages/natives/native/loader-state.jsloadNativevalidateLoadedBindingsselectCpuVariantinstallNativeTokioRuntime
生成的原生导出表面packages/natives/native/index.jsloadNative(surface export const 块)
阻塞任务调度 / 取消 / 采样crates/pi-natives/src/task.rsblockingfutureCancelTokenAbortTokenBlocking::compute
常开环形采样crates/pi-natives/src/prof.rsprofile_regionget_work_profile
遍历器 + TTL 扫描缓存crates/pi-natives/src/fs_cache.rsbuild_walkerget_or_scanforce_rescaninvalidate_fs_scan_cacheGlobMatch
glob 匹配crates/pi-natives/src/glob.rsglobrun_globcollect_sorted_matches_uncachedpush_bounded_match
glob 模式规整 / 容错crates/pi-natives/src/glob_util.rsbuild_glob_patterncompile_globfix_unclosed_braces
模糊找文件(@ 提及)crates/pi-natives/src/fd.rsfuzzy_findfuzzy_subsequence_scorescore_fuzzy_path
启动扫工作区 + AGENTS.md 发现crates/pi-natives/src/workspace.rslist_workspacecollect_agents_md_in_directoryEXCLUDED_DIRS
进程内 ripgrepcrates/pi-natives/src/grep.rsgrepsearchhas_matchbuild_searcherread_file_bytes
tree-sitter 结构摘要crates/pi-natives/src/summary.rscrates/pi-astsummarize_codepi_ast::summary::summarize_code
ast-grep 结构搜索/改写crates/pi-natives/src/ast.rsast_grepast_matchast_edit
BPE token 计数crates/pi-natives/src/tokens.rscount_tokensEncoding
语法高亮crates/pi-natives/src/highlight.rsbuild_syntax_setEXTRA_SYNTAXES
内嵌 bash(napi 壳)crates/pi-natives/src/shell.rsShellShell::runexecute_shellbridge_chunks
内嵌 bash(核心)crates/pi-shell/src/shell.rscrates/pi-shell/src/lib.rsShellexecute_shellShellSessionCore
进程树管理crates/pi-natives/src/ps.rscrates/pi-shell/src/process.rsProcesskill_treeterminatekill_process_group
PTY 交互式执行crates/pi-natives/src/pty.rsPtySession(portable_pty)
工作区隔离(napi 壳)crates/pi-natives/src/iso.rsiso_startiso_stopiso_diffiso_resolveiso_probe
隔离 PAL / 后端选择crates/pi-iso/src/lib.rsIsolationBackendbackendresolveauto_orderBackendKind::native
隔离后端实现crates/pi-iso/src/{apfs,overlayfs,rcopy,projfs,...}.rsapfs::start(clonefile)、overlayfs::startrcopy::start(git worktree)