DeepSeek Harness 持久终端把 PSReadLine 预测补全带进上下文?三种关闭路径与验证

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessPSReadLine持久终端模型上下文terminal_send
持久终端返回的 viewport 里出现没敲过的旧命令,是 PSReadLine 预测补全与逐字符重绘帧被拼进了模型上下文。按 shellArgs 注入 -Command、改包内默认配置、改用 bash 终端三条路径关闭预测源,并给出外发链路的验证方法。

如果你在 Windows 上打开持久终端,terminal_send 返回的 viewport 里出现一串没敲过的旧命令,那不是会话串台,而是 PSReadLine 的预测补全被当成输入收进了模型上下文。 据讨论 #10070 的复现记录,幽灵文本来自系统级共享的 ConsoleHost_history.txt,执行 Set-PSReadLineOption -PredictionSource None 后立刻消失。下面先拆症状和外发链路,再给三条可替换的关闭路径。

DeepSeek Harness 持久终端里没敲过的旧命令从哪来

先分清三样东西:预测幽灵、逐字符重绘、真实输入。 三者在外观上都是文本,但来源完全不同,混着看容易误判成会话泄漏。

  1. 发一次 terminal_send "echo AAA",预期 viewport 出现 echo a 这类没输入过的行,那就是历史前缀匹配出的预测幽灵。
  2. 看行形态,预期 e / ec / ech / echo 这种阶梯是逐字符重绘帧,不是模型或用户输入。
  3. 对照 terminal_read 历史并执行 Get-History,预期只有本会话两条、历史隔离正常,可以排除跨会话泄漏。
  4. 在同一会话先执行 Set-PSReadLineOption -PredictionSource None,再发 echo ZZZ,预期幽灵消失、阶梯仍在,根因锁定在预测源。
  5. 换一台没有历史文件的 Windows 账户再试,预期幽灵消失,进一步确认来源是 ConsoleHost_history.txt。

DeepSeek Harness 终端字节怎样进入模型上下文

模型看到的不是屏幕,而是 sanitizer 拼接后的文本流,所以终端里的预览也会变成 prompt。 链路一共四步:

  1. terminal_open 启动一个新的 pwsh 进程,参数是 -NoLogo -NoProfile,预期进程环境里没有自定义 profile。
  2. PSReadLine 读取 ConsoleHost_history.txt,预期把前缀匹配的历史命令渲染成 faint/italic 幽灵。
  3. TerminalSanitizer 删除全部 ANSI,包括标记幽灵样式的 SGR,预期幽灵变成普通文本。
  4. terminal_send 把拼接结果作为 viewport 返回,预期下一轮 LLM 请求原样外发。

幽灵一旦进入 viewport,就不再是本地显示问题,而是随请求持续外发。 据讨论,外发分四层:

  1. 每次 LLM 请求都会重发上下文里已有的 viewport,预期幽灵被反复发送。
  2. submit=false 只去掉结尾回车,预期按前缀输入 ssh、export 就能定向钓出匹配的历史。
  3. 这条通道不执行命令、不读文件,预期不会留下文件读取类的审计记录。
  4. 幽灵内容可能包含凭据命令、内网地址和私有路径,预期默认预测源加一个不提交的 send 就足够触发。

DeepSeek Harness 根因:预测源默认开启,sanitizer 只过滤流

据讨论中社区核对源码,terminal-bash 包内没有任何一处调用 Set-PSReadLineOption,默认参数只有 -NoLogo -NoProfile。 终端子系统的实现细节可以对照 终端子系统说明 看,这里只留两层原因:

  1. 交互式 pwsh 默认 PredictionSource = HistoryAndPlugin;用非交互 -Command 探测却会显示 None,预期你会看到这个误导性结果,很容易误判为已关闭。
  2. TerminalSanitizer 明确只做流过滤、不做屏幕模拟:CSI 分支跳过参数并丢弃光标定位,预期重绘帧拼在同一行;\r 到 \n 的改写只影响行提交;\x08 原样透传。
  3. 终端里的擦除本质是「再画一次」,不是撤回,预期任何过滤改写都删不掉幽灵,只能从源头关掉预测。
  4. 这两层原因叠在一起,预期任何只在显示层做清理的方案都会复发。

