diff --git a/packages/shell/tool-pwsh-persistent/README.i18n.yaml b/packages/shell/tool-pwsh-persistent/README.i18n.yaml index 7d88be90ce..60b8ae7682 100644 --- a/packages/shell/tool-pwsh-persistent/README.i18n.yaml +++ b/packages/shell/tool-pwsh-persistent/README.i18n.yaml @@ -33,8 +33,8 @@ en: 8d94a7381bca3fa9 zh: f4bd2866fff6fa8d /deepseek-ai-dsh-tool-pwsh-persistent/understand-the-implementation/design-philosophy: - en: b9b75c3251f29f02 - zh: 9114aad4726f6d0c + en: e03cb450010e45db + zh: dddef1a9aad7ffe9 /deepseek-ai-dsh-tool-pwsh-persistent/understand-the-implementation/source-map: en: 857e036f41c913ac zh: e9f383f7ee384c59 diff --git a/packages/shell/tool-pwsh-persistent/README.md b/packages/shell/tool-pwsh-persistent/README.md index 7f11c893a9..0bd03285be 100644 --- a/packages/shell/tool-pwsh-persistent/README.md +++ b/packages/shell/tool-pwsh-persistent/README.md @@ -76,6 +76,7 @@ This section explains the design decisions behind the tool and points at the cod - **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/archived/architecture/2026-08-11-pwsh-persistent-pty.md)). - **Backend-owned prompt readiness.** The shell's `prompt` function belongs to the backend: it prints a BEL-terminated OSC marker plus the controlled printable prompt. The backend accepts recognized prompt text through its fast path and otherwise uses its own readiness checks or silence tier. The tool neither installs nor matches a prompt of its own. +- **Readiness after scrolling and restart.** The [Loader composition test](tests/loader-composition.spec.ts) checks the complete send-settlement sequence, including a command on the same shell after large output and the first command after `exit`. Its failure timeline distinguishes the bundled OpenConsole used on Windows from the unused system conhost. - **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. diff --git a/packages/shell/tool-pwsh-persistent/README.zh.md b/packages/shell/tool-pwsh-persistent/README.zh.md index 3a25457bda..7f05af221f 100644 --- a/packages/shell/tool-pwsh-persistent/README.zh.md +++ b/packages/shell/tool-pwsh-persistent/README.zh.md @@ -76,6 +76,7 @@ kind: "package-reference" - **`dsh-tool-bash-persistent` 的刻意孪生。** 会话注册表、轮询循环与重置约定按设计镜像持久 bash 工具([pwsh 持久 PTY Agent Note](../../../.agents/notes/archived/architecture/2026-08-11-pwsh-persistent-pty.md))。 - **后端拥有提示符就绪。** shell 的 `prompt` 函数属于后端:它打印 BEL 结尾的 OSC 标记加受控的可打印提示符。后端通过快路径接受已识别的提示符文本,否则使用其自身的就绪检查或静默层级。工具既不安装也不匹配自己的提示符。 +- **滚屏与重启后的就绪。** [Loader 组合测试](tests/loader-composition.spec.ts)检查完整的 send 结算序列,包括大量输出后在同一 shell 上执行的命令,以及 `exit` 后的首条命令。失败时间线会区分 Windows 实际使用的自带 OpenConsole 与未使用的系统 conhost。 - **PSReadLine 回显靠锚定剥离。** PowerShell 会把提交的输入渲染回流中;标记锚定提取与包装源码剥离移除回显,而跨终端宽度换行的包装可能在部分输出结果中留下部分回显。 - **重置,而非修复。** 任何不确定状态——显式 `exit`、超时、发送失败、中止——都会关闭 shell 并让下一次调用从全新状态开始。 diff --git a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts index b8da4ba368..90c29923a0 100644 --- a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts @@ -32,12 +32,11 @@ const hasPwsh = spawnSync( { encoding: 'utf8' }, ).status === 0 -// Shell and console-host versions for a failure message: the Windows console host, not this -// process, decides how the prompt marker and its tail reach the session, and its build on the -// self-hosted pool is unknown. +// The system conhost version identifies the host OS; Windows PTYs use node-pty's bundled +// OpenConsole, so that version does not identify the console rendering these sessions. const HOST_FACTS_COMMAND = [ '"pwsh $($PSVersionTable.PSVersion) PSReadLine $((Get-Module PSReadLine -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version)"', - 'if ($env:OS -eq \'Windows_NT\') { "conhost $((Get-Item (Join-Path $env:SystemRoot \'System32\\conhost.exe\')).VersionInfo.FileVersion)" }', + 'if ($env:OS -eq \'Windows_NT\') { "system conhost (unused) $((Get-Item (Join-Path $env:SystemRoot \'System32\\conhost.exe\')).VersionInfo.FileVersion)" }', ].join('; ') function hostFacts(): string { @@ -51,9 +50,9 @@ function hostFacts(): string { return [ `host: ${process.platform} ${process.arch} ${release()} (${version()}); node ${process.version}`, `shell: ${pwshPath}; ${shell}`, - // dsh-subprocess-local spawns node-pty without useConptyDll, so the system console host, - // not the OpenConsole build node-pty bundles, renders every session. - 'conpty: system conhost', + process.platform === 'win32' + ? 'conpty: node-pty bundled OpenConsole (useConptyDll=true)' + : 'conpty: not applicable (POSIX PTY)', ].join('\n') } @@ -248,22 +247,23 @@ describe.skipIf(!hasPwsh)('persistent pwsh through a real cordis.yml Loader comp expect(large).toContain('') expect(large).not.toContain('beginning of this command output was dropped') + const afterScroll = text(await execute('after-scroll', 'Write-Output "cwd=$PWD keep=$env:KEEP"')) + expect(afterScroll).toBe(`cwd=${join(root, 'nested')} keep=loader`) + const exited = text(await execute('exit', 'exit')) expect(exited).toContain('next pwsh call starts from the workspace') expect(text(await execute('after-exit', 'Write-Output "$PWD"'))).toBe(root) - // Six commands settle on the controlled prompt; no send may fall back to the - // silence tier, which is the 3.3 s-per-call degradation this suite pins. The - // counts alone do not say which tier settled which send, so every reason the - // run recorded and the per-send timeline ride in the failure message (the - // self-hosted Windows lane reported one to two stdin_read settlements per - // run on 2026-09-25..27, every other send at the plain silence bound). The - // message is built only when an assertion is about to fail: formatting runs - // the host-facts shell probe, which a passing run must not pay for. - const stdinReads = settleReasons.filter(reason => reason === 'stdin_read').length - const degraded = stdinReads < 6 || settleReasons.includes('inferred_idle') + // Each ordinary command must settle on the prompt, including the next command on the + // scrolled shell and the first command after restart. Extra sends can conceal a timeout. + // Formatting probes host versions, so only a failed sequence pays that cost. + const expected: TerminalWaitReason[] = [ + 'stdin_read', 'stdin_read', 'stdin_read', 'stdin_read', + 'stdin_read', 'stdin_read', 'session_exit', 'stdin_read', + ] + const degraded = settleReasons.length !== expected.length + || settleReasons.some((reason, index) => reason !== expected[index]) const failure = degraded ? `${JSON.stringify(settleReasons)}\n${timeline.format()}` : '' - expect(stdinReads, failure).toBeGreaterThanOrEqual(6) - expect(settleReasons, failure).not.toContain('inferred_idle') + expect(settleReasons, failure).toEqual(expected) }, 120_000) })