fix(shell): preserve pwsh prompt readiness
This commit is contained in:
1 parent
633b3c0f69
commit
94651815f2
8 files changed
+22
-18
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md
|
||||
2026-08-11-pwsh-persistent-pty.md: 4c523d3c7c45e6d86942868df92b981576e76859
|
||||
2026-08-11-pwsh-persistent-pty.zh.md: 4f87490fd60ecc37ad9390e0ce990173bbafc3b8
|
||||
2026-08-11-pwsh-persistent-pty.md: 02d4e105922d31ea1b2190fdb50819d0df5353db
|
||||
2026-08-11-pwsh-persistent-pty.zh.md: d46c9c30f7e89198382def7eed3827dba2ab9c02
|
||||
@@ -30,7 +30,7 @@ One backend, two dialects: `shellDialect: 'bash' | 'pwsh'` (default `'bash'`; th
|
||||
|
||||
A new package mirroring `tool-bash-persistent`: same `Config` (`backendType` default `shell`, `timeoutMs`, `maxOutputChars`, `description`), same owner-scoped shell registry and serialized per-owner queue, same timeout/abort/exit/reset paths. The tool name is `pwsh`; it never co-mounts with the one-shot `tool-pwsh` because the preset rows are mutually exclusive per platform.
|
||||
|
||||
Commands run through a wrapper that resets `$LASTEXITCODE` (assignable, verified), invokes the body via `Invoke-Expression` in a backtick-escaped double-quoted string (`quoteForPwsh`: backtick, quote, `$`, CRLF, and ESC escapes, so no raw control characters ride the input line and the wrapper survives ConstrainedLanguage), and reports the exact native exit code, `1` for a terminating PowerShell error, or `0` for success. PSReadLine echoes the submitted wrapper back into the stream — there is no `stty -echo` equivalent — so the extraction strips the wrapper source from captured output; the echo can never fabricate completion because the status regex needs digits immediately after the END nonce and the echo continues with quote characters. The prompt function installs the tool's own prompt (`__DSH_PERSISTENT_PWSH_PROMPT__ `) over the backend bootstrap value, the same two-layer structure as bash.
|
||||
Commands run through a wrapper that resets `$LASTEXITCODE` (assignable, verified), invokes the body via `Invoke-Expression` in a backtick-escaped double-quoted string (`quoteForPwsh`: backtick, quote, `$`, CRLF, and ESC escapes, so no raw control characters ride the input line and the wrapper survives ConstrainedLanguage), and reports the exact native exit code, `1` for a terminating PowerShell error, or `0` for success. PSReadLine echoes the submitted wrapper back into the stream — there is no `stty -echo` equivalent — so the extraction strips the wrapper source from captured output; the echo can never fabricate completion because the status regex needs digits immediately after the END nonce and the echo continues with quote characters. The prompt function reasserts the backend-compatible controlled prompt (`dsh> `), preserving `terminal-bash`'s exact-tail readiness path after tool initialization; a cross-package test pins both prompt setup commands to the same value.
|
||||
|
||||
### Composition
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ harness 在 Windows 上没有持久 shell。持久 `bash` 栈按构造就是 POS
|
||||
|
||||
新包镜像 `tool-bash-persistent`:同样的 `Config`(`backendType` 默认 `shell`、`timeoutMs`、`maxOutputChars`、`description`)、同样的 owner 作用域 shell 注册表与每 owner 串行队列、同样的超时/中止/退出/重置路径。工具名是 `pwsh`;它与一次性 `tool-pwsh` 永不共挂,因为预设行按平台互斥。
|
||||
|
||||
命令经包装器执行:先重置 `$LASTEXITCODE`(可赋值,已实测),通过 `Invoke-Expression` 在反引号转义的双引号字符串中执行 body(`quoteForPwsh`:反引号、引号、`$`、CRLF 与 ESC 转义,输入行上不携带裸控制字符,包装器可在 ConstrainedLanguage 下存活),报告精确原生退出码、PowerShell 终止性错误的 `1` 或成功的 `0`。PSReadLine 会把提交的包装器回显进流——没有 `stty -echo` 的对应物——因此提取会从捕获输出中剥离包装器原文;回显无法伪造完成,因为状态正则要求 END nonce 后紧跟数字,而回显继续是引号字符。prompt 函数安装工具自有提示符(`__DSH_PERSISTENT_PWSH_PROMPT__ `)覆盖 backend 引导值,与 bash 的双层结构相同。
|
||||
命令经包装器执行:先重置 `$LASTEXITCODE`(可赋值,已实测),通过 `Invoke-Expression` 在反引号转义的双引号字符串中执行 body(`quoteForPwsh`:反引号、引号、`$`、CRLF 与 ESC 转义,输入行上不携带裸控制字符,包装器可在 ConstrainedLanguage 下存活),报告精确原生退出码、PowerShell 终止性错误的 `1` 或成功的 `0`。PSReadLine 会把提交的包装器回显进流——没有 `stty -echo` 的对应物——因此提取会从捕获输出中剥离包装器原文;回显无法伪造完成,因为状态正则要求 END nonce 后紧跟数字,而回显继续是引号字符。prompt 函数重新声明与 backend 兼容的受控提示符(`dsh> `),在工具初始化后保留 `terminal-bash` 的精确尾部就绪路径;跨包测试把两边的 prompt 设置命令固定为相同值。
|
||||
|
||||
### 组合
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/shell/tool-pwsh-persistent/README.md
|
||||
README.md: 4865c7482d29369f65c2c41323f69a3145bb2536
|
||||
README.zh.md: 92826668173c5ba7a1588bba8ba13078937d3d9d
|
||||
README.md: 690221548cf9739f790dbd6409452a038559c149
|
||||
README.zh.md: b3962a26a6c023c02db59a5bf7ad0cb469442bf3
|
||||
@@ -73,7 +73,7 @@ This section explains the design decisions behind the tool and points at the cod
|
||||
### Design philosophy
|
||||
|
||||
- **A deliberate twin of `dsh-tool-bash-persistent`.** The session registry, polling loop, and reset contract mirror the persistent bash tool by design ([pwsh persistent PTY Agent Note](../../../.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md)).
|
||||
- **Prompt-function readiness.** The tool installs its own `prompt` function that prints a BEL-terminated OSC marker plus a printable prompt; the OSC marker carries the last exit code and the printable prompt settles every command, so a model redefinition of `prompt` degrades readiness to the silence tier.
|
||||
- **Prompt-function readiness.** The tool reasserts the same controlled `prompt` function as the default pwsh terminal backend. Its BEL-terminated OSC marker carries the last exit code, and its printable `dsh> ` tail lets the backend settle every command through the marker fast path, including on Windows where stdin-wait inspection is unavailable. A model redefinition of `prompt` degrades readiness to the silence tier.
|
||||
- **PSReadLine echo stripped by anchoring.** PowerShell renders submitted input back into the stream; the marker-anchored extraction and a wrapper-source strip remove the echo, and a wrapper that wraps across the terminal width may leave a partial echo in partial-output results.
|
||||
- **Reset, never repair.** Any uncertain state — an explicit `exit`, a timeout, a send failure, an abort — closes the shell and starts the next call fresh.
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ kind: "package-reference"
|
||||
### 设计理念
|
||||
|
||||
- **`dsh-tool-bash-persistent` 的刻意孪生。** 会话注册表、轮询循环与重置约定按设计镜像持久 bash 工具([pwsh 持久 PTY Agent Note](../../../.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md))。
|
||||
- **prompt 函数就绪。** 工具安装自己的 `prompt` 函数,打印 BEL 结尾的 OSC 标记加可打印提示词;OSC 标记携带最后的退出码,可打印提示词让每条命令都能结算,因此模型重定义 `prompt` 会把就绪降级到静默层级。
|
||||
- **prompt 函数就绪。** 工具重新声明与默认 pwsh terminal 后端相同的受控 `prompt` 函数。BEL 结尾的 OSC 标记携带最后的退出码,可打印的 `dsh> ` 尾部让后端通过标记快路径结算每条命令,包括无法检查 stdin-wait 的 Windows。模型重定义 `prompt` 会把就绪降级到静默层级。
|
||||
- **PSReadLine 回显靠锚定剥离。** PowerShell 会把提交的输入渲染回流中;标记锚定提取与包装源码剥离移除回显,而跨终端宽度换行的包装可能在部分输出结果中留下部分回显。
|
||||
- **重置,而非修复。** 任何不确定状态——显式 `exit`、超时、发送失败、中止——都会关闭 shell 并让下一次调用从全新状态开始。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
const TRUNCATED_MESSAGE = '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with Select-String in order to find the line numbers of what you are looking for.</NOTE>'
|
||||
const LOST_PREFIX_MESSAGE = '<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n'
|
||||
const SHELL_RESET_MESSAGE = 'The persistent pwsh shell was reset; the next pwsh call starts from the workspace with a fresh current directory and environment.'
|
||||
const SHELL_PROMPT = '__DSH_PERSISTENT_PWSH_PROMPT__ '
|
||||
const SHELL_PROMPT = 'dsh> '
|
||||
const TIMEOUT_CODE = 'PERSISTENT_PWSH_TIMEOUT'
|
||||
// One page is enough to find a just-emitted completion marker; the full
|
||||
// scrollback is assembled only when a command settles or needs partial output.
|
||||
@@ -253,10 +253,10 @@ async function respondToSessionExit(
|
||||
}
|
||||
|
||||
/**
|
||||
* The pwsh prompt function that overrides the backend bootstrap value with
|
||||
* this tool's own prompt. `[char]27`/`[char]7` build the OSC bytes at runtime
|
||||
* because raw ESC characters in submitted input are unreliable under
|
||||
* PSReadLine.
|
||||
* The pwsh prompt function that reasserts the backend's controlled prompt.
|
||||
* The shared value preserves exact-tail readiness after initialization.
|
||||
* `[char]27`/`[char]7` build the OSC bytes at runtime because raw ESC characters
|
||||
* in submitted input are unreliable under PSReadLine.
|
||||
*/
|
||||
const PWSH_PROMPT_SETUP =
|
||||
"function prompt { [Console]::Write([char]27 + ']133;D;' + [int]$LASTEXITCODE + [char]7); '" + SHELL_PROMPT + "' }"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import TerminalSessionService from '@deepseek-ai/dsh-terminal'
|
||||
import { PWSH_PROMPT_SETUP as TERMINAL_PWSH_PROMPT_SETUP } from '@deepseek-ai/dsh-terminal-bash'
|
||||
import type {
|
||||
TerminalBackend,
|
||||
TerminalBackendSession,
|
||||
@@ -107,13 +108,14 @@ const START_PATTERN = /__DSH_PERSISTENT_PWSH_START_[^_]+(?:-[^_]+)*__/
|
||||
const END_PATTERN = /__DSH_PERSISTENT_PWSH_END_[^:]+:/
|
||||
|
||||
class StubTerminalSession implements TerminalBackendSession {
|
||||
readonly motd = '__DSH_PERSISTENT_PWSH_PROMPT__ '
|
||||
readonly motd = 'dsh> '
|
||||
readonly pid = 123
|
||||
statusValue: TerminalSessionStatus = { kind: 'running' }
|
||||
scrollback = this.motd
|
||||
closed: string[] = []
|
||||
mode: StubMode
|
||||
sends = 0
|
||||
requests: TerminalSendRequest[] = []
|
||||
pendingText = ''
|
||||
historyTruncated = false
|
||||
throwOnSend = false
|
||||
@@ -124,6 +126,7 @@ class StubTerminalSession implements TerminalBackendSession {
|
||||
|
||||
startSend(request: TerminalSendRequest): TerminalSendOperation {
|
||||
this.sends += 1
|
||||
this.requests.push(request)
|
||||
if (request.text.startsWith('function prompt')) {
|
||||
if (this.mode === 'init-exit') {
|
||||
this.statusValue = { kind: 'exited', exitCode: 1, signal: null }
|
||||
@@ -345,6 +348,7 @@ describe('tool-pwsh-persistent', () => {
|
||||
expect(text(await call(ctx, owner, 'Write-Output two'))).toBe('hello from stub')
|
||||
expect(stub.sessions).toHaveLength(1)
|
||||
expect(stub.sessions[0]?.sends).toBe(3)
|
||||
expect(stub.sessions[0]?.requests[0]?.text).toBe(TERMINAL_PWSH_PROMPT_SETUP)
|
||||
|
||||
const ownerWithoutCwd = agent(ctx, undefined)
|
||||
expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub')
|
||||
@@ -366,7 +370,7 @@ describe('tool-pwsh-persistent', () => {
|
||||
expect(result).not.toContain('Invoke-Expression')
|
||||
})
|
||||
|
||||
it('preserves command output that equals the private shell prompt', async () => {
|
||||
it('preserves command output that equals the controlled shell prompt', async () => {
|
||||
const { ctx, owner, stub } = await setup({ backendType: 'stub' })
|
||||
await call(ctx, owner, 'warm up')
|
||||
const session = stub.sessions[0]!
|
||||
@@ -408,13 +412,13 @@ describe('tool-pwsh-persistent', () => {
|
||||
session.mode = 'prompt-only'
|
||||
const promptFallback = text(await call(ctx, owner, 'bad {'))
|
||||
expect(promptFallback).toContain('pwsh: synt')
|
||||
expect(promptFallback).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
|
||||
expect(promptFallback).not.toContain(session.motd)
|
||||
|
||||
session.mode = 'prompt-crlf'
|
||||
session.scrollback = ''
|
||||
const crlfPromptFallback = text(await call(ctx, owner, 'bad {'))
|
||||
expect(crlfPromptFallback).toContain('pwsh: synt')
|
||||
expect(crlfPromptFallback).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
|
||||
expect(crlfPromptFallback).not.toContain(session.motd)
|
||||
|
||||
session.mode = 'end-only'
|
||||
session.scrollback = ''
|
||||
@@ -509,7 +513,7 @@ describe('tool-pwsh-persistent', () => {
|
||||
const result = text(await call(ctx, owner, 'bad {'))
|
||||
expect(result).toContain('partial syntax output')
|
||||
expect(result).toContain('pwsh: syntax error')
|
||||
expect(result).not.toContain('DSH_PERSISTENT_PWSH_PROMPT')
|
||||
expect(result).not.toContain(session.motd)
|
||||
expect(result).not.toContain('DSH_PERSISTENT_PWSH_START')
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user