diff --git a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.i18n.yaml
index 68061a23f2..83be26ee45 100644
--- a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.i18n.yaml
@@ -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: 02d4e105922d31ea1b2190fdb50819d0df5353db
-2026-08-11-pwsh-persistent-pty.zh.md: d46c9c30f7e89198382def7eed3827dba2ab9c02
+2026-08-11-pwsh-persistent-pty.md: ae7976d070c4ed1f70d2afa96c5b3cca014dd00d
+2026-08-11-pwsh-persistent-pty.zh.md: 9500be3f02790eb86e24458746c07785dfd4f448
diff --git a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md
index 02d4e10592..ae7976d070 100644
--- a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md
+++ b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md
@@ -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 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.
+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 selected terminal backend owns prompt bootstrap and publishes only a ready session; the tool uses that session without submitting a second prompt definition. The real Loader composition test keeps its silence fallback beyond the send timeout, so Windows must settle through the backend's controlled-prompt path.
### Composition
diff --git a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md
index d46c9c30f7..9500be3f02 100644
--- a/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md
@@ -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 函数重新声明与 backend 兼容的受控提示符(`dsh> `),在工具初始化后保留 `terminal-bash` 的精确尾部就绪路径;跨包测试把两边的 prompt 设置命令固定为相同值。
+命令经包装器执行:先重置 `$LASTEXITCODE`(可赋值,已实测),通过 `Invoke-Expression` 在反引号转义的双引号字符串中执行 body(`quoteForPwsh`:反引号、引号、`$`、CRLF 与 ESC 转义,输入行上不携带裸控制字符,包装器可在 ConstrainedLanguage 下存活),报告精确原生退出码、PowerShell 终止性错误的 `1` 或成功的 `0`。PSReadLine 会把提交的包装器回显进流——没有 `stty -echo` 的对应物——因此提取会从捕获输出中剥离包装器原文;回显无法伪造完成,因为状态正则要求 END nonce 后紧跟数字,而回显继续是引号字符。所选 terminal 后端负责 prompt 引导,并且只发布就绪会话;工具直接使用该会话,不再提交第二次 prompt 定义。真实 Loader 组合测试把静默回退设置在 send 超时之后,因此 Windows 必须通过后端的受控 prompt 路径结算。
### 组合
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 06ba7fc978..2b0cdeaed9 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.i18n.yaml
@@ -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 docs/config-catalog.md
-config-catalog.md: 28917f4104069320cac7f42d009738a96f84f016
-config-catalog.zh.md: dc42d568f8c93b8de103b167b6a70976b821b316
+config-catalog.md: 3e8c60a764d8b4261fbcb4cd014c1a4f7225da22
+config-catalog.zh.md: ac12ac3bb6006228a774301efc0b97a5f1cf94d0
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 28917f4104..3e8c60a764 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -2813,7 +2813,7 @@ export interface Config {
}
```
-Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
+Source: [`packages/shell/tool-pwsh-persistent/src/index.ts:456`](../packages/shell/tool-pwsh-persistent/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index dc42d568f8..ac12ac3bb6 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -2815,7 +2815,7 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:472`](../packages/shell/tool-pwsh-persistent/src/index.ts)
+来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:456`](../packages/shell/tool-pwsh-persistent/src/index.ts)
diff --git a/packages/shell/tool-pwsh-persistent/README.i18n.yaml b/packages/shell/tool-pwsh-persistent/README.i18n.yaml
index 3db3715591..fd1efbdbab 100644
--- a/packages/shell/tool-pwsh-persistent/README.i18n.yaml
+++ b/packages/shell/tool-pwsh-persistent/README.i18n.yaml
@@ -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: 690221548cf9739f790dbd6409452a038559c149
-README.zh.md: b3962a26a6c023c02db59a5bf7ad0cb469442bf3
+README.md: cd91e5d2826a0d815ff6053cd0adf3cfb5efae93
+README.zh.md: d8b694d4e48d5f8c9568796d53381b2e94812f80
diff --git a/packages/shell/tool-pwsh-persistent/README.md b/packages/shell/tool-pwsh-persistent/README.md
index 690221548c..cd91e5d282 100644
--- a/packages/shell/tool-pwsh-persistent/README.md
+++ b/packages/shell/tool-pwsh-persistent/README.md
@@ -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 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.
+- **Prompt-function readiness.** The pwsh terminal backend owns prompt bootstrap and publishes the session only after its controlled `prompt` function is ready. The tool uses that existing prompt instead of submitting a second definition. 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.
@@ -81,12 +81,12 @@ This section explains the design decisions behind the tool and points at the cod
| File | Role |
|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: shell registry, prompt setup, command wrapping, scrollback polling, extraction and rendering |
+| [`src/index.ts`](src/index.ts) | Plugin entry: shell registry, command wrapping, scrollback polling, extraction and rendering |
| — | No runtime invariant companion is published; the adapter's private owner-to-shell cache has no observable event or data relation. Lifecycle tests prove its cleanup without adding a public API solely for an invariant. |
### Command flow
-A first command spawns the shell through `ctx.terminals.spawn`, installs the `prompt` override, and waits for readiness. Each command is wrapped into one physical line — `Write-Output` of the start marker, the body escaped with backtick escapes into a double-quoted string, and `Write-Output` of the end marker plus the exit status — so PSReadLine's echo of a wrapped line cannot fabricate completion. The tool polls the scrollback in 1,000-line pages until the end marker or a completed prompt appears, extracts the span, strips the echoed wrapper and prompts, 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` and receives it only after the selected terminal backend has completed prompt bootstrap. Each command is wrapped into one physical line — `Write-Output` of the start marker, the body escaped with backtick escapes into a double-quoted string, and `Write-Output` of the end marker plus the exit status — so PSReadLine's echo of a wrapped line cannot fabricate completion. The tool polls the scrollback in 1,000-line pages until the end marker or a completed prompt appears, extracts the span, strips the echoed wrapper and prompts, and renders it with any status marker. A timeout aborts the deadline, captures the partial output, and resets the shell.
diff --git a/packages/shell/tool-pwsh-persistent/README.zh.md b/packages/shell/tool-pwsh-persistent/README.zh.md
index b3962a26a6..d8b694d4e4 100644
--- a/packages/shell/tool-pwsh-persistent/README.zh.md
+++ b/packages/shell/tool-pwsh-persistent/README.zh.md
@@ -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 函数就绪。** 工具重新声明与默认 pwsh terminal 后端相同的受控 `prompt` 函数。BEL 结尾的 OSC 标记携带最后的退出码,可打印的 `dsh> ` 尾部让后端通过标记快路径结算每条命令,包括无法检查 stdin-wait 的 Windows。模型重定义 `prompt` 会把就绪降级到静默层级。
+- **prompt 函数就绪。** pwsh terminal 后端负责 prompt 引导,并且只在其受控 `prompt` 函数就绪后发布会话。工具直接使用现有 prompt,不再提交第二次定义。BEL 结尾的 OSC 标记携带最后的退出码,可打印的 `dsh> ` 尾部让后端通过标记快路径结算每条命令,包括无法检查 stdin-wait 的 Windows。模型重定义 `prompt` 会把就绪降级到静默层级。
- **PSReadLine 回显靠锚定剥离。** PowerShell 会把提交的输入渲染回流中;标记锚定提取与包装源码剥离移除回显,而跨终端宽度换行的包装可能在部分输出结果中留下部分回显。
- **重置,而非修复。** 任何不确定状态——显式 `exit`、超时、发送失败、中止——都会关闭 shell 并让下一次调用从全新状态开始。
@@ -81,12 +81,12 @@ kind: "package-reference"
| 文件 | 职责 |
|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:shell 注册表、prompt 设置、命令包装、scrollback 轮询、提取与渲染 |
+| [`src/index.ts`](src/index.ts) | 插件入口:shell 注册表、命令包装、scrollback 轮询、提取与渲染 |
| — | 不发布运行时不变式伴生入口;shell 复用可通过工具执行观察。 |
### 命令流程
-首条命令通过 `ctx.terminals.spawn` 生成 shell,安装 `prompt` 覆盖,并等待就绪。随后每条命令都包装成一行物理文本——`Write-Output` 起始标记、用反引号转义进双引号字符串的命令体、`Write-Output` 结束标记加退出状态——因此 PSReadLine 对换行包装的回显无法伪造完成。工具以 1,000 行一页轮询 scrollback,直到出现结束标记或完成的提示词,提取区间、剥离回显的包装与提示词,并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
+首条命令通过 `ctx.terminals.spawn` 生成 shell,并且只在所选 terminal 后端完成 prompt 引导后取得会话。随后每条命令都包装成一行物理文本——`Write-Output` 起始标记、用反引号转义进双引号字符串的命令体、`Write-Output` 结束标记加退出状态——因此 PSReadLine 对换行包装的回显无法伪造完成。工具以 1,000 行一页轮询 scrollback,直到出现结束标记或完成的提示词,提取区间、剥离回显的包装与提示词,并连同任何状态标记一起渲染。超时会中止截止时间、捕获部分输出并重置 shell。
diff --git a/packages/shell/tool-pwsh-persistent/src/index.ts b/packages/shell/tool-pwsh-persistent/src/index.ts
index 90f2eda34a..5e7ea23d62 100644
--- a/packages/shell/tool-pwsh-persistent/src/index.ts
+++ b/packages/shell/tool-pwsh-persistent/src/index.ts
@@ -252,15 +252,6 @@ async function respondToSessionExit(
].filter(part => part.length > 0).join('\n')
}
-/**
- * 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 + "' }"
-
function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShells {
const pending = new WeakMap>()
const live = new Map()
@@ -307,15 +298,8 @@ function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShell
live.delete(owner)
}, 'tool-pwsh-persistent owner cache cleanup')
}
- const setup = ctx.terminals.startSend(owner, spawned.sessionId, {
- text: PWSH_PROMPT_SETUP,
- submit: true,
- signal: combinedSignal,
- })
- const result = await setup.done
- if (result.sessionStatus.kind === 'exited' || result.waitReason === 'timeout') {
- throw new Error('persistent pwsh shell did not accept initialization')
- }
+ // The selected terminal backend owns bootstrap and publishes only a
+ // ready session.
return spawned.sessionId
} catch (error: unknown) {
await reset(owner, 'persistent pwsh initialization failed')
diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
index 6ff1a9d085..5f324a4d83 100644
--- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
+++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
@@ -5,7 +5,6 @@ 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,
@@ -92,8 +91,6 @@ type StubMode =
| 'torn-status'
| 'finish-torn-status'
| 'end-only'
- | 'init-exit'
- | 'init-timeout'
| 'spawn-error'
| 'send-error'
| 'prompt-after-idle'
@@ -127,16 +124,6 @@ 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 }
- return this.operation(Promise.resolve(this.result('', 'session_exit')))
- }
- if (this.mode === 'init-timeout') {
- return this.operation(Promise.resolve(this.result('', 'timeout')))
- }
- return this.operation(Promise.resolve(this.result(this.motd, 'stdin_read')))
- }
if (this.mode === 'send-error') throw new Error('stub send failed')
if (this.throwOnSend) throw new Error('PTY session has exited')
if (this.mode === 'wait-for-abort' || this.mode === 'end-on-abort') {
@@ -347,8 +334,9 @@ describe('tool-pwsh-persistent', () => {
expect(text(await call(ctx, owner, 'Write-Output one'))).toBe('hello from stub')
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)
+ expect(stub.sessions[0]?.sends).toBe(2)
+ expect(stub.sessions[0]?.requests[0]?.text).toContain('__DSH_PERSISTENT_PWSH_START_')
+ expect(stub.sessions[0]?.requests[0]?.text).not.toContain('function prompt')
const ownerWithoutCwd = agent(ctx, undefined)
expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub')
@@ -558,15 +546,6 @@ describe('tool-pwsh-persistent', () => {
},
)
- it.each(['init-exit', 'init-timeout'] as const)(
- 'fails initialization and closes the unusable shell for %s',
- async (mode) => {
- const { ctx, owner, stub } = await setup({ backendType: 'stub' }, mode)
- expect((await call(ctx, owner, 'pwd')).isError).toBe(true)
- expect(stub.sessions[0]?.closed).toContain('persistent pwsh initialization failed')
- },
- )
-
it('clears a failed spawn without trying to close an unpublished shell', async () => {
const { ctx, owner, stub } = await setup({ backendType: 'stub' }, 'spawn-error')
expect((await call(ctx, owner, 'pwd')).isError).toBe(true)