生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~130k tokens,输出 ~5k tokens(本节)
上一节我们深入分析了 exec 工具如何创建和执行进程。当一个命令因 yieldMs 到期或 background: true 而转入后台时,Agent 需要一种手段来监控、交互和终止这些进程。这就是 process 工具和进程注册表的职责。
进程注册表位于 bash-process-registry.ts,使用两个 Map 来管理进程生命周期:
// src/agents/bash-process-registry.ts — 核心数据结构
const runningSessions = new Map<string, ProcessSession>(); // 运行中
const finishedSessions = new Map<string, FinishedSession>(); // 已结束
ProcessSession 是运行中进程的完整状态:
interface ProcessSession {
id: string; // 会话 ID(短码,如 "a3f7bc")
command: string; // 原始命令
child?: ChildProcessWithoutNullStreams; // Node.js 子进程句柄
stdin?: SessionStdin; // stdin 写入适配器(支持 PTY)
pid?: number; // 操作系统 PID
startedAt: number; // 启动时间戳
cwd?: string; // 工作目录
// 输出管理
maxOutputChars: number; // 最大输出字符数(默认 200,000)
totalOutputChars: number; // 已产出总字符数
pendingStdout: string[]; // 待消费的 stdout 缓冲区
pendingStderr: string[]; // 待消费的 stderr 缓冲区
aggregated: string; // 全量输出(有上限截断)
tail: string; // 最后 2000 字符
truncated: boolean; // 是否因超出上限而截断
// 状态
exited: boolean;
exitCode?: number | null;
exitSignal?: NodeJS.Signals | number | null;
backgrounded: boolean; // 是否已转入后台
}
FinishedSession 是进程结束后的精简记录:
当进程产出输出时,appendOutput() 函数负责管理多级缓冲:
这个设计解决了实际问题:
pending 缓冲是 poll 操作的数据源——Agent 每次 poll 时取走 pending,然后清空
aggregated 是完整输出的滚动窗口——超出上限时丢弃开头的内容
tail 用于快速预览——列出进程时显示最后一点输出
衍生解释:这里的"滚动窗口"概念类似于日志系统中的 Ring Buffer(环形缓冲区)。当新数据写入且缓冲已满时,最旧的数据被丢弃。trimWithCap() 函数的实现很直接:text.slice(text.length - max),即只保留最后 max 个字符。
moveToFinished() 有一个关键检查:只有 backgrounded 的进程才会被保存到 finishedSessions。如果进程是同步完成的(即在 yieldMs 内返回),它的结果直接返回给 Agent,不需要注册到 finished 表。
sweeper 的间隔为 TTL 的 1/6(最少 30 秒)。使用 unref() 确保定时器不会阻止 Node.js 进程的正常退出。
15.2.2 process 工具
process 工具支持十种动作,覆盖进程管理的完整需求:
deriveSessionName() 从命令中提取简短名称——取第一个 token 作为"动词",第一个非选项参数作为"目标":
poll 与 log 的核心区别:
poll 是消费性的——每次调用后 pending 被清空;log 是只读性的——可以反复查看。
write 与 send-keys 的区别
两者都向进程的 stdin 写入数据,但用途不同:
write 适合管道式输入(向脚本传递数据);send-keys 适合与 TUI 程序交互(如在 vim 中按 Esc、Ctrl-C)。
15.2.3 PTY 按键编码
encodeKeySequence
PTY 按键编码引擎位于 pty-keys.ts,支持多种输入格式:
按键 token 使用类似 tmux/Emacs 的修饰符语法:
衍生解释:在 VT100/xterm 终端标准中,特殊按键通过 **转义序列(Escape Sequence)**编码。它们以 ESC 字符(\x1b)开头,后跟 CSI(Control Sequence Introducer,即 [),再跟参数和终止字符。例如方向键 Up 编码为 \x1b[A,Delete 键编码为 \x1b[3~。这些序列是终端仿真器的标准协议。
修饰符前缀的处理:
Ctrl 组合键的编码遵循 ASCII 控制字符规则:
衍生解释:ASCII 表中,大写字母 A-Z 的编码是 65-90。对这些编码执行 & 0x1f(与 31 做按位与),结果恰好是 1-26,对应 ASCII 控制字符 SOH 到 SUB。这就是为什么 Ctrl-C 是 \x03(3 = 67 & 31)——这个规则来自 1960 年代的终端硬件设计,并沿用至今。
对于方向键等带修饰符的组合(如 Shift-Up),使用 xterm 扩展语法:
Bracketed Paste
衍生解释:Bracketed Paste 是现代终端仿真器的一个安全特性。当用户粘贴文本时,终端会在文本前后插入特殊序列 \x1b[200~ 和 \x1b[201~。Shell(如 zsh、bash 5.1+)检测到这些序列后,知道内容是粘贴的而非用户手动输入的,因此不会立即执行——防止"粘贴攻击"(恶意网页在剪贴板中放入 rm -rf / 等命令)。OpenClaw 的 paste 动作默认使用 bracketed 模式来模拟正确的粘贴行为。
process 工具支持 scopeKey 来实现进程隔离:
当多个 Agent 实例并行运行时,每个 Agent 只能看到和操作自己 scopeKey 下的进程。这防止了 Agent A 误杀 Agent B 的后台任务。
进程注册表使用双 Map 架构:runningSessions(运行中)和 finishedSessions(已结束),通过 markExited() 在两者之间转移
三级输出缓冲:pending(增量消费)、aggregated(滚动窗口,默认 200K 字符上限)、tail(最后 2000 字符预览)
process 工具支持十种动作:list、poll、log、write、send-keys、submit、paste、kill、clear、remove
PTY 按键编码支持完整的终端按键语法:修饰符前缀(C-/M-/S-)、命名键(Enter/Tab/F1-F12)、hex 字节、xterm 修饰符扩展
Ctrl 字符编码利用 ASCII 位掩码规则(& 0x1f),这是终端硬件时代的设计遗产
Bracketed Paste 防止粘贴攻击——在粘贴文本两端加入特殊转义序列
自动清理器(sweeper) 每隔 TTL/6 扫描一次已完成会话,默认 30 分钟后淘汰
scopeKey 隔离确保多 Agent 并行时互不干扰