Merge pull request #6756 from deepseek-harness/fix/persistent-bash-input-mangling

fix(tools): disable history expansion in the persistent bash shell
This commit is contained in:
Ruilin Geng authored and GitHub committed 2026-10-23 13:37:34 +08:00
commit cd9ee0ee4c
7 files changed
+23 -11

No files matched your search

+1 -1
View File
@@ -3841,7 +3841,7 @@ export interface Config {
## `@deepseek-ai/dsh-tool-bash-persistent`
- `inject`: `tools` · `terminals` · `workingDirectory`
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts)
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts)
```ts config-catalog
/** Configuration for the persistent Bash tool. */
+1 -1
View File
@@ -3843,7 +3843,7 @@ export interface Config {
## `@deepseek-ai/dsh-tool-bash-persistent`
- `inject`: `tools` · `terminals` · `workingDirectory`
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:445`](../packages/shell/tool-bash-persistent/src/index.ts)
- `source`: [`packages/shell/tool-bash-persistent/src/index.ts:447`](../packages/shell/tool-bash-persistent/src/index.ts)
```ts config-catalog
/** Configuration for the persistent Bash tool. */
@@ -39,8 +39,8 @@
en: 857e036f41c913ac
zh: e9f383f7ee384c59
/deepseek-ai-dsh-tool-bash-persistent/understand-the-implementation/command-flow:
en: 3b7bf7c20b266c31
zh: 85a325ef65fa7ac7
en: c511f75009ba3743
zh: b3b722e00b2910b1
/deepseek-ai-dsh-tool-bash-persistent/further-exploration:
en: 42c387ccaab7e64c
zh: e58ec4ea60cf7711
@@ -72,8 +72,8 @@
en: ba6a48888c3361e5
zh: 3a8f648053555dc9
/deepseek-ai-dsh-tool-bash-persistent/known-limitations-and-deferred-work:
en: dc5a908f2b2da040
zh: 3afceaabcf324771
en: b12111850d752f0b
zh: 92b855387ec187ff
/deepseek-ai-dsh-tool-bash-persistent/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
@@ -85,7 +85,7 @@ This section explains the design decisions behind the tool and points at the cod
### Command flow
A first command spawns the shell through `ctx.terminals.spawn`, disables input echo (`stty -echo`), and waits for readiness. Each command is then wrapped into one physical line — a printf of the start marker, the command body escaped with `$'…'`, and a printf of the end marker plus `$?` — so embedded newlines cannot leak terminal prompts into the result. The tool polls the scrollback in 1,000-line pages until the end marker appears, extracts the span, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell.
A first command spawns the shell through `ctx.terminals.spawn`, disables input echo and history expansion (`stty -echo; set +H`), and waits for readiness. Each command is then wrapped into one physical line — a printf of the start marker, the command body escaped with `$'…'`, and a printf of the end marker plus `$?` — so embedded newlines cannot leak terminal prompts into the result. With history expansion off, `!` sequences reach bash unchanged instead of being rewritten or rejecting the whole line. The tool polls the scrollback in 1,000-line pages until the end marker appears, extracts the span, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell.
</details>
@@ -148,6 +148,7 @@ These limits define when the tool is a poor fit or needs special care. They are
- **An interactive foreground child returns early with partial output only where the subprocess provider proves its stdin wait** — elsewhere the call runs to `timeoutMs`.
- **Explicit `exit` and timeout discard shell state** — cancellation also resets and discards the result, even when a complete status marker is already observable; the next call starts a fresh shell.
- **Environment facts such as network access and package mirrors belong in the configured `description`** — not this package's default.
- **Raw control characters other than newline and carriage return reach readline as typed keys** — a literal TAB, ESC, or DEL in the command is consumed as an editing key, so content that needs them, such as a Makefile heredoc, should be written with a file tool or produced by `printf` escapes.
<a id="dev-note"></a>
### Dev Note
@@ -85,7 +85,7 @@ kind: "package-reference"
### 命令流程
首条命令通过 `ctx.terminals.spawn` 生成 shell,禁用输入回显(`stty -echo`),并等待就绪。随后每条命令都包装成一行物理文本——printf 起始标记、用 `$'…'` 转义的命令体、printf 结束标记加 `$?`——因此内嵌换行无法把终端提示符泄漏进结果。工具以 1,000 行一页轮询 scrollback,直到出现结束标记,提取区间并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
首条命令通过 `ctx.terminals.spawn` 生成 shell,禁用输入回显和历史展开(`stty -echo; set +H`),并等待就绪。随后每条命令都包装成一行物理文本——printf 起始标记、用 `$'…'` 转义的命令体、printf 结束标记加 `$?`——因此内嵌换行无法把终端提示符泄漏进结果。关闭历史展开后,`!` 序列原样到达 bash,不会被改写,也不会使整行被拒绝。工具以 1,000 行一页轮询 scrollback,直到出现结束标记,提取区间并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
</details>
@@ -148,6 +148,7 @@ kind: "package-reference"
- **交互式前台子进程只在子进程提供方证明其 stdin 等待时才提前返回部分输出**——否则调用一直运行到 `timeoutMs`。
- **显式 `exit` 与超时会丢弃 shell 状态**——取消同样重置并丢弃结果,即使完整状态标记已经可观察;下一次调用启动全新 shell。
- **网络访问与包镜像等环境事实属于配置的 `description`**——而不是本包的默认描述。
- **除换行与回车外,原始控制字符会作为按键到达 readline**——命令中的字面 TAB、ESC 或 DEL 会被当作编辑按键消耗,因此需要这些字符的内容(例如 Makefile heredoc)应通过文件工具写入,或用 `printf` 转义生成。
<a id="dev-note"></a>
### 开发备注
@@ -267,10 +267,12 @@ function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShell
live.delete(owner)
}, 'tool-bash-persistent owner cache cleanup')
}
// Echo suppression only: the prompt stays the backend's own, so the
// backend's prompt-based readiness detection keeps working.
// Suppress input echo and history expansion, which would otherwise
// rewrite `!` sequences or discard the whole wrapper line before bash
// parses it. The prompt stays the backend's own, so the backend's
// prompt-based readiness detection keeps working.
const setup = ctx.terminals.startSend(owner, spawned.sessionId, {
text: 'stty -echo',
text: 'stty -echo; set +H',
submit: true,
signal: combinedSignal,
})
@@ -165,6 +165,14 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
))
expect(heredoc).toBe('alpha\nbeta\n[Command finished with exit code 0]')
// Interactive bash applies history expansion to typed input before parsing;
// it must neither rewrite `!` sequences nor drop the whole wrapper line.
const history = text(await execute(
'history-expansion',
"echo '<!doctype html>' > page.html; cat page.html; echo 'it'; echo hi!!",
))
expect(history).toBe('<!doctype html>\nit\nhi!!\n[Command finished with exit code 0]')
const pipeline = text(await execute(
'pipeline',
'{ sleep 0.1; printf "delayed\\n"; } | cat',