DeepSeek Harness 持久终端把 PSReadLine 预测补全带进上下文?三种关闭路径与验证
如果你在 Windows 上打开持久终端,terminal_send 返回的 viewport 里出现一串没敲过的旧命令,那不是会话串台,而是 PSReadLine 的预测补全被当成输入收进了模型上下文。 据讨论 #10070 的复现记录,幽灵文本来自系统级共享的 ConsoleHost_history.txt,执行 Set-PSReadLineOption -PredictionSource None 后立刻消失。下面先拆症状和外发链路,再给三条可替换的关闭路径。
DeepSeek Harness 持久终端里没敲过的旧命令从哪来
先分清三样东西:预测幽灵、逐字符重绘、真实输入。 三者在外观上都是文本,但来源完全不同,混着看容易误判成会话泄漏。
- 发一次
terminal_send "echo AAA",预期 viewport 出现echo a这类没输入过的行,那就是历史前缀匹配出的预测幽灵。 - 看行形态,预期
e/ec/ech/echo这种阶梯是逐字符重绘帧,不是模型或用户输入。 - 对照
terminal_read历史并执行Get-History,预期只有本会话两条、历史隔离正常,可以排除跨会话泄漏。 - 在同一会话先执行
Set-PSReadLineOption -PredictionSource None,再发echo ZZZ,预期幽灵消失、阶梯仍在,根因锁定在预测源。 - 换一台没有历史文件的 Windows 账户再试,预期幽灵消失,进一步确认来源是
ConsoleHost_history.txt。
DeepSeek Harness 终端字节怎样进入模型上下文
模型看到的不是屏幕,而是 sanitizer 拼接后的文本流,所以终端里的预览也会变成 prompt。 链路一共四步:
terminal_open启动一个新的 pwsh 进程,参数是-NoLogo -NoProfile,预期进程环境里没有自定义 profile。- PSReadLine 读取
ConsoleHost_history.txt,预期把前缀匹配的历史命令渲染成 faint/italic 幽灵。 TerminalSanitizer删除全部 ANSI,包括标记幽灵样式的 SGR,预期幽灵变成普通文本。terminal_send把拼接结果作为 viewport 返回,预期下一轮 LLM 请求原样外发。
幽灵一旦进入 viewport,就不再是本地显示问题,而是随请求持续外发。 据讨论,外发分四层:
- 每次 LLM 请求都会重发上下文里已有的 viewport,预期幽灵被反复发送。
submit=false只去掉结尾回车,预期按前缀输入ssh、export就能定向钓出匹配的历史。- 这条通道不执行命令、不读文件,预期不会留下文件读取类的审计记录。
- 幽灵内容可能包含凭据命令、内网地址和私有路径,预期默认预测源加一个不提交的 send 就足够触发。
DeepSeek Harness 根因:预测源默认开启,sanitizer 只过滤流
据讨论中社区核对源码,terminal-bash 包内没有任何一处调用 Set-PSReadLineOption,默认参数只有 -NoLogo -NoProfile。 终端子系统的实现细节可以对照 终端子系统说明 看,这里只留两层原因:
- 交互式 pwsh 默认
PredictionSource = HistoryAndPlugin;用非交互-Command探测却会显示None,预期你会看到这个误导性结果,很容易误判为已关闭。 TerminalSanitizer明确只做流过滤、不做屏幕模拟:CSI 分支跳过参数并丢弃光标定位,预期重绘帧拼在同一行;\r到\n的改写只影响行提交;\x08原样透传。- 终端里的擦除本质是「再画一次」,不是撤回,预期任何过滤改写都删不掉幽灵,只能从源头关掉预测。
- 这两层原因叠在一起,预期任何只在显示层做清理的方案都会复发。
DSH插件排障时容易踩的四个误判
这四条都会把排查带偏,逐条对照可以少走弯路。 它们分别错在进程、探测方式、过滤层和噪声来源上:
- 把旧命令当成跨会话泄漏,实际每次
terminal_open都是新进程,历史文件是操作系统级共享的,预期你会在Get-History里看到隔离正常的本会话记录。 - 用
-Command探测PredictionSource得到None,非交互模式不加载预测,预期这个结果没有代表性。 - 以为 sanitizer 能擦掉幽灵,它是流过滤器,判断「屏幕上还剩什么」需要屏幕状态,预期你会看到幽灵仍留在 viewport。
- 以为关掉预测就消掉所有重复文本,逐字符重绘帧来自另一条路径,预期阶梯文本仍然存在。
DeepSeek Harness 持久终端的三条关闭路径
三条路都能关掉历史那一半,差别在改动位置:配置、包内默认值,还是换一个 shell。 如果你在自己的 DSH插件里封装 terminal_send,同样按这三层排查。
路径一:shellArgs 加 -Command 关闭预测源
这是不改代码、立刻能用的止血方式,代价是自定义参数会整体替换方言默认。 据讨论,shellArgs 非空时默认的 -NoLogo -NoProfile 不再生效,必须重复写全。
- 打开 DSH 配置里持久终端插件的配置项,找到
shellArgs,预期能看到当前的参数数组。 - 写入下面这组参数,保留
-NoExit让进程在命令执行后继续交互,预期保存后配置里包含四条参数。 - 保存配置并重启应用,确认旧的 shell 进程已经退出,预期任务管理器里没有残留 pwsh。
- 重新打开终端,发
echo AAA,预期 viewport 不再出现历史旧命令,只剩逐字符重绘的阶梯文本。
{
"shellArgs": ["-NoLogo", "-NoProfile", "-NoExit", "-Command", "Set-PSReadLineOption -PredictionSource None"]
}
路径二:改包内默认配置,让所有会话生效
如果要让所有 profile、所有新会话都默认关闭预测,就把这一行放进包内的启动路径。 据讨论,PWSH_PROMPT_SETUP 目前只定义 prompt,是成本最低的落点;改 DEFAULT_PWSH_ARGS 同样可行。
- 在
terminal-bash的config.ts找到DEFAULT_PWSH_ARGS,或找到PWSH_PROMPT_SETUP的拼接位置,预期能定位默认参数或提示符初始化脚本。 - 追加
-NoExit -Command "Set-PSReadLineOption -PredictionSource None",或在提示符初始化脚本里加同名命令,预期每个新 pwsh 进程都会收到关闭预测的命令。 - 重新构建并加载插件,新开一次
terminal_open,预期新包已经生效。 - 在会话里执行
Get-PSReadLineOption | Select-Object PredictionSource,预期输出为None;升级包后需要重新应用,因为上游尚未修复合入。
路径三:改用 bash 终端绕开 PSReadLine
不依赖 PowerShell 的预测机制,直接把该会话切到 bash 方言最省事。 适合本来就在 Git Bash 或 WSL 里工作的项目。
- 在持久终端配置里把方言或后端从 pwsh 换成 bash,预期配置指向 bash 方言。
- 重开终端,确认提示符已经是 bash 而不是 PowerShell,预期不再加载 PSReadLine。
- 发同样的命令并检查 viewport,预期没有 PSReadLine 幽灵。
- 不要沿用 pwsh 的
-Command参数,预期 bash 会在启动时因未知参数失败;Windows 上还需要可用的 bash 环境,路径分隔符与转义规则也会随之改变。
DeepSeek Harness 关闭预测后的验证与取舍
三条路径都只关掉历史那一半,逐字符重绘仍会留在 viewport。 据讨论,真正结构性的解法是改用屏幕快照,而不是继续改过滤器。
- 用
terminal_send只输入前缀ssh且不提交,预期 viewport 不再回显历史,按前缀钓历史的通道已关闭。 - 检查
terminal_open的 motd 和terminal_read历史,预期没有幽灵残留。 - 若仍看到多帧拼成一行,预期那是 sanitizer 的 CSI 丢弃行为,只能等基于
@xterm/headless的屏幕快照方案。 - 换一个全新 profile 再跑一遍上述三项检查,预期配置随 profile 生效;不要把「用户自己也能看到幽灵」当成同意外发,用户看到的是一行预览,模型收到的是拼接后的多帧文本。
- 重启应用后再发一次前缀输入,预期修复仍然生效,而不是只在当前进程里临时消失。
常见问题
DeepSeek Harness 持久终端里不是跨会话泄漏,而是 PSReadLine 的 inline prediction 把历史命令画成了输入预览。据讨论 #10070,每次 terminal_open 都是新 pwsh 进程,Get-History 只有本会话两条,PTY 也没有复用;旧命令来自系统级共享的 ConsoleHost_history.txt。执行 Set-PSReadLineOption -PredictionSource None 后幽灵立即消失。
DSH插件把幽灵文本当成输入收进了模型上下文。用户在真实终端里看到的是一行 faint 预览,模型收到的却是 sanitizer 去掉 ANSI 后拼接的多帧文本,并随下一次 LLM 请求发给提供商。两者不是同一份披露,而且 submit=false 时按前缀输入还能定向钓出历史命令。
DeepSeek Harness 持久终端的非空 shellArgs 会整体替换方言默认值,不是追加。据讨论中社区核对源码,默认参数只有 -NoLogo -NoProfile,所以自定义时要完整写成 -NoLogo -NoProfile -NoExit -Command 加关闭预测的命令,否则会丢掉无 Logo、无 profile 的行为。
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
来源
- deepseek-harness Discussion #10070:persistent terminal leaks PSReadLine prediction ghosts (shell history) into the model context· deepseek-ai(GitHub Discussions)