DeepSeek Harness bash 工具遇感叹号挂死 300 秒:最小复现与转义写法
一条 printf 'p4-start %s p4-end\n' '!',在 bash 工具里跑满 300 秒,最后返回的不是结果而是一屏 dsh> 提示符。 这条命令本身没有任何问题,在终端里秒出结果;挂死的原因是它在投递时被包成 $'...' 形式,而交互式 bash 在读行阶段就对 ! 做了历史展开,整行被丢弃(#10636)。下面按实验顺序复现并验证。
DeepSeek Harness 实验一:最小复现,只改一个字符
控制变量只有一处:命令里带不带 !。
- 先跑一条等价但不带感叹号的命令:
printf 'p4-start %s p4-end\n' 'x',正常返回。 - 再跑
printf 'p4-start %s p4-end\n' '!',挂住。 - 观察投递形态:工具层把命令包成类似下面的字符串(含 START/END 标记与
eval --)。 - 把这串内容喂给与生产同构的
bash --noprofile --norc -i,输出是:
bash: !\: event not found
dsh>
- 注意两点:START 的 printf、eval、END 的 printf 全都没执行;错误之后那个提示符前面没有完成标记。
- 结论:不是命令写错,是整行在「读行」阶段就被 shell 丢了。
DSH插件 实验二:确认问题在引用形式,不在命令
$'...' 与单引号对 ! 的处理不同:历史展开豁免单引号里的内容,不豁免 ANSI-C 引用里的内容。
- 看
quoteForBash:它转义了反斜杠、单引号、\r、\n,唯独没处理!。 - 看 shell 启动参数:
--noprofile --norc -i,是交互模式,histexpand 默认开启。 - 看初始化序列:只有
stty -echo,没有关闭历史展开。 - 于是
!后面跟着被转义的单引号,被当成事件标识符!\,查找失败后打印event not found并丢弃整行。 - 这一步把范围缩到了「引用形式」:命令内容怎么改都只是绕过,不是修复。
DeepSeek Harness 三层放大:为什么一行被丢弃会烧 300 秒
触发只是第一层,真正把 300 秒烧掉的是后两层:提示符缺完成标记,工具循环又不认这个状态。
- 层一(触发):历史展开丢弃整行,START/END 两个标记 printf 一起没了。
- 层二(放大):完成检测依赖
PROMPT_COMMAND打印OSC 133;D,但丢弃行之后 bash 重打的提示符不执行PROMPT_COMMAND,于是只有裸提示符。 - 层三(空转):工具循环的返回分支只认「END 标记出现」「会话退出」「等待原因为 stdin_read」三种情况,
inferred_idle不在其中,于是每 25ms 空转一轮,直到 300 秒 deadline。 - 超时后走 timeout 分支,执行
shells.reset()把整个 shell 杀掉。 - 现场表现:满屏
dsh>,event not found被 scrollback 上限截掉,还会出现「开头被丢弃」的提示。
DeepSeek插件 实验三:验证两种修法
一种改引用形式,一种关掉历史展开;两者都能让这条命令正常返回,但只有后者进了 master。
| 方案 | 层 | 改法 | 社区验证 |
|---|---|---|---|
| B | 层一 | $'...' 换成传统单引号引用 | 全链正常输出 |
| C | 层一 | 初始化追加 set +H | 全链正常输出 |
| D | 层三 | 连续空转且无输出增长时早退并提示 | 韧性加固 |
| E | 层二 | 完成检测不只依赖 PROMPT_COMMAND | 韧性加固 |
方案 B 要注意保留对 \r、\n 字面量的处理;方案 C 只有一行,是根因修法。D 与 E 不解决触发,但能让未来同类「行被丢弃」快速失败,而不是烧满 300 秒。
DeepSeek Harness 版本状态:master 已修,rc.3 未回移
提交 967ba4c4b8(2026-10-21)把初始化从 stty -echo 改成 stty -echo; set +H,并补了回归测试;但它不是 rc.3 tag 的祖先。
- 用
git merge-base --is-ancestor 967ba4c4b8 <rc.3 提交>可以自查包含关系。 - 社区结论:它是 master(
22ddee3)的祖先,不是dsh-v0.2.1-rc.3(1f81c04)的祖先。 - 所以在 rc.3 上仍会 100% 复现。
- 即便修复合入,层一依然没变:
quoteForBash还是只转义反斜杠、单引号、换行,set +H是唯一挡在!前面的东西。 - 换句话说,若将来换了后端或重写包装层,这个引用缺陷会立刻回来。
DSH插件 今天的规避:别让裸 ! 进命令
在修复到达你的版本之前,最稳的做法是让感叹号不经过历史展开的判断。
- 避免在命令里直接出现裸
!,用等价写法替代,比如用printf生成字面量。 - 如果必须带,确保它处在传统单引号引用里,而不是 ANSI-C 引用里。
- 不要把
set +H写在命令行首期待生效——展开发生在读行阶段,那一行还没执行。 - 长任务拆成几条短命令,万一挂住也能快速定位到具体哪一条。
- 挂住之后不要反复重试同一个字符,先改成等价写法验证链接本身是通的。
注意事项
- 别把 300 秒当成网络慢。这里的超时点是本地 shell 空转,不是请求超时。
- 别只看最后一屏输出。
event not found可能已经被 scrollback 截掉。 - 别指望恢复会话。超时会重置 shell,之前的交互状态一并丢失。
- 记录 bash 版本。社区在 macOS 自带 3.2.57 上复现;5.x 的行为未在本机验证,修复不应依赖版本。
- 别把 B 和 C 当成互相替代。一个是引用形式正确,一个是关掉展开,纵深防御应该都做。
如果你的命令里根本没有 ! 却也挂到超时,那不属于这一类,去查完成检测与 shell 初始化。
最后补一句判断技巧:同类「命令被 shell 丢弃」的故障,最终表现往往是「标记没出现 + 输出为空 + 精确卡到超时上限」,记住这个组合能省掉大量猜测。
同理,heredoc 里出现 ! 也会走同一条路径——展开发生在整行读取时,跟它是不是多行写法无关。相关排查见 DeepSeek Harness 持久终端把 PSReadLine 预测补全带进上下文?三种关闭路径与验证。
常见问题
因为 DeepSeek Harness 的命令投递层用 $'...' 包裹命令,而受控 shell 是交互式 bash,历史展开默认开启;$'...' 不豁免 !,整行被 bash 以 event not found 丢弃,完成标记也没打印,工具就一直空转到超时。
因为整行被丢弃后,DeepSeek Harness 的 START 标记 printf 也没执行,超时后工具找不到起始标记就回退到 scrollback 尾部;真正的 event not found 被 scrollback 上限截掉,只剩一串裸提示符。
不能,在命令里写 set +H 救不了 DeepSeek Harness 的这次挂死。历史展开发生在读行阶段,行内语句还没执行;跨调用设置的 set +H 又会被超时后的 shell reset 抹掉。真正有效的写法是避免裸 !。
DeepSeek Harness 在 master 上的提交 967ba4c4b8 把初始化从 stty -echo 改成 stty -echo; set +H,但它是 master 的祖先、不是 rc.3 tag 的祖先,所以 rc.3 仍会复现。
在 DeepSeek Harness 里把 ! 放进单引号并确保整条命令经过单引号引用,或直接用不含 ! 的等价写法(比如用 printf 生成字面量)。另外把长命令拆短,能让失败更快暴露。
相关术语
- history expansion
- history expansion 是 bash 在读取行时执行的历史展开机制,把 ! 开头的内容替换为历史事件。它在解析与执行之前发生,因此行内关闭命令来不及生效。— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- quoteForBash
- quoteForBash 是工具层把命令包装成可投递字符串的函数,使用 ANSI-C 引用 $'...'。它转义了反斜杠、单引号与换行,但没有处理感叹号。— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- OSC 133;D
- OSC 133;D 是终端完成检测协议中由 PROMPT_COMMAND 打印的标记,表示一条命令结束。丢弃行后 bash 重打的提示符不带它,完成检测因此失效。— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- inferred_idle
- inferred_idle 是完成检测的兜底推断状态,表示看起来空闲但没有拿到完成标记。当前工具循环不把它当作早退条件,于是会一直等到超时。— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
来源
- #10636 — heredoc / 单行命令含 ! 会导致整个命令行挂死· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub