Parcourir la source

docs(pty): persistent pwsh READMEs, dialect docs, and the implemented note

Adds the tool-pwsh-persistent README trio, documents the pty-local
shellDialect and the subprocess-local Windows inspector (console-wide
signalling, pseudo foreground groups, taskkill teardown) in both
languages, updates the tool-pwsh and persistent-pty notes in place, and
moves the pwsh-persistent-pty design note to implemented with the
shipped Decision and Consequences.
Huanqi Cao il y a 1 mois
Parent
commit
da4701d28b

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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: 51988586a6c260d528e140ab718eb302355c7314
+2026-08-11-pwsh-persistent-pty.zh.md: e753c050827b216330ac9bce0258f9b59d866999

+ 65 - 0
.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md

@@ -0,0 +1,65 @@
+# Agent Note: Persistent pwsh over the PTY seam on Windows
+
+Status: implemented
+
+English | [中文](2026-08-11-pwsh-persistent-pty.zh.md)
+
+## Problem
+
+The harness had no persistent shell on Windows. The persistent `bash` stack was POSIX-only by construction: `@deepseek-ai/dsh-subprocess-local` threw at terminal allocation (`createProcessInspector()` rejected win32), `@deepseek-ai/dsh-pty-local` was bash-shaped (`/bin/bash` defaults, `PS1`/`PROMPT_COMMAND` environment markers), `@deepseek-ai/dsh-tool-bash-persistent` wrapped commands in bash syntax, and every pty test skipped on win32. The one-shot `pwsh` tool (`@deepseek-ai/dsh-tool-pwsh` over `@deepseek-ai/dsh-pwsh-local`) already ran on Windows, but each call started a fresh `pwsh -Command` process: cwd, `$env:` variables, functions, and interactive children ended with the call, and its README recorded "No persistent shell or PTY" as deferred work.
+
+The gap excluded Windows workflows whose state lives in a terminal: stepping a debugger, exploring in a Python or Node REPL, or returning to a shell after interrupting its foreground command — the same class of work the persistent bash pty serves on POSIX.
+
+Two foundations already existed. The PTY service itself (`ctx.pty` registry, owner scoping, send/read/signal/kill contract) is platform-neutral. The Loader's `disabled: !!js` interpolation (PR #2234) gates shell rows per platform and pins the invariant that exactly one shell stack mounts per host; a persistent pwsh stack composes through the same rows.
+
+## Decision
+
+A model-facing persistent `pwsh` tool ships on Windows with the same contract as `tool-bash-persistent`: one owner-scoped persistent shell per Agent, marker-detected command completion, exact native exit codes, bounded output, and timeout/cancel/`exit` semantics that reset the shell and tell the model. Three pieces deliver it: a Windows substrate in `subprocess-local`, a shell-dialect option in `pty-local`, and the new `tool-pwsh-persistent` package with the minimal-preset composition rows.
+
+### Windows substrate in `@deepseek-ai/dsh-subprocess-local`
+
+`createProcessInspector()` returns a `WindowsProcessInspector` on win32 instead of throwing. The koffi-backed inspector enumerates the process table through Toolhelp32 with GetProcessTimes creation-time identities (pid-reuse fencing like the POSIX start identity), reports the **shell pid as a pseudo foreground group** (Windows has no POSIX groups; the stable value lets the prompt-marker readiness fast path settle in one poll interval), reports no stdin-wait evidence (readiness degrades exactly like macOS), and signals through `taskkill /T` escalation (`/F` only for SIGKILL). koffi (`^3.1.0`, the version `sandbox-windows-acl` already pins) loads lazily on win32 only.
+
+`LocalTerminalHandle` branches for win32 because node-pty's `kill(signal)` throws ("Signals not supported on windows") and its bare kill delegates to a console-list agent that fails without a parent console. Teardown escalates through taskkill fenced on the shell's start identity, and — because an externally taskkilled shell may never fire node-pty's exit notification — the handle settles `done` from the inspector-verified absence (`settleExitIfGone`). `signalForeground` maps SIGINT to a `\x03` Ctrl-C input write (the console-wide delivery conhost turns into a CTRL_C event; verified to interrupt a running command), routes SIGTERM/SIGKILL to taskkill, and rejects SIGTSTP/SIGHUP as unavailable on Windows. The public `PtySignal` set and seam types are unchanged; the mapping lives in the backend.
+
+### Shell dialect in `@deepseek-ai/dsh-pty-local`
+
+One backend, two dialects: `shellDialect: 'bash' | 'pwsh'` (default `'bash'`, existing deployments byte-identical). The effective `shellPath`/`shellArgs` resolve per dialect (bash `/bin/bash --noprofile --norc -i`; pwsh through the shared `dsh-pwsh-local` resolver with `-NoLogo -NoProfile`, keeping the interactive host for child REPLs). The child environment drops the bash-only `PS1`/`PROMPT_COMMAND` markers and adds `NO_COLOR` for pwsh. pwsh cannot install its prompt from the environment, so the backend writes the prompt function through the session at startup and waits until the controlled prompt is actually visible, looping over follow-up sends because the pwsh banner-to-prompt gap can outlast the silence bound; a `session_exit` or `timeout` wait rejects the spawn. Both dialects emit the same BEL-terminated OSC `133;D;` marker, so the sanitizer, `PROMPT_MARKER_PREFIX`, `CONTROLLED_PROMPT`, and the exact-tail readiness logic are reused untouched — the marker stays a readiness signal with an unconsumed payload, exactly as in the bash path, and no model-notification channel was added (aligned with the current implementation; the deferred BEL event channel stays deferred).
+
+### `@deepseek-ai/dsh-tool-pwsh-persistent`
+
+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.
+
+### Composition
+
+The minimal preset gates its persistent shell stack by platform with the #2234 `disabled: !!js` interpolation: the bash rows (`pty-local` + `tool-bash-persistent`) mount on POSIX, and the pwsh rows (`pty-local` with `shellDialect: pwsh` + `tool-pwsh-persistent`) mount on win32 — exactly one persistent shell per host. `windows-shell.spec` pins the per-platform roster; the real Loader composition exercises the whole stack over a real ConPTY pwsh.
+
+### Testing
+
+The subprocess-local and pty-local suites now run on Windows: bash-shaped fixtures self-skip through platform gates, the spawn/terminal suites translate their simple shell commands to node one-liners and exercise injected POSIX group paths, and the koffi-backed inspector joins the windows-only coverage exclusions on Linux while the windows-native lane enforces its 100% coverage. The tool suite mirrors `tool-bash-persistent`'s stub-mode matrix plus an echo-stripping mode; the real-pwsh suites prove persistent cwd/env, secret scrubbing, multiline and here-string commands, large-output clipping, and exit/reset over real ConPTY sessions.
+
+## Alternatives considered
+
+- **A separate `pty-pwsh-local` backend package.** Rejected: the local session, sanitizer, readiness tiers, and sandbox fence are shared machinery; duplicating the 500-line session for argv/env/startup differences trades one config field for a package of copy-paste, unlike the bash group's thin parallel executors.
+- **tasklist or wmic polling for the process tree.** Rejected: `inspectForeground` runs on every readiness poll (~50 ms), so a spawned probe per tick is untenable, and wmic is removed from current Windows releases. koffi + Toolhelp32 is in-process and cheap.
+- **A native helper or `GenerateConsoleCtrlEvent` for SIGINT.** Rejected: writing `\x03` to ConPTY input interrupts running commands (verified) with zero new code. The semantic difference — at a prompt, `\x03` cancels the pending line instead of signalling a process — is documented rather than engineered around.
+- **Base64 body encoding for the wrapper.** Rejected: decoding needs `[Convert]`/`[System.Text.Encoding]` calls whose ConstrainedLanguage status is unproven, while backtick-escaped double-quoted strings use only language-level constructs and were verified end-to-end.
+- **Tolerating the echo without stripping the wrapper.** Rejected: in complete and prompt-settled paths the echo is naturally excluded, but timeout and lost-START fallbacks would leak the wrapper source (including marker nonces) into model-visible text.
+- **Resurrecting a BEL model-notification channel.** Rejected: the current implementation consumes no marker payload and delivers no BEL events; the design aligns with the current implementation and keeps the deferred item deferred.
+- **Windows PowerShell 5.1 as a first-class target.** Rejected: pwsh 7 (including the Store install) is the target; `resolvePwshPath` keeps 5.1 as the last-resort executable fallback without promising full persistent-shell behavior on it.
+
+## Consequences
+
+**Windows became a first-class persistent-shell host.** The pty family now runs, tests, and is coverage-gated on the windows-native lane; the one-shot/persistent shell split mirrors POSIX, and the preset spec pins exactly one shell stack per host on both platforms.
+
+**The windows-native coverage flip is a standing commitment.** subprocess-local and pty-local sources are coverage-required on win32; their suites run there (with platform gates and node-translated commands) and must keep 100% coverage on the windows-native lane.
+
+**Windows readiness is weaker than Linux.** The pseudo-pgid marker fast path covers shell prompts, but a child without a prompt settles on the silence tier (~3 s), exactly like macOS; there is no exact stdin-wait tier.
+
+**Windows teardown and signalling differ from POSIX.** taskkill without `/F` does not terminate console processes (the TERM tier is a grace wait before `/F`), SIGINT is console-wide Ctrl-C, SIGTSTP/SIGHUP are unavailable, and externally taskkilled shells may not fire node-pty's exit notification — the handle settles from verified absence instead.
+
+**Input echo is an accepted platform fact.** PSReadLine echoes submitted input; the marker-anchored extraction and wrapper-source strip remove it in complete results, with bounded residual in partial-output fallbacks.
+
+**Risks carried.** Under the Windows ACL sandbox's read-only mode, ConstrainedLanguage may deny the prompt function's `[Console]::` call; the `Write-Host -NoNewline` fallback is designed and decided by the Windows-native lane. A model redefinition of the `prompt` function degrades readiness to the silence tier. Raw ESC characters in model commands are unsupported (PSReadLine consumes them). koffi is now a dependency of the process substrate, carrying the same install/prebuild review the sandbox package already has.

