Files
NetMesh/docs/research/grok-build-terminal-context-comparison.md
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

23 KiB
Raw Blame History

Grok Build 终端上下文工程研究:给 Catty 的对照结论

研究范围与结论口径

  • Grok Build 本地源码:/Users/chenqi/.codex/external-sources/grok-build
  • 仓库当前提交:8adf9013a0929e5c7f1d4e849492d2387837a28d
  • 仓库内 SOURCE_REV2ec0f0c8488842da03a71eeee3c61154957ca919
  • Catty 源码:本报告所在 Netcatty 工作树
  • 已验证:可直接由源码或本地只读实跑证明。
  • 推断:由多处实现拼合出的行为判断,尚未做完整产品级端到端复现。

一句话结论:Grok 最值得 Catty 学的不是“多截一点输出”而是把终端分成三层原始输出单独保存、给模型的内容严格限额、持续日志进入对话前先做限流和抑制。Catty 的增量轮询和远程编码处理其实更好,但当前有两个入口能绕过限额,另有一个入口会稳定累积重复输出;这三个确定性问题都足以直接塞满上下文,必须先修。

一、Grok 的终端输出怎样流动

命令进程
  ├─ 原始 stdout/stderr ──> session/terminal/<tool_call_id>.log
  │                           ├─ 运行期最多 5 GiB超过则杀进程
  │                           └─ 退出后只留文件前 64 MiB
  ├─ 模型结果内存 ────────> 默认约 20k 字符:前半冻结 + 后半滚动
  │                           └─ 去 ANSI、超长行每 2000 字符软换行
  └─ 后台读取 ────────────> get_task_output
                              └─ 超限时只回前 2k 预览 + 文件路径

交互 PTY 是另一条通道256 KiB 环形缓冲 + offset + base64 推送,
不会自动把整个终端画面塞进模型对话。

1. 原始输出与模型输出分开

  • 已验证:每次 Bash 调用都会把输出写入会话目录下的 terminal/<tool_call_id>.log,而不是只留在对话消息里。bash/mod.rs:1915
  • 已验证:模型侧默认终端预算为 20,000 字符;超出后冻结前半、滚动保留后半,再插入截断提示。bash/mod.rs:156 terminal.rs:346
  • 已验证:原始字节转模型文本时用 UTF-8 容错解码;非法字节会被替换,因此二进制输出和错误编码并不是无损的。terminal.rs:308
  • 已验证:模型看到的文本会去掉 ANSI 控制码,超长单行按 2,000 字符软换行;磁盘文件仍保存原始字节。bash/mod.rs:406 output.rs:413
  • 已验证stdout 与 stderr 被收进同一个输出流和文件;每次轮询先读完当前 stdout再读 stderr所以二者的相对时间顺序并不精确。terminal.rs:1528

这套设计最重要的价值是:模型消息只是有界索引,原始输出是独立资产。 Catty 目前成功命令也有类似“预览 + handle”但全文仍在渲染进程内存中没有磁盘配额、TTL 或范围读取。

2. 长任务和失控输出有多层保险

  • 已验证:前台可自动转后台的命令默认最多阻塞当前轮 15 秒;后台任务最长运行 10 小时。terminal.rs:47
  • 已验证:运行中输出文件默认最多 5 GiB超出会终止进程进程结束后文件被 set_len(64 MiB)terminal.rs:65 terminal.rs:382 terminal.rs:1236
  • 已验证:退出后仍会最多等待 2 秒排空管道,避免子进程继承管道导致永久卡住。terminal.rs:77
  • 已验证:完成的后台任务在内存中保留 5 分钟,之后只保留最多 100 条轻量元数据;输出仍指向磁盘文件。terminal.rs:42 terminal.rs:1432
  • 已验证:会话文件默认 30 天后清理,可配置;当前会话目录会跳过。persistence.rs:2600

需要注意两个不能照搬的点:

  1. 5 GiB 对桌面应用太大Catty 应采用更小的默认值和全局总配额。
  2. Grok 提示模型“完整输出在文件”,但退出后只保留文件开头 64 MiB因此对更大输出这句话不准确末尾错误甚至可能丢失。

3. 后台读取并不是真正的增量读取

  • 已验证get_task_output 每次读取当前完整快照;若超过预算,只回前 2,000 字符预览并给出文件路径。task_output/tool.rs:11
  • 已验证:多任务读取允许最多 20 个任务,各自独立套用默认约 40 KiB 上限,没有统一总预算。task.rs:318 task_output/mod.rs:205
  • 推断:一次多任务调用最坏仍可能向上下文注入约 800 KiB而且重复轮询会把相同预览反复写进历史。Grok 的模型读取没有 Catty 的 nextOffset 语义。

因此,Catty 的 terminal_start / terminal_poll 设计更好:它有 256 KiB 滚动尾窗、outputBaseOffsettotalOutputCharsoutputTruncatednextOffset,并按聊天会话隔离任务。aiExec.cjs:16 aiExec.cjs:165 aiExec.cjs:196

Catty 当前的问题不是底层没有增量,而是上层没有利用好它:相同 offset 的重复 poll 不会被历史裁剪SessionState 也不记录 jobId、nextOffset 和已读位置。由于该问题已实测可在一次短循环内稳定累积近 50k 字符,本文将它列为 P0而不是一般优化。

4. 持续日志先限流,再进入对话

Grok 为 monitor 单独做了一条“事件入口”,这是最值得 Catty 借鉴的部分:

  • 已验证:单行最多 500 字符,一个事件批次最多 3,000 字符,原始行缓冲最多 1 MiB200ms 合批。monitor/types.rs:1
  • 已验证:令牌桶初始允许 10 个事件,之后每 2 秒补 1 个;被抑制的事件只累计数量,恢复时发一条摘要。rate_limiter.rs:5
  • 已验证:持续 30 秒过载后停止向对话发送该监控流,并提示改写为更精确的 grep/awk 过滤。rate_limiter.rs:121
  • 推断:该“自动停止”只中断事件管线,没有在同一路径调用终端 kill底层命令可能继续运行到超时或会话清理。因此 Catty 借鉴时应同时停止进程或明确标为“仅静音”。monitor/tool.rs:321 monitor/tool.rs:360

对于 Catty正确做法不是让每个终端刷新都成为一条消息而是把它们变成时间窗口内合批 -> 单行/单批上限 -> 总速率限制 -> 被抑制数量摘要 -> 必要时停流/停进程

5. 交互终端和模型上下文彻底分开

这一边界对 Catty 尤其重要:用户看到的终端画面可以高频、完整;模型读取必须显式、有界、可增量、可审计。 不应把终端 UI 的 scrollback 直接当模型记忆。

二、上下文裁剪、压缩与恢复

1. 普通旧工具结果先做便宜裁剪

  • 已验证Grok 在上下文使用超过 50% 后,构造请求时裁剪旧工具结果;最近 3 轮不动,超过 4,000 字符的旧结果保留 1,500 头 + 1,500 尾10 轮前直接替换为占位。request_builder.rs:155 types.rs:67
  • 已验证:这一步发生在请求副本上,不会先破坏完整会话记录;更深层的 compaction 另有归档与重建流程。request_builder.rs:20 compaction.rs:219

Catty 已经会在预算压力下按终端 session 只保留最近两次成功的 terminal_execute,方向正确。staleContextPruner.ts:184 但它没有覆盖 terminal_pollterminal_read_context,所以 6 次相同 offset 的本地实跑仍累计约 49.6k 字符且未触发调整。

2. 压缩后要恢复“可继续工作”的状态,而不是恢复大段日志

  • 已验证Grok compaction 后会重新注入正在运行的终端任务、子代理、已编辑文件、待办和工具状态,而不是重新塞入任务输出。compaction.rs:1205
  • 推断:它注入运行任务 ID/命令/状态,但没有“模型上次读到哪个 offset”加上 get_task_output 是全快照,压缩后仍可能重复读取。
  • 推断:终端任务表是进程内 Map新建 actor 时为空,未发现从磁盘恢复活进程的路径。重启后旧日志文件可能还在,但旧 taskId 不能继续作为真实运行任务读取。terminal.rs:543

Catty 应保存的是结构化任务续读状态:jobId + sessionId + command + status + nextOffset + outputBaseOffset + handleId + sideEffectClass。恢复时先向后端核实任务是否仍存在,再注入“仍运行 / 已完成 / 已失联”,不能假装恢复,也不能建议盲目重跑。

三、Catty 当前最危险的三个确定性漏洞

