dsh-loop-guard
Manifest validA passive watchdog for DeepSeek Harness: watches a turn's thinking and text streams and cancels it when the model starts repeating itself.
循环守卫 · dsh-loop-guard
DSH 的独立插件:监视模型的思考流和正式输出流,发现重复内容后报警,并可自动停止当前回合。无需群聊插件;功能坞可选,不构成硬依赖。
安装与入口
兼容当前验证的 DSH 0.2.0-rc.2;包声明范围为 >=0.2.0-0 <0.3.0。宿主需要提供 webServer 和 agent 运行时,以及 @deepseek-ai/schemastery ^3.18.1。
在包含本文件与 package.json 的插件目录中安装:
dsh plugin --profile desktop add "link:$($PWD.Path)"
从 GitHub 安装(无需先克隆):
dsh plugin --profile desktop add "github:XiaoSHang05/dsh-loop-guard"
安装或更新宿主代码后重启 DSH,然后刷新原来的网页。对话区会出现「监控」标签页。报警挂在全局浮层,无需一直停留在监控页。已安装过旧目录时,请先在插件管理器里处理旧链接,再装交付目录,避免两个目录的同名插件混用。
该命令走 DSH 的包安装流程。若现有 profile 的其他依赖触发包龄等供应链限制,安装可能失败;不要为本插件放宽整个 profile 的安全策略。检查失败信息后再处理对应依赖。本机此前已登记开发目录链接,交付文件夹不会自动替换那个链接。
使用
- 默认监视全部会话,思考与输出分别检测。
- 「启用监控」控制全局开关;会话行可以暂停/恢复单独监控。
- 「重复次数」控制连续周期重复阈值,默认 5。
- 「重复行占比」控制独立的重复行检测,默认最近 8 行中 75% 是已出现的行。调高重复次数不会关闭这条规则,需要同时调整重复行占比。
- 「编号循环占比」控制编号循环检测(第三条规则),默认 0.6。它抓的是「同一句式、每行只换一个数字」这类复读,见下节。
- 命中后会显示会话、流、重复片段、次数与打断状态。报警不会自动消失,点击「知道了」才关闭;关闭请求失败时卡片保留。
- 提示音使用 WebAudio。浏览器可能要求先与页面交互才能播放声音;声音不可用不影响报警卡片或宿主打断。
- 配置
cancel: false可切换为只报警;alarm: false不显示卡片;sound: false关闭声音。 - 功能坞的客户端桥接可用时尝试注册入口;桥接不可用时仍可使用监控页和独立报警。
检测与边界
- 周期重复:在尾部窗口寻找连续相同片段;展示时把英文周期旋转到可读词边界。
- 自定义短语(默认关闭):你自己填几句最怕它复读的话,一行一句、每句至少两个字,出现
customRepeat(默认 5)次就报警。默认不填、不工作;在监控页打开开关并输入即生效。抓的是前三把尺子都没覆盖的形状——不同模型卡住的句子不一样,与其我们猜,不如你把它贴进来。正常行文里提到几次不算:命中要求这几遍落在一个很短的跨度里,因此长文里引用你的句子多少次都不会触发。 - 编号循环:把每行的数字抹成同一个占位符后比较句式,连续 ≥12 行同一句式且这些行里字母占字母加数字的比例 ≥60% 才算命中。抓的是
第 1 步:读取配置文件完成。/第 2 步:…这类每行都不同的假进度复读——它在字符和整行两个尺度上都不重复,另外三条规则看不见它。实测它可以在监控面板进度条始终 0% 的情况下白刷 6692–9492 字。 - 重复行密度:只归一化行首尾和连续空白,再比较相同的行。数字不同仍是不同内容,不把正常进度列表、带变化计数的重试日志视为重复行。
- 一次命中锁存:同一流在同一请求中只报告一次;新请求或重新配置时清空检测缓存。
四条规则按 周期重复 → 自定义短语 → 编号循环 → 重复行密度 的顺序问,先命中的算数:确切的复读不会被报成更模糊的信号;而你亲手填的句子比我们推导出来的句式更具体——它不是最具体的那条(逐字复读更具体),但比另外两条推测更具体。
阈值来自实测,不是估计:把这台机器有史以来记录的 970 MB 输出(约 4.2 万条正常回答)全部喂进真实检测器,最重的「同句式连跑」是 12 行、字母占比 34%(一份文件清单);编号循环最轻的一条是 67%。60% 落在这两者之间。12 行的余量来自同一份语料:正常输出里 8 行以上的同句式连跑,字母占比上限是 50%。
已知边界(不是意外,是取舍):一条很长的正常枚举也会命中,例如 port 8001 is listening 连写 20 行。它与编号循环在语法上是同一件东西——一句话加一个新数字,没有任何东西能说明它会不会停——字母占比也分不开(那条清单是 79%,比最轻的真循环还高)。只能在「两个都报」和「两个都不报」之间选:报错一条长清单的代价是一张可关闭的报警卡,漏掉循环的代价是整个回合。不认同这个取舍的可以用 structLines: 0 关掉这条规则,或把行数阈值调高。
宿主通过 agent/assistant-stream 全局订阅实时增量,命中后调用:
agent.cancel(
{ kind: 'hook', reason: 'loop-guard: repeated output' },
{ keepInbox: true },
)
这是回合级止损,不是逐段撤回:已经输出的字无法收回。保留排队输入不等于撤销已执行的工具操作。正常文章故意重复很多次也可能被打断,可按场景暂停监控或改成只报警。
只由空白或单个不同字符组成的片段会被排除,避免分隔线误报;因此单字符刷屏不是本版的检测目标。超出窗口长度/周期长度的重复也可能漏检。未加载插件之前的输出不能回溯检测。
配置
监控页的修改先立即作用于本插件,再尽力通过宿主 Settings 持久化。若 Settings 缺席或写入失败,配置仅保存在内存中。会话级暂停/恢复名单和命中历史均为内存数据,重启后清空。历史最多 50 条,默认界面展示最新 20 条;命中历史标题右侧的「清除历史」清空全部记录——只清记录,不影响任何配置,也不会恢复已经被打断的回合。
| 配置键 | 默认值 | 含义 |
|---|---|---|
| enabled | true | 全局监控开关 |
| minRepeat | 5 | 连续周期重复阈值 |
| minUnit / maxUnit | 6 / 1024 | 搜索周期长度范围 |
| windowChars | 8192 | 各流保留的尾部窗口 |
| densityLines / densityRatio | 8 / 0.75 | 重复行窗口与占比 |
| structLines / structShare | 12 / 0.6 | 编号循环的行数与字母占比;structLines: 0 关闭该规则 |
| customPhrases | [] | 你自己填的句子,一行一句;空数组即关闭该规则(默认关闭) |
| customRepeat | 5 | 上面那些句子出现几次算循环 |
| cancel / alarm / sound | true | 自动打断、报警卡片、提示音 |
| scopeDefault | all | 默认全部监控;selected 仅监控显式选中的会话 |
checkEvery=32 是核心内部默认扫描步长,不是宿主 HTTP 配置字段。watchText、watchReasoning、excludedSessions 不属于本版配置 API。
结构
src/detect.js:纯检测核心。src/index.js:宿主半,包含流订阅、配置、命中记录、取消与 HTTP/SSE。src/client.js、src/client.css:监控页与报警。lib/client.js:已构建的浏览器模块,无需再构建即可安装。cordis.patch.yml:安装条目,id 为loop-guard。CONTRACT.md:跨半数据契约。VALIDATION.md:交付验证记录与证据边界。scripts/:构建、检查、测试与独立交付目录打包。_probe/:测试台与语料探针(离线,不随插件运行)。
读写接口位于 /dsh-loop-guard/api/:health、state、config、monitor、hits、events。SSE 只通知重新读取,状态以 GET state 为准;连接异常时退回轮询。接口使用宿主 Web 的认证边界,不额外提供公网认证。
开发与测试(完整源码目录)
交付文件夹与 GitHub 仓库包含源码、构建脚本和测试脚本,但不携带 node_modules。测试用的 React、jsdom 与 Schemastery 已声明为开发依赖,clone 之后先装一次:
npm install # 装 jsdom / react / react-dom / @deepseek-ai/schemastery
npm run build # 由 src/client.js + src/client.css 重建 lib/client.js
npm test # 静态校验 + 八组测试台
node _probe/mutation.mjs
npm test 会先跑 scripts/check.mjs(产物新鲜度、语法、越权能力、颜色、清单路径、机器相关路径),再跑八组测试台,共 232 个断言。变异台会临时改写八处源码并在 finally 中恢复,不要与其他编辑或构建并行运行;它专门证明台子能抓到 bug(把已知 bug 放回去必须变红),而不是只证明代码今天能跑。
回放语料用的探针(_probe/struct-real-fp.mjs)读取本机 DSH 会话日志,把全部 assistant 消息重放给真实检测器。它需要本机的会话日志,换一台机器会得到不同数字——编号循环的阈值就是这么定的,方法与数字写在 VALIDATION.md。
发布前还有一道历史检查,因为 git grep 只看当前工作区,看不见已经改掉的内容:
git log --all -p --no-color > history.txt
node _probe/history-scan.mjs history.txt
它找的是家目录、session- 开头的会话 id、令牌与私钥。这个仓库在公开之前用同一份表达式跑过一次 git filter-repo,把作者机器的路径与会话 id 从全部 13 次提交里清掉了。历史里的一个路径比当前目录里的一个路径更难撤回——已经 push 过的仓库,清理历史等于强制所有人重新 clone。
MIT,见 LICENSE。改动记录见 CHANGELOG.md。
Comments
Loading…