Back to home@zqh260619

dsh-dupguard

Real-time repetition guard for DeepSeek Harness (DSH): stops model generation the moment the same string repeats ≥10 times in the streamed output. 防复读守卫:实时检测 DSH 大模型输出中的重复内容并立即停止生成。

Stars
0
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 17, 2026

Introduction

dupguard · DSH 大模型重复输出守卫

dupguard — a real-time repetition guard for DeepSeek Harness (DSH): stops model generation as soon as the same string repeats ≥ 10 times (configurable) in the streamed output.

dupguardDeepSeek Harness (DSH) 的实时重复输出守卫插件:当最新输出中同一字符串连续重复 10 次及以上(可配置)时,立即停止本次生成。

npm version License: MIT CI dsh-plugin

触发后,已生成的内容会正常提交为助手消息,本轮对话干净结束——不会报错、不会丢弃输出、不会污染会话日志。

When triggered, the already-generated text is committed as a normal assistant message and the turn ends cleanly — no errors, no lost output, no session-log pollution.


特性 / Features

  • 实时检测:逐 token(text-delta)检测,复读出现即停,延迟为单个增量。
  • 多种复读形态:单字符循环、词语循环、带空格/换行分隔的复读均能识别(默认去空白后检测)。
  • 真正的服务端停止:提前关闭流迭代 → 适配器 consumer.abort() → 中断 HTTP 连接,模型在服务端停止生成。
  • 安全停止:绝不 abort() agent 步骤信号;补发协议合规的 block-end + finish(stop),消息正常提交。
  • 零依赖 / 零配置:纯 JavaScript,无运行时依赖;默认配置开箱即用。
  • 双入口交付:动态插件(plugin/host.js)+ npm 组合挂载(lib/index.js),行为一致、CI 防漂移。
  • 内置 DSH 兼容补丁fixStandingMountConflict,默认开启):幂等化 cordisInspect.register, 修复 DSH ≤ rc.6 的 preset standing-mount 多代并存冲突(见下文"已知限制")。

快速开始 / Quick Start

方式一:动态插件(无需安装,进程内生效)/ Dynamic plugin (no install)

plugin/host.js 的全部内容作为 code.host 提交给 cordis_define,再 cordis_run 激活即可:

  1. cordis_define:kind 选 new,idPrefix 例如 dupguardcode.host 填入 plugin/host.js 内容;
  2. cordis_run:激活返回的 packageId(首次使用 mode run)。

动态插件随 DSH 进程存在;重启后需重新 define + run。

Paste the entire content of plugin/host.js as code.host in cordis_define, then activate the returned packageId with cordis_run.

方式二:npm 安装 + 组合挂载(常驻,随 DSH 启动)/ npm + composition (persistent)

插件已发布到 npm:dsh-dupguard

1. 在 DSH profile 目录安装依赖(例如 web GUI 的 $DSH_HOME/profiles/web):

pnpm add dsh-dupguard      # 或 npm install dsh-dupguard

2. 在 profile 的用户补丁层 cordis.patch.yml 插入组合行

# $DSH_HOME/profiles/web/cordis.patch.yml
- insert:
    - id: dupguard
      name: dsh-dupguard
  • 用户补丁层在 bundle 层之后、--patch 之前应用;运行中的 DSH 通过 watchUserPatches 热重载它——保存即生效,无需重启;加载失败会事务性回滚,不影响应用。
  • 验证:dsh --profile web --dump-config 末尾应出现 dupguard 行;loader 日志会出现 apply plugin dupguard
  • 撤销:删掉该 insert 项即可热卸载。

本机当前状态profiles/web 的组合行仍以 file: URL 直连仓库 lib/index.js (发布前的过渡形态)。切换到 npm 包只需两步:在 $DSH_HOME/profiles/web 下执行 pnpm add dsh-dupguard,再把 cordis.patch.yml 中该行的 name 改为 dsh-dupguard

本地开发替代:未发布/调试时,name 也可直接用 file: URL 指向仓库内的 lib/index.js(CJS 导出 { name, apply },与 loader 的 unwrapExports 兼容,零构建)。

