Skip to content

fix(ai/agent): pager 包裹不再吞掉 heredoc 结束符导致 agent 卡死 - #326

Open
Chiiz0 wants to merge 1 commit into
zerx-lab:mainfrom
Infinimesh-ai:upstream/fix-heredoc-pager-hang
Open

fix(ai/agent): pager 包裹不再吞掉 heredoc 结束符导致 agent 卡死#326
Chiiz0 wants to merge 1 commit into
zerx-lab:mainfrom
Infinimesh-ai:upstream/fix-heredoc-pager-hang

Conversation

@Chiiz0

@Chiiz0 Chiiz0 commented Jul 27, 2026

Copy link
Copy Markdown

Description

修复 Agent 执行含 heredoc 的多行命令时整个会话卡死的问题。

turn_off_pager_for_command 用裸文本拼接把闭合 token 粘到命令最后一个字符后面。命令最后一行是 heredoc 结束符时(agent 写/改文件的常见写法),拼出来是这样:

(unset PAGER GIT_PAGER MANPAGER; export PAGER=cat …; python3 - <<'PY'
from pathlib import Path
…
PY)

heredoc 结束符只有独占一行时才被识别。PY) 不匹配,shell 于是永远停在 PS2 续行提示符上等那一行 PY

连锁反应:

  • 命令永不结束 → precmd 不触发 → block 停在非 finished 状态
  • ActionResultDelay::UntilCompletion 一路挂到 MAX_UNTIL_COMPLETION_DURATION(30 分钟)
  • 期间 active_block().is_active_and_long_running() 恒为真,RequestCommandOutput 全部短路成 CancelledBeforeExecution