P0-1失败或超时输出绕过终端压缩

  • 已验证:工具执行器把失败命令的完整 stdout/stderr 拼进 error。toolExecutors.ts:96
  • 已验证capabilityTools 对失败结果直接返回,只有成功结果进入 fitTerminalExecuteResultForModelcapabilityTools.ts:328
  • 已验证:前台 PTY 默认 maxBufferedChars=0,而 helper 将 0 解释为不限长度。ptyExec.cjs:27 ptyExecHelpers.cjs:277
  • 实跑已验证:构造约 200k 字符后超时,进入模型的 error 长度为 200,035handle 数为 0同等大小的成功输出会被截断并生成 handle。

这意味着最容易产生巨量日志的“失败、超时、构建报错”恰好绕过保护。修复标准必须是:无论成功、失败、超时、取消、桥接异常,所有 stdout/stderr 都先经过同一个终端输出封装器,模型只拿有界预览。

P0-2tool_output_read 可以把全文重新灌回上下文

  • 已验证handle 默认读 12k但调用方可传任意 maxCharsfull 模式只是从头截到调用值,没有硬上限。toolOutputStore.ts:67 toolInputs.cjs:345
  • 已验证tool_output_read 被明确排除在二次 fitting 之外,因此模型可以用一个巨大 maxChars 将原文一次性拉回。capabilityTools.ts:126

修复标准:服务端写死单次硬上限,支持 offset/range/search/head/tail,返回 nextOffset/hasMore/totalChars;每一轮所有 handle 读取还要共享总预算,不能只限制单次调用。

P0-3重复 poll/read_context 不参与终端历史去重

  • 已验证:跨轮历史省略只识别 terminal_execute 的几个别名,不覆盖 terminal_pollterminal_read_contextcattyHistoryReplay.ts:112
  • 已验证:预算压力下的终端特化裁剪同样只识别 terminal_executestaleContextPruner.ts:98
  • 实跑已验证:连续 6 次使用相同 offset 读取约 8k 内容,预算压力下 didAdjust=false,约 49.6k 重复文本全部保留。

修复标准:把每次读取标识为 chatSessionId + sessionId/jobId + requestedOffset + nextOffset + snapshotVersion;完全相同的读取只保留一次,后续增量可合并为一个范围。terminal_read_context 当前的行号会随 scrollback 淘汰或 reflow 漂移,不能假装成可靠日志 cursor需要单独的输出版本或快照 ID。

四、逐项对比

维度 Grok Build Catty 现状 判断
成功命令预览 默认约 20k头尾 stdout 24k / stderr 12k重复行压缩、头尾 + handle Catty 不弱
失败/超时输出 同一终端结果路径,仍受预算约束 绕过 fit可无上限 Catty P0
原文保存 会话磁盘文件,有运行/完成/TTL上限 JS 内存全文,无总量/TTL/LRU Grok 更完整
后台读取 全快照,超限前 2k + 文件;无 offset nextOffset 增量 + 256 KiB 尾窗 Catty 更好
重复读取去重 无真正 cursor可能重复 底层有 cursor上下文层不按 job+offset 去重 Catty 应补上层
handle 读取 通过有界工具读文件 head/tail/fullmaxChars 无硬上限 Catty P0
ANSI/进度条 模型预览去 ANSImonitor 合批限流 PTY normalize 去 ANSI/CR但各工具路径不统一 Grok 更一致
UTF-8/远程编码 UTF-8 lossy Stateful decoder支持 GBK/GB18030 分块 Catty 更强
持续日志 monitor 专用入口:批次、速率、抑制 主要依赖模型谨慎 poll Grok 值得学
交互 PTY 与模型工具分离256 KiB 环形缓冲 UI 终端与 AI 能力分层,但 read_context 仍是屏幕行语义 原则一致
历史裁剪 通用旧 tool result 分层裁剪 terminal_execute 特化poll/read_context 未覆盖 Grok 更完整
压缩后任务状态 注入运行任务,但无已读 offset SessionState 只记 lastCommand 两边都有缺口
敏感信息 未发现通用终端 secret redaction 未发现catalog 还标为 sensitiveRead:false 两边都缺
会话/跨聊天隔离 任务有 owner session 后台 job 按 chatSessionId 校验 两边都有基础

五、建议实施顺序

