fix(tools): disable history expansion in the persistent bash shell
The persistent bash tool types each wrapped command into an interactive bash, which applies history expansion before parsing. A `!` after a single quote made bash reject the whole line with `event not found`, so neither marker printed and the call waited for the full timeout; `!!` silently substituted the previous history entry. Disable history expansion when the shell is initialized, as OpenHands does for its interactive terminal. Raw control characters other than newline and carriage return still reach readline as typed keys; the README records that limitation.
This commit is contained in:
1 parent
78aeeae767
commit
967ba4c4b8
7 files changed
+23
-11
No files matched your search
@@ -3832,7 +3832,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. */
|
||||
|
||||
@@ -3834,7 +3834,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,
|
||||
})
|
||||
|
||||
@@ -160,6 +160,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',
|
||||
|
||||
Reference in new issue
Block a user