用户看到的就是整个 agent 会话死掉。Ctrl-C 救不回来——此时 shell 在读续行输入,^C 只取消当前输入行,( 仍未闭合。输出最后一行是 ^C 就是这个 bug 的指纹。

不止 heredoc:命令最后一行以 # 注释结尾时闭合 token 被注释掉,属于同一类破绽。四个 shell 分支(bash/zsh 的 )、fish 的 end、pwsh 的 })形状相同,PowerShell here-string(@''@)也有同样的行位置要求。

改动

让闭合 token 独占一行,只对本身已经是多行的命令

// 多行
format!("({prelude}\n{command}\n)")
// 单行:字节不变
format!("({prelude}; {command})")

单行路径刻意保持字节级不变。把单行命令变成两行,在不支持 bracketed paste 的 shell(如 macOS 自带 bash 3.2)上会被 bytes_to_execute_command\n\r 替换拆成多个 block,而 agent mode 假定一条命令一个 block。多行命令在那些 shell 上本来就已经被拆分,本改动不会让情况变差。

装饰逻辑从 &self 方法抽成自由函数 wrap_command_without_pager(Option<ShellType>, &str) 以便单测,原方法退化为一行委托。

已知未修

单行命令以 # 注释结尾时(git log # 看看历史)仍会吞掉闭合 token,同样挂死。

不修的理由:修它需要让单行命令也走多行形态,会把上面那个 bracketed paste 拆块的风险扩散到所有被装饰的命令、所有 shell 上——把一个罕见故障换成一个面很广的回归风险,取舍上不划算。

而且这个组合实际上基本不可达:装饰只在 wait_until_completion=true 时触发,一条单行命令在这条路径上还要以尾随注释结尾,我们没见过。相比之下多行 heredoc 是 agent 写文件的常规写法,这次就是这么撞上的。

上游情况

同一个 bug 在 warpdotdev/warp master 上同样存在(三个 shell 分支全中,只是触发条件更窄:要 uses_pager=truewait_until_completion=true)。已向 warp 提 issue:warpdotdev#14324

本改动已先在 Infinimesh-ai/zap 验证合入(Infinimesh-ai#7),现回流到本仓库。

Testing

新增 4 个单测(shell_command_tests.rs):

  • multiline_heredoc_keeps_delimiter_and_closer_on_their_own_lines — heredoc 结束符与闭合括号各自独占一行,回归锁
  • multiline_trailing_comment_does_not_swallow_closer — 覆盖 bash/fish/pwsh 三个分支
  • single_line_command_stays_on_one_line — 钉死单行不换行
  • unknown_shell_passes_command_through

另外在 bash 5 上直接验证了 shell 层行为(脱离 Zap):

# 修复前:闭合括号粘在结束符后面
$ printf "(export PAGER=cat; python3 - <<'PY'\nprint('ok')\nPY)\n" > /tmp/before.sh
$ bash /tmp/before.sh
/tmp/before.sh: line 3: warning: here-document at line 1 delimited by end-of-file (wanted `PY')
/tmp/before.sh: line 4: syntax error: unexpected end of file from `(' command on line 1

# 修复后:闭合括号独占一行
$ printf "(export PAGER=cat; python3 - <<'PY'\nimport os; print('PAGER=' + os.environ['PAGER'])\nPY\n)\n" > /tmp/after.sh
$ bash /tmp/after.sh
PAGER=cat

写进脚本时 EOF 会终止 heredoc,所以报语法错误;PTY 里 stdin 不会 EOF,所以真实症状是无限挂起而非报错。退出码透传也验证过(子壳里 sys.exit(7)$? == 7),pager 抑制仍生效。

  • 聚焦测试 ai::blocklist::action_model::execute::shell_command:10/10 通过
  • cargo fmt --check:改动文件干净
  • 全量 nextest 基线 diff:见下

cargo nextest run --no-fail-fast -p warp,同一台机器上分别跑本仓库 main5d874456)和本分支:

总数 通过 失败 跳过
main 基线 3695 3615 80 10
本分支 3699 3619 80 10
  • 失败测试名称集合逐条比对完全一致(80 → 80,新增 0、消失 0),80 个均为 main 上的既有失败
  • 新增的 4 个测试全部通过(+4 总数、+4 通过)

注:AGENTS.md 给的是 --workspace 范围,但那会带上 integration crate 的 ui_tests,在有桌面的机器上会真的拉起大量 Zap GUI 窗口。本改动完全落在 warp 包内,因此用 -p warp 做基线 diff。

Server API dependencies

无。

Agent Mode

  • Zap Agent Mode - This PR was created via Zap AI Agent Mode

Changelog Entries for Stable

CHANGELOG-BUG-FIX: 修复 Agent 执行以 heredoc 结尾的多行命令时,shell 卡在续行提示符导致整个会话挂死的问题。

`turn_off_pager_for_command` 用裸文本拼接把闭合 token 粘到命令最后一个字符后面。
命令最后一行是 heredoc 结束符时(agent 写文件的常见写法 `python3 - <<'PY' … PY`),
拼出来是 `PY)` —— heredoc 结束符要求独占一行,shell 于是永远停在 PS2 续行提示符上
等那一行 `PY`。

后果:命令永不结束 → precmd 不触发 → block 停在非 finished 状态,
`ActionResultDelay::UntilCompletion` 一路挂到 `MAX_UNTIL_COMPLETION_DURATION`(30 分钟),
期间 `is_active_and_long_running()` 守卫把 agent 后续所有命令打成
`CancelledBeforeExecution`,表现为整个 agent 会话卡死。Ctrl-C 救不回来:此时 shell 在读
续行输入,`^C` 只取消当前输入行,`(` 仍未闭合 —— 输出尾部的 `^C` 就是这个 bug 的指纹。
以尾随 `#` 注释结尾的命令是同一类破绽(闭合 token 被注释掉)。

修复:让闭合 token(`)` / `end` / `}`)独占一行,四个 shell 分支都改。

只在命令本身已经是多行时才切多行形态。单行命令保持原有单行字节形态 —— 在不支持
bracketed paste 的 shell(如 macOS 自带 bash 3.2)上,`bytes_to_execute_command` 会把
`\n` 换成 `\r`,给单行命令加换行会把一条命令拆成多个 block,而 agent mode 假定一条命令
一个 block。多行命令在那些 shell 上本来就已经被拆分,本改动不会让情况变差。

装饰逻辑从 `&self` 方法抽成自由函数 `wrap_command_without_pager` 以便单测。

已知未修:单行命令以尾随 `#` 注释结尾时仍会吞掉闭合 token。修它需要让单行命令也走多行
形态,会把上述 bracketed paste 拆块风险扩散到所有被装饰的命令,取舍上不划算;且该组合
在实际 agent 输出中基本不出现。

先在 #7 验证合入,现回流到本仓库。

Co-authored-by: Chiiz0 <248785316+Chiiz0@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
jwp2987 pushed a commit to jwp2987/phosphor that referenced this pull request Jul 29, 2026
turn_off_pager_for_command's no-pager wrapper appended its suffix directly
after the command on one line. For multi-line commands ending in a heredoc
(python3 <<'PY' ... PY), that glued the closing delimiter to the pager
suffix (e.g. "PY)"), which bash no longer recognizes as the terminator —
the shell hangs waiting for a delimiter that will never arrive.

Replace the inline per-shell match with wrap_command_without_pager, which
detects multi-line commands and puts the closing token (`)` / `end` / `}`)
on its own line so the heredoc delimiter stays intact.

Ports upstream zerx-lab#326.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant