Bash 输出太长时,完整结果去了哪里
追踪 BashOperations、OutputAccumulator 与 truncateTail,说明流式输出如何限内存、何时落临时文件,以及超时和 abort 如何保留已产生的尾部。
运行 npm test 时,最后几十行通常比开头更有价值:失败堆栈、退出码、汇总都在尾部。但把几兆字节 stdout/stderr 原样塞进一次 tool result,会同时挤占进程内存、终端渲染和模型上下文。Pi 的选择不是简单 slice(-50000)。它一边接收字节,一边维护可显示尾部;真正越界后,再把完整原始流转存到临时文件。
默认限制是 2000 行与 50KB,任一先达到就截断。通用 truncateHead() 适合读文件开头,Bash 使用 truncateTail() 保留结尾;后者在“最后一整行本身超过 50KB”时允许返回部分行,并用 lastLinePartial 明示这个例外。
Accumulator 同时记总量和显示窗口
OutputAccumulator 持有 streaming TextDecoder、原始 chunks、已解码尾部、行数和字节数。append(Buffer) 用 { stream: true } 解码,避免一个 UTF-8 字符被 chunk 边界切开时产生替换字符;显示尾部超过滚动上限后会裁掉较早内容,同时调整到合法 UTF-8 起点。总行数与总字节数单独累计,因此即使旧显示内容被丢掉,截断提示仍能报告原始规模。
在未越界前,原始 chunks 暂存在内存。第一次判断 totalRawBytes、totalDecodedBytes 或 totalLines 超限时,ensureTempFile() 创建形如 /tmp/pi-output-<random>.log 的文件,先补写此前 chunks,再把后续 Buffer 直接写入 stream。这一点很关键:Pi 保存的是收到的原始字节,不是已被 tail trimming 处理的字符串。
flowchart LR
accTitle: Bash 输出的双通道保存
accDescr: stdout 与 stderr chunk 进入同一个 accumulator;有界 tail 用于 partial update 和最终 tool result,越界后完整原始字节写入临时日志。
PROC["child process stdout + stderr"] --> APPEND["OutputAccumulator.append"]
APPEND --> TAIL["bounded decoded tail"]
APPEND --> COUNT["total lines + bytes"]
COUNT -->|"over limit"| FILE["temporary full-output log"]
TAIL --> UPDATE["throttled partial update"]
TAIL --> RESULT["final tool result"]
FILE --> META["fullOutputPath"]
META --> RESULT
图里 stdout 与 stderr 汇入同一个 accumulator,是因为本地 operations 对两个流都调用传入的 onData。这保留进程交付给 Node 回调的大体到达顺序,却不是分别可查询的 stdout/stderr 档案。若调用方需要区分通道,必须替换 BashOperations 或设计新的 tool result,不能从临时日志反推每个字节来自哪条 fd。
Partial update 与最终结果不是同一快照
Bash executor 收到 chunk 后 output.append(data),再按节流间隔安排 onUpdate。partial snapshot 不要求持久化临时文件;最终 finishOutput() 才停止接收、flush decoder、发最后一次 update,并调用 snapshot({ persistIfTruncated: true }),随后等待临时 stream 关闭。这样最终结果若给出路径,文件已经完成写入,而不是仍在后台 flush。
最终文本有三种提示:按行截断时写显示区间;按字节截断时额外写 50KB limit;最后一行过长时写该行完整大小。结构化 details 同时携带 truncation 与 fullOutputPath,所以 TUI 可以渲染提示,SDK 调用方也不必解析英文文本。不过模型主要看到的仍是 content 文本,路径因此也被写进提示。
可以用一条不会执行 Pi extension 的静态检查核对阈值和保存点:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" grep -n 'DEFAULT_MAX_LINES\|DEFAULT_MAX_BYTES' v0.83.0 -- \
packages/coding-agent/src/core/tools/truncate.ts
git -C "$repo" grep -n 'persistIfTruncated: true\|Full output:' v0.83.0 -- \
packages/coding-agent/src/core/tools/bash.ts
这里不把命令输出写成固定行号快照,是为了让读者在同一个 tag 内直接看到定义与消费点。章节旁的 SourceEvidence 才是本册锁定的行号合同。
失败、超时和 abort 仍要先收尾输出
ops.exec() 抛出 aborted 或 timeout:<seconds> 时,executor 先 finishOutput(),再把已经产生的尾部附到错误信息。非零退出码也先完成相同收尾,再抛出包含输出和 exit code 的错误。于是“tool call 失败”不等于“输出被扔掉”;失败前的诊断仍能进入 error result,超长时也有完整日志路径。
仍要保留两个运维边界。第一,临时文件没有在这条执行链里自动删除;它位于操作系统 temp dir,可能包含密钥、构建日志或用户数据。展示 fullOutputPath 是恢复完整证据的便利,也把清理与访问控制留给宿主环境。第二,只有越界输出才保证 spill file;短输出只存在于 tool result 和会话后续持久化路径,不能预期每次 Bash 都有独立日志文件。
OutputAccumulator 解决的是“一条已获准执行的命令产生太多输出”。它不决定命令是否可信,也不发现 AGENTS.md、Skills 或扩展。下一章回到运行前资源层,解释这些文本与模板从哪些目录进入 system prompt,以及同名资源冲突时谁先留下。