The plugin is published on npm as dsh-dupguard. Install it inside the DSH profile (e.g. pnpm add dsh-dupguard under $DSH_HOME/profiles/web), then insert the row { id: dupguard, name: dsh-dupguard } into the profile's user patch layer cordis.patch.yml. The running DSH hot-reloads that file (watchUserPatches) — no restart needed; a failed reload rolls back transactionally. Verify with dsh --profile web --dump-config or the loader log line apply plugin dupguard; remove the insert entry to uninstall.

Current state on this machine: the profiles/web row still points at the repo's lib/index.js via a file: URL (pre-publish transitional form). To switch to the npm package, run pnpm add dsh-dupguard in $DSH_HOME/profiles/web and change that row's name to dsh-dupguard. The file: URL form also remains handy for local development against lib/index.js (CJS, module.exports = { name, apply }, compatible with the loader's unwrapExports, no build step).


配置 / Configuration

修改 plugin/host.jslib/index.js 顶部 CONFIG 常量(两个入口需保持同步,CI 会校验一致性):

Edit the CONFIG block at the top of plugin/host.js / lib/index.js (both entries must stay in sync; CI verifies behavioral parity).

配置项 / Option默认 / Default说明 / Description
threshold10触发阈值:同一字符串连续重复 ≥ 该值时停止 / stop when the same string repeats ≥ this many times
minUnitLength1最小重复单元长度 / minimum repeating-unit length (1 also catches single-char loops like aaaaaaaaaa)
maxUnitLength80最大重复单元长度 / maximum repeating-unit length
detectionWindow8192检测滚动窗口(字符,去空白后)/ rolling detection window in chars (after whitespace removal)
stripWhitespacetrue检测前移除空白/换行,识别带分隔符的复读 / strip whitespace so "x x x" and "x\nx\nx" are caught
monitorReasoningfalse是否检测思考文本 / also guard reasoning (thinking) text — off by default, high false-positive risk
monitorToolArgumentsfalse是否检测工具调用参数 / also guard tool-call JSON args — off by default (base64/JSON repeats are common)
fixStandingMountConflicttrueDSH ≤ rc.6 兼容补丁:幂等化 cordisInspect.register,修复 preset standing-mount 多代并存冲突 / idempotent cordisInspect.register patch for the DSH ≤ rc.6 standing-mount conflict

工作原理 / How it works

1. 拦截流式输出 / Intercept the stream

监听 llm/stream 瀑布事件(包裹每次流式模型调用),返回包装后的 AsyncIterable。与 DSH 自带 @deepseek-ai/dsh-llm invariant 插件、dsh-session-checkpoint-policy 同款接入方式。

Listens to the llm/stream waterfall (wraps every streaming model call) and returns a wrapped AsyncIterable.

2. 检测算法 / Detection

  • 按块索引(chunk.index)分别累积文本,多块交替输出互不干扰;
  • 去空白后做尾部连续重复检测:文本以某个单元(长度 1..80)连续重复 ≥ 阈值结尾即触发。 模型一旦复读,重复必然在尾部,因此尾部检测即可实时捕获所有循环,同时避免全窗口词频的误报 (如正常中文里高频的"的")。

Tails-only consecutive-run detection on the whitespace-stripped buffer: catches every loop in real time without the false positives of whole-window frequency counting.

3. 停止机制 / Stopping

守卫生成器提前结束 → for await 调用上游 iterator.return() → 适配器 finallyconsumer.abort() 中断 HTTP 连接 → 服务端真正停止生成。绝不直接 abort() options.signal(对 loop 请求它就是 agent 步骤信号,直接中止会以 aborted 结束并丢弃消息)。

Graceful early end: iterator.return() propagates to the adapter, whose finally aborts the HTTP connection server-side. We never abort options.signal directly (for loop requests it is the agent step signal).

4. 协议合规收尾 / Protocol-compliant closure

停止时补发所有打开块的 block-end(携带完整已生成文本)与 finish{kind:'stop'},满足 llm-invariant 校验器要求;agent-loop 将已生成内容正常提交为助手消息。

Emits synthetic block-ends plus finish(stop) to satisfy the llm-invariant validator, so the agent-loop commits the partial text as a normal assistant message.


触发示例 / What gets stopped

形态 / Pattern示例 / Example
单字符循环 / single-char loopaaaaaaaaaa
词语循环 / word loop哈哈 ×10
带空格复读 / space-separatedhello hello hello ... ×10
逐行复读 / line repeats抱歉,我无法完成。 ×10 行
前缀后循环 / loop after prefix好的,下面开始回答: + 循环 ×10

不会触发 / Won't trigger:正常文本中的高频词(检测只针对连续重复)、重复 9 次及以下、 reasoning 与工具参数(默认关闭)。/ high-frequency words in normal prose (consecutive runs only), ≤9 repeats, reasoning and tool args (off by default).


项目结构 / Project layout

.
├── plugin/
│   └── host.js                 # 动态插件形式(cordis_define 的 code.host)
├── lib/
│   └── index.js                # npm/组合常驻形式(package.json main 入口)
├── tests/
│   ├── detector.test.js        # 端到端测试:15 项 × 2 入口(防漂移)
│   └── experiment-cancel.mjs   # 诊断实验(不进 CI):验证截停不阻塞于底层流取消
├── .github/workflows/ci.yml    # GitHub Actions:Node 18/20/22
├── package.json
├── CHANGELOG.md
├── LICENSE                     # MIT
└── README.md

测试 / Tests

node tests/detector.test.js   # 或 npm test

同一套 15 项用例分别驱动两个入口(plugin/host.jsnew Function 求值、lib/index.jsrequire 加载),覆盖:透传完整性、各类复读形态、阈值边界、协议闭合、上游 return() 调用、 默认不检测 reasoning/工具参数、未闭合工具调用块的闭合、多次调用状态隔离等。CI 在 Node 18/20/22 上运行。

The same 15-test suite drives both entries, guarding against drift between the two forms. CI runs on Node 18/20/22.


已知限制 / Limitations

  • 停止时若恰有未闭合的工具调用块(顺序输出块的适配器几乎不可能),该块会按已累积参数闭合并可能被执行。
  • 服务端停止依赖适配器在流关闭时中止底层请求的语义(已验证 dsh-llm-deepseek;自定义适配器需自查)。
  • 阈值语义为 >= threshold:第 10 次重复出现时即停止。

DSH 运行期间编辑 preset 后的 standing-mount 冲突(DSH ≤ rc.6 缺陷,本插件已内置补丁)

现象:对某个会话执行模型选择等操作时报 resume failed ... preset ... failed to mount ... Host Cordis inspect provider "Service" is already registered, 此后该错误持续出现,只有重启 DSH 才能恢复。

机制:preset 以 standing mount 方式每 preset 挂载一次并常驻;当 preset 的 composition 文件在 DSH 运行期间被编辑过(mtime/size 变化),下一次对"无活跃 agent 的会话"的操作(模型切换、打开历史会话等) 会新建一代 standing mount,而旧代从不销毁(DSH 注释明示 "a superseded one is never disposed while the process lives")。tool-cordis 在每次挂载时向进程全局cordisInspect 注册表注册 Service/Event/Builtin/Tool 四个 provider,新旧两代并存即冲突;失败的新代回滚、旧代残留, 重试永远重复冲突——这正是报错后"必须重启才能恢复"的原因。

本插件的修复(默认开启)apply 时把 cordisInspect.register 幂等化——同 id 已有注册时 共享既有注册并返回 no-op disposer,多代并存不再冲突。补丁进程内常驻(卸载本插件后仍生效, 重启后由本插件重新安装;HMR 重载不会叠加)。依赖 cordisInspect.providers 为可读 Map (rc.6 实测如此);DSH 升级修复后可将 CONFIG.fixStandingMountConflict 置为 false 关闭。

仍建议的操作纪律:运行期间编辑已挂载 preset 后重启 DSH(补丁消除的是报错,旧代残留的 组合仍占用资源,这是 DSH 的既有行为);根治仍待上游修复。

If you edit a mounted preset's agent.cordis.yml while DSH is running, the next session resume (triggered e.g. by the model picker on a session whose agent is gone) mounts a NEW standing-mount generation of that preset while the old generation is never disposed — tool-cordis then registers its process-global Host inspect providers (Service …) twice and every retry fails with Host Cordis inspect provider "Service" is already registered until DSH restarts. This plugin patches it by default: cordisInspect.register is made idempotent (a same-id registration shares the existing one and gets a no-op disposer), so coexisting generations no longer collide. The patch is process-resident (survives plugin unload, reinstalled on restart; HMR reload does not stack it); set CONFIG.fixStandingMountConflict to false once a fixed DSH ships.

License

MIT