DSH插件排障时容易踩的四个误判

这四条都会把排查带偏,逐条对照可以少走弯路。 它们分别错在进程、探测方式、过滤层和噪声来源上:

  1. 把旧命令当成跨会话泄漏,实际每次 terminal_open 都是新进程,历史文件是操作系统级共享的,预期你会在 Get-History 里看到隔离正常的本会话记录。
  2. 用 -Command 探测 PredictionSource 得到 None,非交互模式不加载预测,预期这个结果没有代表性。
  3. 以为 sanitizer 能擦掉幽灵,它是流过滤器,判断「屏幕上还剩什么」需要屏幕状态,预期你会看到幽灵仍留在 viewport。
  4. 以为关掉预测就消掉所有重复文本,逐字符重绘帧来自另一条路径,预期阶梯文本仍然存在。

DeepSeek Harness 持久终端的三条关闭路径

三条路都能关掉历史那一半,差别在改动位置:配置、包内默认值,还是换一个 shell。 如果你在自己的 DSH插件里封装 terminal_send,同样按这三层排查。

路径一:shellArgs 加 -Command 关闭预测源

这是不改代码、立刻能用的止血方式,代价是自定义参数会整体替换方言默认。 据讨论,shellArgs 非空时默认的 -NoLogo -NoProfile 不再生效,必须重复写全。

  1. 打开 DSH 配置里持久终端插件的配置项,找到 shellArgs,预期能看到当前的参数数组。
  2. 写入下面这组参数,保留 -NoExit 让进程在命令执行后继续交互,预期保存后配置里包含四条参数。
  3. 保存配置并重启应用,确认旧的 shell 进程已经退出,预期任务管理器里没有残留 pwsh。
  4. 重新打开终端,发 echo AAA,预期 viewport 不再出现历史旧命令,只剩逐字符重绘的阶梯文本。
json
{
  "shellArgs": ["-NoLogo", "-NoProfile", "-NoExit", "-Command", "Set-PSReadLineOption -PredictionSource None"]
}

路径二:改包内默认配置,让所有会话生效

如果要让所有 profile、所有新会话都默认关闭预测,就把这一行放进包内的启动路径。 据讨论,PWSH_PROMPT_SETUP 目前只定义 prompt,是成本最低的落点;改 DEFAULT_PWSH_ARGS 同样可行。

  1. 在 terminal-bash 的 config.ts 找到 DEFAULT_PWSH_ARGS,或找到 PWSH_PROMPT_SETUP 的拼接位置,预期能定位默认参数或提示符初始化脚本。
  2. 追加 -NoExit -Command "Set-PSReadLineOption -PredictionSource None",或在提示符初始化脚本里加同名命令,预期每个新 pwsh 进程都会收到关闭预测的命令。
  3. 重新构建并加载插件,新开一次 terminal_open,预期新包已经生效。
  4. 在会话里执行 Get-PSReadLineOption | Select-Object PredictionSource,预期输出为 None;升级包后需要重新应用,因为上游尚未修复合入。

路径三:改用 bash 终端绕开 PSReadLine

不依赖 PowerShell 的预测机制,直接把该会话切到 bash 方言最省事。 适合本来就在 Git Bash 或 WSL 里工作的项目。

  1. 在持久终端配置里把方言或后端从 pwsh 换成 bash,预期配置指向 bash 方言。
  2. 重开终端,确认提示符已经是 bash 而不是 PowerShell,预期不再加载 PSReadLine。
  3. 发同样的命令并检查 viewport,预期没有 PSReadLine 幽灵。
  4. 不要沿用 pwsh 的 -Command 参数,预期 bash 会在启动时因未知参数失败;Windows 上还需要可用的 bash 环境,路径分隔符与转义规则也会随之改变。