P0先堵住能直接撑爆上下文或泄密的入口

  1. 统一终端输出封装:成功、失败、超时、取消、异常全部走同一预算器;先保存原文,再只回结构化预览。
  2. 锁死 tool_output_read:单次和单轮双重硬上限;增加 offset/range/search禁止调用参数放大服务端上限。
  3. 让 poll/read_context 真正去重:相同范围不得重复进入历史;只保存已读区间、摘要和下次 offset。
  4. 终端敏感信息处理:在进入模型前识别并遮罩常见 token、私钥、密码、连接串原文只留本地受控存储并明确生命周期。
  5. 写命令 413 只执行一次压缩或重试只能重放已记录结果不能重新执行有副作用的命令。历史提示也不应默认写“Re-run terminal_execute”。cattyHistoryReplay.ts:112

P1把现有增量能力变成真正的上下文能力

  1. SessionState 保存运行任务和最后已读 offsetcompaction 后先核实真实状态,再恢复续读位置。
  2. ToolOutputStore 增加每 handle、每 chat、全局总字节数、条数、TTL、LRU关闭终端和删除聊天时精确清理。
  3. 大输出从 JS heap 移到 Netcatty 专用临时目录;以不可猜 handle 访问,使用受限权限,支持字节范围读取和搜索。
  4. 统一 ANSI、CR 进度、超长单行、重复行、binary/base64/高熵文本处理;保留 encoding 与是否 lossy 的元数据。
  5. 为 follow/watch/log tail 增加 Grok monitor 式入口200ms 合批、单行/单批预算、速率限制、抑制计数和明确的停流/停进程行为。

P2长期上下文质量

  1. 通用工具历史裁剪覆盖 terminal_execute、terminal_poll、terminal_read_context 和 tool_output_read而不是按工具名漏补。
  2. 结构化保存命令 outcome命令、退出码、时间、是否有副作用、摘要、handle、任务状态旧历史删原文但保留事实。
  3. 建立离线终端上下文基准,记录峰值上下文占比、重复率、错误定位率、秘密暴露数和写命令执行次数。

六、交付前硬门槛与 13 个压测场景

以下门槛建议作为合并前必须通过的条件:

  • 写命令遇到 413WriteCount = 1
  • 并发任务串线、跨聊天读取:crossTalk = 0crossChatRead = 0
  • 模型可见内容里的测试密钥:SecretExposure = 0
  • UTF-8 分块边界不损坏Catty 原有 GBK/GB18030 能力不退化。ptyExecHelpers.cjs:8
  • 任一场景不得把上下文推到窗口的 90% 以上。

建议自动化 13 场景:

  1. 持续高速日志;
  2. 超长单行;
  3. 关键错误位于输出中段;
  4. ANSI 颜色与 \r 进度刷新;
  5. UTF-8 多字节字符跨 chunk
  6. 二进制与高熵 base64
  7. 四个并发终端;
  8. 后台任务多次轮询;
  9. 相同 offset 重复读取;
  10. 有副作用命令执行后触发 413
  11. 应用重启 / session resume
  12. 输出包含 token、密码、私钥片段
  13. handle 过期、淘汰和跨聊天访问。

每个场景至少采集进入模型字符数、原始输出字符数、压缩比、重复字符比例、峰值上下文占比、handle 数和内存/磁盘占用;涉及命令的再采集执行次数、任务归属和最终状态。

七、不建议照搬 Grok 的地方

  1. 不照搬 5 GiB 运行文件默认上限;桌面产品应更保守并有全局配额。
  2. 不照搬“完成后截文件开头 64 MiB”应保留头尾或分块索引否则真正错误常在末尾。
  3. 不照搬 get_task_output 全快照Catty 已有 offset应把它贯彻到上下文历史。
  4. 不照搬多任务各自限额却没有聚合限额。
  5. 不照搬 monitor “显示已停止但未确认进程已杀”的含糊语义。
  6. 不照搬仅 UTF-8 lossyCatty 的 stateful decoder 和远程编码支持必须保留。
  7. 不把“文件路径”直接当安全 handle路径泄露、权限和生命周期都需要单独设计。

最终判断

Catty 不需要复制 Grok 的终端系统。Catty 已经有更适合真实终端代理的底座:远程会话、增量 offset、尾窗、跨聊天隔离和多编码解码。真正需要补的是模型入口治理

任何终端字节进入模型前都必须经过同一个有界入口;任何大输出只能通过有额度、有游标、可过期的句柄读取;任何持续日志都先限流再入对话;任何压缩与恢复都保存任务事实和已读位置,而不是保存或重放大段日志。

先修三个 P0再补句柄生命周期和 monitor 式限流Catty 在“会真正操控终端”的场景里会比 Grok 当前实现更稳,也更不容易因一次失败命令把整个上下文窗口打满。