+ 65 - 0
.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md

@@ -0,0 +1,65 @@
+# Agent Note: Windows 上基于 PTY seam 的持久化 pwsh
+
+Status: implemented
+
+[English](2026-08-11-pwsh-persistent-pty.md) | 中文
+
+## 问题
+
+harness 在 Windows 上没有持久 shell。持久 `bash` 栈按构造就是 POSIX-only:`@deepseek-ai/dsh-subprocess-local` 在终端分配时直接抛错(`createProcessInspector()` 拒绝 win32),`@deepseek-ai/dsh-pty-local` 是 bash 形态(`/bin/bash` 默认值、`PS1`/`PROMPT_COMMAND` 环境标记),`@deepseek-ai/dsh-tool-bash-persistent` 用 bash 语法包装命令,pty 测试全部在 win32 上 skip。一次性 `pwsh` 工具(`@deepseek-ai/dsh-tool-pwsh` + `@deepseek-ai/dsh-pwsh-local`)已经能在 Windows 运行,但每次调用都是全新的 `pwsh -Command` 进程:cwd、`$env:` 变量、函数和交互式子进程都随调用结束,其 README 把 "No persistent shell or PTY" 记为 deferred work。
+
+这个缺口排除了状态驻留在终端里的 Windows 工作流:单步调试、在 Python 或 Node REPL 中探索、中断前台命令后回到原 shell —— 正是持久 bash pty 在 POSIX 上服务的同一类工作。
+
+两个基础已经存在。PTY 服务本身(`ctx.pty` 注册表、owner 作用域、send/read/signal/kill 契约)是平台无关的。Loader 的 `disabled: !!js` 插值(PR #2234)按平台门控 shell 行,并钉死了"每宿主恰好挂载一个 shell 栈"的不变量;持久 pwsh 栈通过同一行机制组合。
+
+## 决定
+
+模型侧持久 `pwsh` 工具在 Windows 上交付,契约与 `tool-bash-persistent` 逐项对齐:每个 Agent 一个 owner 作用域的持久 shell、标记检测的命令完成、精确的原生退出码、有界输出,以及超时/取消/`exit` 时重置 shell 并告知模型的语义。三块交付:`subprocess-local` 的 Windows 基座、`pty-local` 的 shell 方言选项、新的 `tool-pwsh-persistent` 包加 minimal 预设组合行。
+
+### `@deepseek-ai/dsh-subprocess-local` 的 Windows 基座
+
+`createProcessInspector()` 在 win32 返回 `WindowsProcessInspector` 而不是抛错。基于 koffi 的检查器通过 Toolhelp32 枚举进程表并取 GetProcessTimes 创建时间身份(与 POSIX start-identity 相同的 PID 复用防护),把 **shell pid 作为伪前台进程组**(Windows 没有 POSIX 进程组;这个稳定值让 prompt-marker 就绪快路径在一个轮询间隔内结算),不报告 stdin-wait 证据(就绪与 macOS 同档),信号走 `taskkill /T` 升级(仅 SIGKILL 加 `/F`)。koffi(`^3.1.0`,`sandbox-windows-acl` 已固定的版本)仅在 win32 惰性加载。
+
+`LocalTerminalHandle` 为 win32 分支,因为 node-pty 的 `kill(signal)` 会抛错("Signals not supported on windows"),其无参 kill 委托的 console-list agent 在没有父控制台时失败。拆卸经 taskkill 升级并以 shell 的启动身份作栅栏;由于被外部 taskkill 的 shell 可能永远不会触发 node-pty 的退出通知,句柄从 inspector 验证的消失状态结算 `done`(`settleExitIfGone`)。`signalForeground` 把 SIGINT 映射为 `\x03` Ctrl-C 输入写入(conhost 转为控制台级 CTRL_C 事件的投递方式;实测可中断运行中的命令),SIGTERM/SIGKILL 路由到 taskkill,SIGTSTP/SIGHUP 以 Windows 不可用为由拒绝。公共 `PtySignal` 集合与 seam 类型不变;映射全部留在 backend。
+
+### `@deepseek-ai/dsh-pty-local` 的 shell 方言
+
+一个 backend、两种方言:`shellDialect: 'bash' | 'pwsh'`(默认 `'bash'`,存量部署逐字节不变)。有效 `shellPath`/`shellArgs` 按方言解析(bash `/bin/bash --noprofile --norc -i`;pwsh 经共享的 `dsh-pwsh-local` 解析器取 `-NoLogo -NoProfile`,保留交互宿主供子 REPL)。子环境去掉 bash 专属 `PS1`/`PROMPT_COMMAND` 标记并为 pwsh 加 `NO_COLOR`。pwsh 无法从环境安装提示符,因此 backend 在启动时通过会话写入 prompt 函数,并等待受控提示符真正可见——因为 pwsh 从横幅到提示符的间隙可能超过静默上限,所以会在后续 send 上循环等待;`session_exit` 或 `timeout` 结算拒绝 spawn。两种方言发出相同的 BEL 终结 OSC `133;D;` 标记,因此 sanitizer、`PROMPT_MARKER_PREFIX`、`CONTROLLED_PROMPT` 与精确尾部就绪逻辑原样复用——标记仍只是就绪信号、载荷不被消费,与 bash 路径完全一致,且没有新增模型通知通道(与当前实现对齐;延后的 BEL 事件通道保持延后)。
+
+### `@deepseek-ai/dsh-tool-pwsh-persistent`
+
+新包镜像 `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 的双层结构相同。
+
+### 组合
+
+minimal 预设用 #2234 的 `disabled: !!js` 插值按平台门控持久 shell 栈:bash 行(`pty-local` + `tool-bash-persistent`)在 POSIX 挂载,pwsh 行(`shellDialect: pwsh` 的 `pty-local` + `tool-pwsh-persistent`)在 win32 挂载——每宿主恰好一个持久 shell。`windows-shell.spec` 钉死按平台的花名册;真实 Loader 组合在真实 ConPTY pwsh 上跑通整条栈。
+
+### 测试
+
+subprocess-local 与 pty-local 套件现在在 Windows 上运行:bash 形态 fixture 通过平台门控自跳过,spawn/terminal 套件把简单 shell 命令翻译为 node 单行并覆盖注入的 POSIX 组路径,koffi-backed inspector 在 Linux 侧加入 windows-only 覆盖豁免,windows-native 车道强制执行其 100% 覆盖。工具套件镜像 `tool-bash-persistent` 的 stub 模式矩阵并加回显剥离模式;真实 pwsh 套件在真实 ConPTY 会话上证明持久 cwd/env、密钥清洗、多行与 here-string 命令、大输出裁剪与退出/重置。
+
+## 备选方案
+
+- **独立的 `pty-pwsh-local` backend 包。** 拒绝:本地 session、sanitizer、就绪档位和沙箱栅栏是共享机制;为一个 config 字段复制 500 行 session 换来的是一包复制粘贴,与 bash 组并置薄 executor 的情形不同。
+- **tasklist 或 wmic 轮询进程树。** 拒绝:`inspectForeground` 每次就绪轮询(约 50ms)都跑,每 tick 生成一次探测进程不可行;wmic 已从现行 Windows 移除。koffi + Toolhelp32 是进程内、廉价的。
+- **为 SIGINT 加原生 helper 或 `GenerateConsoleCtrlEvent`。** 拒绝:向 ConPTY 输入写 `\x03` 即可中断运行中的命令(已实测),零新增代码。语义差异——在提示符处 `\x03` 取消当前行而不是给进程发信号——文档化而不是绕开。
+- **包装器 body 用 base64 编码。** 拒绝:解码需要 `[Convert]`/`[System.Text.Encoding]` 调用,其在 ConstrainedLanguage 下的可用性未证实;反引号转义的双引号字符串只用语言级构造,且已端到端实测。
+- **容忍回显而不剥离包装器。** 拒绝:完整路径和提示符就绪路径下回显天然被排除,但超时和 START 丢失的回退会把包装器源码(含 marker nonce)泄漏进模型可见文本。
+- **复活 BEL 模型通知通道。** 拒绝:当前实现不消费任何 marker 载荷、不投递任何 BEL 事件;设计对齐当前实现,deferred 项保持 deferred。
+- **把 Windows PowerShell 5.1 当一等目标。** 拒绝:pwsh 7(含 Store 安装)是目标;`resolvePwshPath` 保留 5.1 作为最后的可执行回退,但不承诺持久 shell 在其上的完整行为。
+
+## 后果
+
+**Windows 成为一等公民的持久 shell 宿主。** pty 家族现在在 windows-native 车道上运行、测试并受覆盖门禁约束;一次性/持久 shell 的划分与 POSIX 镜像,预设 spec 在两种平台上都钉死每宿主恰好一个 shell 栈。
+
+**windows-native 覆盖翻转是常驻承诺。** subprocess-local 与 pty-local 源码在 win32 上受覆盖约束;它们的套件在那里运行(带平台门控与 node 翻译命令),并必须在 windows-native 车道保持 100% 覆盖。
+
+**Windows 就绪弱于 Linux。** 伪 pgid marker 快路径覆盖 shell 提示符,但没有提示符的子进程按静默档结算(约 3s),与 macOS 完全一致;没有精确的 stdin-wait 档。
+
+**Windows 的拆卸与信号不同于 POSIX。** 不带 `/F` 的 taskkill 无法终止控制台进程(TERM 档是 `/F` 升级前的宽限等待)、SIGINT 是控制台级 Ctrl-C、SIGTSTP/SIGHUP 不可用,且被外部 taskkill 的 shell 可能不触发 node-pty 的退出通知——句柄改从验证的消失状态结算。
+
+**输入回显是接受的平台事实。** PSReadLine 回显提交的输入;marker 锚定提取与包装器原文剥离在完整结果中移除它,部分输出回退中残留有界。
+
+**携带的风险。** Windows ACL 沙箱只读模式下,ConstrainedLanguage 可能拒绝 prompt 函数的 `[Console]::` 调用;`Write-Host -NoNewline` 回退已设计好,由 Windows-native 车道裁决。模型重定义 `prompt` 函数会使就绪降级到静默档。模型命令中的裸 ESC 字符不受支持(PSReadLine 会吞掉)。koffi 成为进程基座的依赖,承担与沙箱包相同的安装/prebuild 评审。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.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/feature/2026-07-16-persistent-pty-sessions.md
-2026-07-16-persistent-pty-sessions.md: fb9cd06bade7bc357baa738f0d9dd03b7f5b7936
-2026-07-16-persistent-pty-sessions.zh.md: 55a5848c1ab1e8c2cd3b29f2d4748ea4abbe088c
+2026-07-16-persistent-pty-sessions.md: fa9d90e3ded97b279c5cad7c8b5733333403beaf
+2026-07-16-persistent-pty-sessions.zh.md: f801f4d0728452b2a5acf75c5bcbef0d9e1e6901

+ 1 - 1
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md

@@ -134,7 +134,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
 - Declarative per-agent startup requires an agent-setup composition point; plugin-load global sessions remain prohibited.
 - Session restoration across harness-process loss requires an out-of-process owner and a versioned protocol.
 - Network-egress policy and rollback of external side effects are broader than PTY and remain separate security work.
-- Windows/ConPTY support requires a backend with Windows-native process ownership and signaling semantics.
+- Windows/ConPTY sessions run through the subprocess-local Windows inspector (Toolhelp32 identities, pseudo foreground groups, taskkill teardown) and the `pty-local` pwsh dialect; see the [pwsh persistent tool note](../../architecture/2026-08-11-pwsh-persistent-pty.md).
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md

@@ -134,7 +134,7 @@ plugins:
 - 声明式 per-agent 启动需要 agent-setup 组合点;仍然禁止插件加载期全局会话。
 - harness 进程丢失后的会话恢复需要进程外 owner 和版本化协议。
 - 网络出口策略与外部副作用回滚超出 PTY 范围,继续作为独立安全工作。
-- Windows/ConPTY 支持需要具备 Windows 原生进程所有权与信号语义的后端
+- Windows/ConPTY 会话经由 subprocess-local 的 Windows inspector(Toolhelp32 身份、伪前台进程组、taskkill 拆卸)与 `pty-local` 的 pwsh 方言运行;见 [pwsh 持久工具 note](../../architecture/2026-08-11-pwsh-persistent-pty.md)
 
 ## 备选方案
 

+ 2 - 2
packages/bash/tool-pwsh/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/bash/tool-pwsh/README.md
-README.md: 4126e718c569f17fb8be465351b2576970e93c63
-README.zh.md: aba669733a7ecb9924287abb298bb9a154b5afa7
+README.md: b843ed11823aadec7c28cc26e45da41701e40d3d
+README.zh.md: 04f471fd15d6bf4343f674a29d03bb48c4870fdb

+ 1 - 1
packages/bash/tool-pwsh/README.md

@@ -121,6 +121,6 @@ Append-only; newly visible content follows the reusable request prefix and does
 ## Known Limitations and Deferred Work
 
 - **Language mode and named-pipe capture under the Windows sandbox** — under the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md), read-only pwsh starts in ConstrainedLanguage because its temp write denial makes PowerShell's AppLocker probe fail closed: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. Workspace-write's private temp lets the probe complete, so it stays in FullLanguage unless host policy says otherwise. Both confined modes deny named-pipe opens, so a piped-stdio spawn inside a confined command fails with EPERM. The tool description teaches both contracts to the model; the backend README owns the full limitations.
-- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`; the PTY backends are Linux/macOS-only today, and a Windows ConPTY persistent shell is roadmap work.
+- **No persistent shell** — every call starts a fresh `pwsh -Command`; the persistent-shell counterpart is [`@deepseek-ai/dsh-tool-pwsh-persistent`](../../pty/tool-pwsh-persistent/README.md), which keeps one owner-scoped pwsh alive across calls on Windows (ConPTY) and POSIX hosts with pwsh.
 - **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
 - **Session-cwd identity is not canonicalized** — the workdir base is the session header cwd as-is, unlike the bash tool's sandbox-root-canonicalized identity. Under a confining executor the policy's workspace root IS canonicalized (by the shared policy service), so the workdir and the confinement root can diverge when the raw session cwd differs from its canonical form — a parity gap deferred to the shared shell-tool base extraction.

+ 1 - 1
packages/bash/tool-pwsh/README.zh.md

@@ -121,6 +121,6 @@ ack 是固定短行;任务输出按读取有界。
 ## Known Limitations and Deferred Work
 
 - **Windows 沙箱下的语言模式与 named-pipe 捕获** — 在 [Windows ACL 沙箱](../../sandbox/sandbox-windows-acl/README.md) 下,read-only pwsh 会以 ConstrainedLanguage 启动,因为临时目录写入被拒绝,导致 PowerShell 的 AppLocker 探针失败并按 fail-closed 处理:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,且该模式无法从内部解除。workspace-write 的私有临时目录使探针得以完成,因此除非主机策略另有规定,否则它保持 FullLanguage。两种受限模式都拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个约定教给模型;后端 README 负责完整的限制说明。
-- **无持久 shell 或 PTY** — 每次调用都启动全新的 `pwsh -Command`;PTY 后端目前仅限 Linux/macOS,Windows ConPTY 持久 shell 属于路线图工作
+- **无持久 shell** — 每次调用都启动全新的 `pwsh -Command`;持久 shell 对应物是 [`@deepseek-ai/dsh-tool-pwsh-persistent`](../../pty/tool-pwsh-persistent/README.md),它在 Windows(ConPTY)以及装有 pwsh 的 POSIX 主机上跨调用保持一个 owner 作用域的 pwsh 存活
 - **PowerShell 方言约定** — 模型必须写 PowerShell(原生路径、`$env:` 变量),而不是 bash;没有方言翻译。
 - **会话 cwd 身份不做规范化** — workdir 基座直接取会话头 cwd 原值,不同于 bash 工具经 sandbox-root 规范化的身份。在隔离执行器下,策略的工作区根**会**被规范化(由共享的策略服务完成),因此当原始会话 cwd 与其规范化形态不同时,workdir 与隔离根可能不一致——这一 parity 差距留待共享 shell 工具基座提取时解决。

+ 2 - 2
packages/pty/pty-local/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/pty/pty-local/README.md
-README.md: 5acc92853e6e8fcb8938c48e391559bf4a28fb75
-README.zh.md: 7d070a9a4aceab921264f020716fbdf16fdd57ff
+README.md: 4565f9a6efd122e6302368c2d616f09649f0cae0
+README.zh.md: 44c1440c20a428048e1509b22141066d16758d31

+ 4 - 1
packages/pty/pty-local/README.md

@@ -8,6 +8,8 @@ Persistent shell backend for `ctx.pty` over `ctx.subprocess.spawnTerminal`. It s
 
 The plugin injects `pty`, `sandboxPolicy`, and `subprocess`, then registers the configured backend type (`shell`). `danger-full-access` starts the shell directly without requiring a sandbox provider; confined modes require a same-world `ctx.sandbox` and wrap the exact shell argv through it, failing before spawn when none is mounted. At spawn, one `ctx.sandboxPolicy.resolve({ session })` call supplies both the effective mode and the session workspace root; the same root is the default shell cwd when the caller omits one. A change to a different effective mode is rejected before its `sandbox/mode` event commits while that owner has an open PTY or a spawn in progress; the fence is attached to the exact owner and therefore outlives a provider reload that retains existing sessions. Wait for creation to settle and close the sessions before changing modes, so a terminal opened with wider access cannot survive a downgrade.
 
+`shellDialect` selects the shell stack (`bash` default, `pwsh`): it picks the default `shellPath`/`shellArgs` (bash `--noprofile --norc -i`; pwsh `-NoLogo -NoProfile` through the shared `dsh-pwsh-local` resolver) and the startup contract. The bash dialect installs its prompt through the environment (`PS1` plus an OSC `133;D;`-terminated `PROMPT_COMMAND`). pwsh cannot install a prompt from the environment, so the backend writes a `prompt` function through the session and waits until the controlled prompt is actually visible — looping over follow-up sends because the pwsh banner-to-prompt gap can outlast the silence bound — while its environment drops the bash-only markers and adds `NO_COLOR`. Both dialects emit the same BEL-terminated OSC marker, so the readiness machinery and consumers are dialect-agnostic.
+
 Readiness combines a foreground-verified private bash prompt marker, provider-reported foreground stdin-wait facts, silence fallback, and absolute timeout. A marker is not ready until the printable tail after the latest owned marker exactly equals the controlled `PS1`, including when the OSC marker and prompt are split across data callbacks; echoed input or output following an earlier prompt therefore cannot settle the current send. Prompt and silence evidence collected before the provider write, including while pre-write foreground inspection is pending, is discarded at the write boundary. When bash prints the marker before the terminal provider publishes its return to the foreground process group, polling retains the candidate for `handoffGraceMs` past the ordinary silence bound so a coincident handoff can win. An interactive child that inherits `PROMPT_COMMAND` therefore cannot suppress inferred-idle readiness until the absolute timeout. Unknown foreground state is never a positive exact-idle signal. A foreground group's stdin wait that existed before a send is likewise not post-write readiness: the same group must be observed outside that wait before a later wait can settle the send, while a changed foreground group is new evidence. During unpublished startup, a fallback requires observed output; zero-output silence cannot publish an empty session, and timeout rejects the spawn. Cancellation closes the unpublished shell and rejects with the caller's exact abort reason; `PtyBackendCleanupError` separately preserves a cleanup failure. The caller's signal is forwarded for terminal allocation and readiness initialization; after publication the handle owns its lifetime. Incomplete terminal-control sequences are bounded by `maxReadBytes` and discarded through their terminator after crossing that limit; malformed UTF-8 terminal output uses replacement characters, and a trailing carriage return is carried across callbacks so split CRLF becomes one newline.
 
 Send cancellation marks queued input as canceled before asking the terminal handle to signal the current foreground process group with a real `SIGINT`; if asynchronous pre-write inspection later settles, it cannot execute that input. If a provider write is already in flight, signalling waits for it to settle; a rejected write sends no signal. The canceled send retains its slot until the write and foreground signalling settle, so a successor cannot receive either late bytes or that signal. A provider write or signal that never settles therefore retains the slot indefinitely; closing the session (`terminal_close`) is the recovery. The absolute deadline remains armed while cancellation waits. A signal failure is a terminal transport failure and rejects the active send. Cancellation never emulates interruption by writing `\x03`, so raw-mode programs remain cancellable. Close rejects new public signals, stops readiness polling, and awaits the handle's provider-owned complete-session termination before settling the active send as `session_exit`.
@@ -31,6 +33,7 @@ A standing-policy change appends an owner-rendered superseding runtime-context s
 ## Known Limitations and Deferred Work
 
 - Line-oriented output is normalized; full-screen alternate-buffer interaction is unsupported.
-- Exact stdin-wait detection depends on the mounted subprocess provider; providers that cannot prove it use prompt-marker and silence/timeout readiness.
+- Exact stdin-wait detection depends on the mounted subprocess provider; providers that cannot prove it use prompt-marker and silence/timeout readiness. Windows is such a provider: the shell pid is the pseudo foreground group and there is no exact stdin-wait tier, so a marker-less child settles on the silence bound.
+- The pwsh `prompt` bootstrap writes through `[Console]::`, which the Windows ACL sandbox's read-only mode (ConstrainedLanguage) may deny; the `Write-Host -NoNewline` fallback is the designed alternative, decided by the Windows-native lane.
 - Cleanup guarantees are those of `SubprocessTerminalHandle`; provider-specific gaps belong to that implementation's contract rather than this PTY consumer.
 - Sessions do not survive harness process exit.

+ 4 - 1
packages/pty/pty-local/README.zh.md

@@ -8,6 +8,8 @@
 
 该插件注入 `pty`、`sandboxPolicy` 和 `subprocess`,然后注册所配置的后端类型(`shell`)。`danger-full-access` 无需沙箱提供方即可直接启动 shell;受限模式要求同一执行世界中存在 `ctx.sandbox`,并通过它包装确切的 shell argv,未挂载时会在 spawn 前失败。spawn 时,一次 `ctx.sandboxPolicy.resolve({ session })` 调用会同时给出实际模式与会话工作区根目录;调用方省略 cwd 时,同一根目录也是 shell 的默认 cwd。当某个所有者存在开放的 PTY 或正在进行 spawn 时,如果配置变更会得到不同的实际模式,系统会在对应 `sandbox/mode` 事件提交前拒绝该变更。该限制绑定到确切所有者,因此即使提供方重新加载并保留现有会话,它仍然有效。更改模式前,请等待创建完成并关闭会话,避免以更宽权限打开的终端在权限降级后继续存在。
 
+`shellDialect` 选择 shell 栈(默认 `bash`,或 `pwsh`):它决定默认的 `shellPath`/`shellArgs`(bash 为 `--noprofile --norc -i`;pwsh 经共享的 `dsh-pwsh-local` 解析器得到 `-NoLogo -NoProfile`)与启动契约。bash 方言通过环境安装提示符(`PS1` 加 OSC `133;D;` 终结的 `PROMPT_COMMAND`)。pwsh 无法从环境安装提示符,因此后端通过会话写入 `prompt` 函数,并等待受控提示符真正可见——因为 pwsh 从横幅到提示符的间隙可能超过静默上限,所以会在后续 send 上循环等待;同时其环境去掉 bash 专属标记并加 `NO_COLOR`。两种方言发出相同的 BEL 终结 OSC 标记,因此就绪机制与消费方与方言无关。
+
 就绪检测结合以下机制:由前台状态验证的私有 bash 提示符标记、提供方报告的前台 stdin 等待事实、静默回退和绝对超时。只有最新自有标记之后的可打印尾部与受控 `PS1` 完全相等,标记才算就绪;即使 OSC 标记和提示符被拆到多个数据回调中也一样。因此,较早提示符之后的回显输入或输出无法使当前 send 完成。提供方写入前收集的提示符与静默证据,包括写入前前台检查仍在等待时收集的证据,都会在写入边界丢弃。如果 bash 在终端提供方发布其重新取得前台进程组的状态前打印标记,轮询会在普通静默上限之后再保留该候选状态 `handoffGraceMs`,使恰好同时发生的前台交接有机会胜出。因此,继承 `PROMPT_COMMAND` 的交互式子进程无法一直抑制推断空闲就绪直至绝对超时。未知的前台状态绝不会作为精确空闲的正向信号。同样,一次 send 之前就已存在的前台进程组 stdin 等待并不代表写入后就绪:必须先观察到同一进程组脱离该等待,之后再次进入等待才能使该次 send 完成;前台进程组发生变化则构成新的证据。尚未发布的启动过程中,回退路径要求已经观察到输出;零输出静默不能发布空会话,超时则拒绝 spawn。取消操作会关闭尚未发布的 shell,并以调用方提供的确切中止原因拒绝;`PtyBackendCleanupError` 会单独保留清理失败。调用方的 signal 会转发给终端分配与就绪初始化;发布后,句柄负责其生命周期。未完成的终端控制序列受 `maxReadBytes` 限制;超过上限后,系统会丢弃内容直到其终止符。格式错误的 UTF-8 终端输出使用替换字符;末尾的回车会跨回调保留,使拆分的 CRLF 合并为一个换行。
 
 取消发送时,系统会先把排队输入标记为已取消,再要求终端句柄向当前前台进程组发送真正的 `SIGINT`;异步写入前检查即使随后结算,也无法执行该输入。如果提供方写入已在途,信号发送会等待其结算;写入被拒绝时不会发送信号。已取消的 send 会保留其位置,直到写入与前台信号发送都结算,因此后继 send 不会收到延迟字节或该信号。因此,永不结算的提供方写入或信号会无限期保留该位置;恢复手段是关闭会话(`terminal_close`)。取消等待期间,绝对 deadline 仍保持启用。信号发送失败是终端传输失败,会拒绝活跃 send。取消绝不会通过写入 `\x03` 模拟中断,因此,即使程序运行在 raw 模式下,也仍可取消。关闭操作会拒绝新的公开信号、停止就绪轮询,并等待由句柄提供方负责的完整会话终止,然后才把活跃 send 结算为 `session_exit`。
@@ -31,6 +33,7 @@
 ## 已知限制与暂缓事项
 
 - 输出按行规范化;不支持全屏备用缓冲区交互。
-- 精确 stdin 等待检测取决于已挂载的进程管理提供方;无法证明该状态的提供方使用提示符标记和静默/超时就绪机制。
+- 精确 stdin 等待检测取决于已挂载的进程管理提供方;无法证明该状态的提供方使用提示符标记和静默/超时就绪机制。Windows 正是这样的提供方:shell pid 是伪前台进程组,没有精确的 stdin-wait 档,因此无标记的子进程按静默上限结算。
+- pwsh `prompt` 引导通过 `[Console]::` 写入,Windows ACL 沙箱的只读模式(ConstrainedLanguage)可能拒绝它;`Write-Host -NoNewline` 回退是设计好的备选,由 Windows-native 车道裁决。
 - 清理保证以 `SubprocessTerminalHandle` 的保证为准;提供方特定的缺口属于该实现的约定,而非这个 PTY 消费方。
 - harness 进程退出后,会话无法继续存在。

+ 6 - 0
packages/pty/tool-pwsh-persistent/README.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/pty/tool-pwsh-persistent/README.md
+README.md: 32fcaf15d38648c347e6423c9b68753ccca91287
+README.zh.md: 8c2201657796d2d6bd39ea5d130ee246a58e17d9

+ 55 - 0
packages/pty/tool-pwsh-persistent/README.md

@@ -0,0 +1,55 @@
+# @deepseek-ai/dsh-tool-pwsh-persistent
+
+English | [中文](README.zh.md)
+
+Model-facing `pwsh(command)` backed by one owner-scoped `ctx.pty` shell. The package owns the tool contract and shell reuse; deployments select the PTY backend (a `pty-local` instance configured with `shellDialect: pwsh`) and sandbox policy. It is the Windows counterpart of `tool-bash-persistent`: same persistent-state contract, PowerShell dialect.
+
+## Config
+
+| Key | Default | Meaning |
+|---|---:|---|
+| `backendType` | `shell` | Registered PTY backend used for each Agent shell. |
+| `timeoutMs` | `300000` | Wall-clock limit for one command; timeout closes the shell. |
+| `maxOutputChars` | `16000` | Maximum retained command-output characters; fixed diagnostics are added afterward. |
+| `description` | Persistent-shell description | Model-facing environment contract. |
+
+## Model Experience
+
+### Tool schema
+
+#### What the model sees
+
+The generated [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh-persistent), including the configured `description`. The plugin contributes no standalone system-prompt section; the deployment owns persona and environment guidance.
+
+#### Token effect
+
+Fixed schema cost while `pwsh` is visible.
+
+#### KV Cache effect
+
+Prefix-stable while the configured description and schema remain unchanged.
+
+### Tool results
+
+#### What the model sees
+
+Commands share one shell per Agent, so cwd, `$env:` variables, functions, and background jobs persist across calls. Results exclude private completion markers, the shell prompt, and the echoed input line (PSReadLine renders submitted input back into the stream; the marker-anchored extraction and the wrapper-source strip remove it). A nonzero wrapped command appends `[exit code: N]` — the exact native exit code when the command ran a native program, `1` for a terminating PowerShell error. A shell that exits before reporting that status instead appends `[shell exited: code N]`, `[shell killed by signal: SIG]`, or `[shell exited]` when the backend supplies neither (Windows forced termination reports exit 1 without a signal), then resets and tells the model that the next call starts fresh. Long output keeps the earliest retained prefix plus a clipping notice; if the PTY has already dropped that prefix, the result says so explicitly. Timeout returns bounded partial output, closes the uncertain shell, and reports the reset.
+
+#### Token effect
+
+Data-dependent. `maxOutputChars` bounds retained command output; fixed clipping, lost-prefix, status, timeout, and reset diagnostics can extend the result.
+
+#### KV Cache effect
+
+Append-only tool results follow the reusable request prefix.
+
+## Known Limitations and Deferred Work
+
+- The tool requires an owning Agent and a real PTY backend with a pwsh dialect (Windows ConPTY or a POSIX pwsh).
+- **Input echo is unavoidable**: PowerShell's PSReadLine renders submitted input back into the terminal stream, and there is no `stty -echo` equivalent. The marker-anchored extraction excludes the echo in complete results; the wrapper-source strip covers fallback paths, but a wrapper that wraps across the terminal width may leave a partial echo in partial-output results, bounded by `maxOutputChars`.
+- Raw ESC characters inside model commands are unsupported: PSReadLine consumes them before execution. The wrapper escapes the control bytes it needs (`[char]27`-built OSC markers, backtick escapes for the body).
+- A model redefinition of the `prompt` function removes the readiness marker; the shell then settles on the silence tier instead of the marker fast path.
+- There is no interactive stdin during a command: a foreground command that reads input blocks until the readiness timeout, which resets the shell.
+- SIGTSTP/SIGHUP are unavailable on Windows (backend-rejected); SIGINT is delivered as a console-wide Ctrl-C input write, which at a prompt cancels the pending line instead of signalling a process.
+- Under the Windows ACL sandbox's read-only mode, pwsh starts in ConstrainedLanguage, which may deny the prompt function's `[Console]::` call; the backend's documented `Write-Host -NoNewline` fallback is selected by the Windows-native lane evidence.
+- The BEL-terminated OSC marker remains a readiness signal only; a BEL event channel to the model stays deferred, aligned with the current implementation.

+ 55 - 0
packages/pty/tool-pwsh-persistent/README.zh.md

@@ -0,0 +1,55 @@
+# @deepseek-ai/dsh-tool-pwsh-persistent
+
+[English](README.md) | 中文
+
+模型侧 `pwsh(command)`,由一个 owner 作用域的 `ctx.pty` shell 支撑。本包拥有工具契约与 shell 复用;部署方选择 PTY backend(配置 `shellDialect: pwsh` 的 `pty-local` 实例)与沙箱策略。它是 `tool-bash-persistent` 的 Windows 对应物:相同的持久状态契约,PowerShell 方言。
+
+## 配置
+
+| 键 | 默认值 | 含义 |
+|---|---:|---|
+| `backendType` | `shell` | 每个 Agent shell 使用的已注册 PTY backend。 |
+| `timeoutMs` | `300000` | 单条命令的墙钟上限;超时关闭 shell。 |
+| `maxOutputChars` | `16000` | 保留的命令输出字符上限;固定诊断文本在其后追加。 |
+| `description` | 持久 shell 描述 | 模型可见的环境契约。 |
+
+## 模型体验
+
+### 工具 schema
+
+#### 模型看到什么
+
+生成的 [`pwsh` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-pwsh-persistent),含配置的 `description`。本插件不贡献独立的 system-prompt 段落;persona 与环境指引由部署方负责。
+
+#### Token 影响
+
+`pwsh` 可见期间每个请求有固定的 schema 成本。
+
+#### KV Cache 影响
+
+配置的 description 与 schema 不变时前缀稳定。
+
+### 工具结果
+
+#### 模型看到什么
+
+命令共享每个 Agent 的一个 shell,因此 cwd、`$env:` 变量、函数和后台任务跨调用保留。结果排除私有完成标记、shell 提示符与回显的输入行(PSReadLine 会把提交的输入渲染回输出流;marker 锚定提取与包装器原文剥离将其移除)。非零包装命令追加 `[exit code: N]` —— 命令运行原生程序时是精确的原生退出码,PowerShell 终止性错误为 `1`。shell 在报告状态前退出的,改为追加 `[shell exited: code N]`、`[shell killed by signal: SIG]` 或 `[shell exited]`(backend 两者都没有时;Windows 强杀按无 signal 的 exit 1 报告),然后重置并告知模型下一次调用从全新 shell 开始。长输出保留最早的前缀并附裁剪提示;若 PTY 已丢弃该前缀,结果会明确说明。超时返回有界的部分输出、关闭不确定的 shell 并报告重置。
+
+#### Token 影响
+
+数据相关。`maxOutputChars` 限制保留的命令输出;固定裁剪、前缀丢失、状态、超时与重置诊断可能扩展结果。
+
+#### KV Cache 影响
+
+追加式工具结果跟随可复用的请求前缀。
+
+## 已知限制与延后工作
+
+- 工具需要拥有 Agent 与一个真实支持 pwsh 方言的 PTY backend(Windows ConPTY 或 POSIX 上的 pwsh)。
+- **输入回显不可避免**:PowerShell 的 PSReadLine 会把提交的输入渲染回终端流,且没有 `stty -echo` 的对应物。完整结果中 marker 锚定提取排除回显;包装器原文剥离覆盖回退路径,但跨越终端宽度的包装器折行可能在部分输出结果中残留片段回显,受 `maxOutputChars` 约束。
+- 模型命令中的裸 ESC 字符不受支持:PSReadLine 会在执行前吞掉它们。包装器转义它需要的控制字节(`[char]27` 构造的 OSC 标记、body 的反引号转义)。
+- 模型重定义 `prompt` 函数会移除就绪标记;shell 随后退化为静默档而非 marker 快路径。
+- 命令执行期间没有交互 stdin:读取输入的前台命令会阻塞到就绪超时,随后重置 shell。
+- SIGTSTP/SIGHUP 在 Windows 不可用(backend 拒绝);SIGINT 以控制台级 Ctrl-C 输入写入投递,在提示符处取消当前行而非向进程发信号。
+- 在 Windows ACL 沙箱的只读模式下,pwsh 以 ConstrainedLanguage 启动,可能拒绝 prompt 函数的 `[Console]::` 调用;backend 文档化的 `Write-Host -NoNewline` 回退由 Windows-native 车道证据裁决。
+- BEL 终结的 OSC 标记仍只是就绪信号;面向模型的 BEL 事件通道保持延后,与当前实现对齐。

+ 2 - 2
packages/subprocess/subprocess-local/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/subprocess/subprocess-local/README.md
-README.md: 2817e02861db6caad89cad258d14a90c34afcbaf
-README.zh.md: 251b994a35e8bd7c84827957a548a1f352583ac6
+README.md: 0685cb32e4c80589de7a1f284712655a5de05b37
+README.zh.md: 1442d7c62e67c5d006f4b0a43d9ea090dbf2f284

+ 2 - 2
packages/subprocess/subprocess-local/README.md

@@ -11,7 +11,7 @@ Local Service provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/REA
 - **Credential scrub + explicit merge** — `process.env` minus credential-shaped vars (`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names; the spec's explicit `env` merges after that scrub with no namespace validation, so a deliberately supplied credential or current `DSH_*` fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
 - **Offset-based reads** — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement.
 - **Executable lookup** — `resolveExecutable` checks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions; relative paths containing separators are rejected at the seam, and relative PATH entries resolve from the host process cwd.
-- **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. The higher PTY backend owns prompt readiness, buffers, and model-facing operations.
+- **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. On Windows the koffi-backed inspector enumerates the process table through Toolhelp32 with GetProcessTimes start identities, reports the shell pid as the pseudo foreground group (Windows has no POSIX groups), and teardown verifies the shell's absence through those identities because externally taskkilled shells may never fire node-pty's exit notification. The higher PTY backend owns prompt readiness, buffers, and model-facing operations.
 - **Terminate-and-join disposal** — the service retains live handles only so its own disposal can escalate every running tree and await its exit; settled and spawn-failed handles leave the live set on settlement.
 
 ## Model Experience
@@ -25,7 +25,7 @@ No direct invalidation; the named consumers own any request-prefix changes.
 ## Known Limitations and Deferred Work
 
 - **Windows tree support is best-effort** — termination routes through `taskkill /PID <pid> /T /F` with all outcomes contained (absent tree, races, missing binary), and liveness falls back to the direct-child boundary.
-- **Terminal process inspection is Linux/macOS only** — the terminal primitive fails when its inspector has no supported platform implementation; Linux exact probes cover x64 and arm64, while macOS uses `ps` snapshots.
+- **Windows terminal signalling is console-wide** — SIGINT is delivered as a `\x03` Ctrl-C input write that conhost turns into a console-wide CTRL_C event; SIGTSTP and SIGHUP are rejected as unavailable; a `taskkill` without `/F` does not terminate console processes, so the teardown TERM tier is a grace wait before the `/F` escalation. Windows readiness has no exact stdin-wait tier: the prompt-marker fast path compares the shell pid as the pseudo foreground group, and silence/timing tiers cover the rest.
 - **A daemonized terminal descendant can still escape the observable boundary** — on macOS, a child that reparents before any foreground-inspection snapshot is no longer discoverable from the `node-pty` root; on Linux, a child that calls `setsid` leaves both the tree and owned terminal session. The local provider does not add a continuous process-table monitor.
 - **The credential scrub is a name heuristic** — `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSPHRASE*`) pass through, and a whitelist for over-scrubbed vars is noted future work.
 - **Completed spill files are not deleted** — bounded full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them; oversize incomplete spills are discarded and deletion is attempted immediately, but a cleanup failure can leave a bounded file behind.

+ 2 - 2
packages/subprocess/subprocess-local/README.zh.md

@@ -11,7 +11,7 @@
 - **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
 - **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
 - **可执行文件查找**:`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索;含分隔符的相对路径在该能力入口被拒绝,相对 PATH 条目从宿主进程 cwd 解析。
-- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
+- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。在 Windows 上,基于 koffi 的检查器通过 Toolhelp32 枚举进程表并取 GetProcessTimes 启动身份,把 shell pid 作为伪前台进程组(Windows 没有 POSIX 进程组),拆卸则通过这些身份验证 shell 已消失——因为被外部 taskkill 的 shell 可能永远不会触发 node-pty 的退出通知。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
 - **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。
 
 ## 模型体验
@@ -25,7 +25,7 @@
 ## 已知限制与暂缓事项
 
 - **Windows 进程树支持仅为尽力而为**:终止经由 `taskkill /PID <pid> /T /F` 完成,所有结果都被就地吸收,不向外抛出(进程树已不存在、竞态、二进制缺失),存活探测则回退到直接子进程边界。
-- **终端进程检查仅支持 Linux/macOS**:检查器没有受支持的平台实现时,终端原语会失败;Linux 精确探针覆盖 x64 与 arm64,macOS 则使用 `ps` 快照
+- **Windows 终端信号是控制台级的**:SIGINT 以 `\x03` Ctrl-C 输入写入投递,由 conhost 转为控制台级 CTRL_C 事件;SIGTSTP 与 SIGHUP 被拒绝(不可用);不带 `/F` 的 `taskkill` 无法终止控制台进程,因此拆卸的 TERM 档是 `/F` 升级前的宽限等待。Windows 就绪没有精确的 stdin-wait 档:prompt-marker 快路径把 shell pid 作为伪前台进程组比较,其余由静默/计时档覆盖
 - **守护化的终端后代仍可能逃出可观察边界**:在 macOS 上,子进程如果在任何前台检查快照之前重新设定父进程,将无法再从 `node-pty` 根进程发现;在 Linux 上,调用 `setsid` 的子进程会同时离开进程树与自有终端会话。本地提供方不会新增持续进程表监视器。
 - **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSPHRASE*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。
 - **不会删除已完成的 spill 文件**:有界的完整输出恢复文件(以及每个进程的私有 spill 目录)会在 OS tmpdir 下累积,直到外部机制进行清理;超大的不完整 spill 会被丢弃并立即尝试删除,但清理失败可能留下一个有界文件。