DeepSeek Harness 关闭预测后的验证与取舍

三条路径都只关掉历史那一半,逐字符重绘仍会留在 viewport。 据讨论,真正结构性的解法是改用屏幕快照,而不是继续改过滤器。

  1. 用 terminal_send 只输入前缀 ssh 且不提交,预期 viewport 不再回显历史,按前缀钓历史的通道已关闭。
  2. 检查 terminal_open 的 motd 和 terminal_read 历史,预期没有幽灵残留。
  3. 若仍看到多帧拼成一行,预期那是 sanitizer 的 CSI 丢弃行为,只能等基于 @xterm/headless 的屏幕快照方案。
  4. 换一个全新 profile 再跑一遍上述三项检查,预期配置随 profile 生效;不要把「用户自己也能看到幽灵」当成同意外发,用户看到的是一行预览,模型收到的是拼接后的多帧文本。
  5. 重启应用后再发一次前缀输入,预期修复仍然生效,而不是只在当前进程里临时消失。

来源:Discussion #10070。

常见问题

DeepSeek Harness 持久终端的 viewport 出现没敲过的命令,是跨会话泄漏吗?

DeepSeek Harness 持久终端里不是跨会话泄漏,而是 PSReadLine 的 inline prediction 把历史命令画成了输入预览。据讨论 #10070,每次 terminal_open 都是新 pwsh 进程,Get-History 只有本会话两条,PTY 也没有复用;旧命令来自系统级共享的 ConsoleHost_history.txt。执行 Set-PSReadLineOption -PredictionSource None 后幽灵立即消失。

为什么 DSH插件里也算数据外发,用户自己终端也能看到预测补全?

DSH插件把幽灵文本当成输入收进了模型上下文。用户在真实终端里看到的是一行 faint 预览,模型收到的却是 sanitizer 去掉 ANSI 后拼接的多帧文本,并随下一次 LLM 请求发给提供商。两者不是同一份披露,而且 submit=false 时按前缀输入还能定向钓出历史命令。

DeepSeek Harness 持久终端配 shellArgs 关闭预测,为什么要重复写默认参数?

DeepSeek Harness 持久终端的非空 shellArgs 会整体替换方言默认值,不是追加。据讨论中社区核对源码,默认参数只有 -NoLogo -NoProfile,所以自定义时要完整写成 -NoLogo -NoProfile -NoExit -Command 加关闭预测的命令,否则会丢掉无 Logo、无 profile 的行为。

DeepSeek Harness 里关掉 PSReadLine 预测源后,逐字符重绘的阶梯文本为什么还在?

DeepSeek Harness 里那部分噪声来自 TerminalSanitizer 的流过滤方式,不是预测源。sanitizer 丢弃 CSI 后重绘帧会拼在同一行,\r 又被改写成 \n,所以逐键帧无法被撤回。幽灵可以关掉,重绘帧只能等屏幕快照方案,据讨论包内已依赖 @xterm/headless。

相关术语

PSReadLine inline prediction(预测补全)
PSReadLine inline prediction 是 PowerShell 行编辑器把历史或插件建议的补全以 faint/italic 样式直接画在光标后的功能。交互式 pwsh 默认 PredictionSource 为 HistoryAndPlugin,因此新进程也会读取系统级历史文件并显示幽灵文本。— https://github.com/deepseek-ai/deepseek-harness/discussions/10070
TerminalSanitizer(终端流过滤器)
TerminalSanitizer 是 DSH 持久终端把 PTY 原始字节转成模型可读文本的过滤层。它不做屏幕模拟,只删除 ANSI 控制序列并规范化换行,因此无法判断屏幕上最终还剩什么。— https://github.com/deepseek-ai/deepseek-harness/discussions/10070
viewport(终端视口)
viewport 是 terminal_send 返回给模型的终端可见文本快照。它由 sanitizer 处理后的字节拼接而成,会作为模型上下文的一部分进入后续 LLM 请求。— https://github.com/deepseek-ai/deepseek-harness/discussions/10070

来源