15.2 进程管理

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~130k tokens,输出 ~5k tokens(本节)


上一节我们深入分析了 exec 工具如何创建和执行进程。当一个命令因 yieldMs 到期或 background: true 而转入后台时,Agent 需要一种手段来监控、交互和终止这些进程。这就是 process 工具进程注册表的职责。


15.2.1 进程注册表

内存中的双 Map 架构

进程注册表位于 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)

sweeper 的间隔为 TTL 的 1/6(最少 30 秒)。使用 unref() 确保定时器不会阻止 Node.js 进程的正常退出。


15.2.2 process 工具

工具定义

十种动作

process 工具支持十种动作,覆盖进程管理的完整需求:

动作
用途
需要 sessionId

list

列出所有后台会话(运行中 + 已完成)

poll

获取增量输出(消费 pending 缓冲)

log

读取完整日志(支持分页)

write

向 stdin 写入数据

send-keys

发送终端按键序列

submit

发送回车键(\r

paste

粘贴文本(支持 bracketed paste)

kill

终止进程

clear

清除已完成会话记录

remove

强制移除会话(运行中则先杀死)

list — 会话列表

deriveSessionName() 从命令中提取简短名称——取第一个 token 作为"动词",第一个非选项参数作为"目标":

poll — 增量输出

polllog 的核心区别:

poll 是消费性的——每次调用后 pending 被清空;log 是只读性的——可以反复查看。

writesend-keys 的区别

两者都向进程的 stdin 写入数据,但用途不同:

write 适合管道式输入(向脚本传递数据);send-keys 适合与 TUI 程序交互(如在 vim 中按 EscCtrl-C)。


15.2.3 PTY 按键编码

encodeKeySequence

PTY 按键编码引擎位于 pty-keys.ts,支持多种输入格式:

按键 Token 语法

按键 token 使用类似 tmux/Emacs 的修饰符语法:

Token
含义
编码结果

Enter

回车

\r

Tab

制表符

\t

Escape / Esc

ESC 键

\x1b

BSpace

退格

\x7f

Up / Down / Left / Right

方向键

\x1b[A ~ \x1b[D

F1 ~ F12

功能键

VT100 序列

C-c

Ctrl-C

\x03

M-x

Alt-X

\x1b + x

S-Tab

Shift-Tab

\x1b[Z

C-M-a

Ctrl-Alt-A

\x1b + \x01

衍生解释:在 VT100/xterm 终端标准中,特殊按键通过 **转义序列(Escape Sequence)**编码。它们以 ESC 字符(\x1b)开头,后跟 CSI(Control Sequence Introducer,即 [),再跟参数和终止字符。例如方向键 Up 编码为 \x1b[A,Delete 键编码为 \x1b[3~。这些序列是终端仿真器的标准协议。

修饰符前缀的处理:

Ctrl 字符编码

Ctrl 组合键的编码遵循 ASCII 控制字符规则:

衍生解释:ASCII 表中,大写字母 A-Z 的编码是 65-90。对这些编码执行 & 0x1f(与 31 做按位与),结果恰好是 1-26,对应 ASCII 控制字符 SOH 到 SUB。这就是为什么 Ctrl-C 是 \x03(3 = 67 & 31)——这个规则来自 1960 年代的终端硬件设计,并沿用至今。

xterm 修饰符编码

对于方向键等带修饰符的组合(如 Shift-Up),使用 xterm 扩展语法:

Bracketed Paste

衍生解释:Bracketed Paste 是现代终端仿真器的一个安全特性。当用户粘贴文本时,终端会在文本前后插入特殊序列 \x1b[200~\x1b[201~。Shell(如 zsh、bash 5.1+)检测到这些序列后,知道内容是粘贴的而非用户手动输入的,因此不会立即执行——防止"粘贴攻击"(恶意网页在剪贴板中放入 rm -rf / 等命令)。OpenClaw 的 paste 动作默认使用 bracketed 模式来模拟正确的粘贴行为。


15.2.4 作用域隔离

process 工具支持 scopeKey 来实现进程隔离:

当多个 Agent 实例并行运行时,每个 Agent 只能看到和操作自己 scopeKey 下的进程。这防止了 Agent A 误杀 Agent B 的后台任务。


本节小结

  1. 进程注册表使用双 Map 架构runningSessions(运行中)和 finishedSessions(已结束),通过 markExited() 在两者之间转移

  2. 三级输出缓冲:pending(增量消费)、aggregated(滚动窗口,默认 200K 字符上限)、tail(最后 2000 字符预览)

  3. process 工具支持十种动作:list、poll、log、write、send-keys、submit、paste、kill、clear、remove

  4. PTY 按键编码支持完整的终端按键语法:修饰符前缀(C-/M-/S-)、命名键(Enter/Tab/F1-F12)、hex 字节、xterm 修饰符扩展

  5. Ctrl 字符编码利用 ASCII 位掩码规则& 0x1f),这是终端硬件时代的设计遗产

  6. Bracketed Paste 防止粘贴攻击——在粘贴文本两端加入特殊转义序列

  7. 自动清理器(sweeper) 每隔 TTL/6 扫描一次已完成会话,默认 30 分钟后淘汰

  8. scopeKey 隔离确保多 Agent 并行时互不干扰

Last updated