Repository navigation
Releases
v0.2.5
修复
「窗口里出现过几次」不是复读的判据。 三条计数判据此前只看一个窗口内的共现次数,于是一次会话里连续误报五次:模型在推理里反复引用同一行 Java 语句、几条并列的 runcode 命令共享同一段前缀、以及它真在逐字打出 36 个 x 的图块——三次都被判成了循环。真正的复读是时间里的重复:等距推进,或一直延续到尾部。三处现场各需不同答案:
- 代码行不再被报告。
CODE_LINE补上「行尾分号」与「点号调用链」,命中时该行不参与「被报告的行」,但仍计入分母(只从分母里也删掉会反过来抬高代码密集流的占比)。 block-repeat要求重复块等距出现。count改报最长等距链长,原始出现次数留在rawCount里——并列命令的间距由内容长度决定,不可能相等。periodic-run否决非原始单元。 单元本身是更短串的整数次重复时("xxxxxxxx"是一个字符打了八遍)不算周期循环。
三条都是分类判断,不是抬阈值:lineCount 要从 3 提到 6 才挡得住第一例,而 6 已在真循环占据的区间内。复核口径:同一批语料上(chunk 7 / 41 / 200 三档各跑一遍),三处修改前后的触发集合逐字节相同,夹具仍是 166 健康 0 误报 / 11 退化全命中,而五条误报全部转为 inert。
real-protocol 测试不再因宿主构建缺模块而失败。 较新的 DSH 把语法校验门并进 dsh-llm/lib/index.js 而不是独立的 invariant.js,该测试改为按实际安装位置探测,缺失时降级为 skip——缺模块是宿主构建的属性,不是护栏的缺陷。
改动
每次中断后在窗口角落弹一条提示。 此前中断只留下终态错误与日志里的一行,得自己去翻日志才知道「刚刚那一下是谁拦的、测到了多少」。新增开关 notify.enabled(默认开):宿主侧维护一个进程内的触发广播,并注册 GET /reasoning-loop-guard/events 的 SSE 端点;web 半开一个 EventSource 订阅它,在 shell.overlay 槽位挂一个 Toast。它不改变任何发给 provider 的东西,也不改变纠正消息的发送者或内容。开关每次触发时重新读取,页面开着不动、改设置也立刻生效。事件路由与日志路由共用同一道同源回环栅栏,只接受 GET / HEAD。
新增第 9 套测试 test/test-events.mjs,钉住事件通道的六条承重性质:抛异常的订阅者只摘掉自己、同源栅栏与方法限制、断开时解绑、开关逐次重读、销毁路由时关闭连接、帧的形状。test-guard 新增三条分类断言,test-card 新增角落提示的帧解析与 key 重播,test-settings 两处 7→8。npm test 九套全绿。
两版 README 同步,新增「一次窗口不是一个循环:第二层判别」整节。 五条判据表补上三个新条件、新增三行现场对照表与复核口径、配置表加 notify.enabled、功能开关从三个变四个、文件表加 lib/events.js、测试表从八套改九套。README.md 303 → 325 行,README.en.md 308 → 330 行。
v0.2.4 — 代码行不再被当成复读
修复
line-repeat 误报——中断了一条并没有循环的推理流。
- 根因:归一化把空白、标点、符号全部剥掉,于是「引用同一行代码」被压成同一个字符串。3 条
if (e.isKeyboardEvent()) {再加上 diff 的-行与+行各一条,凑出「同一行出现 5 次、占窗口 22.6%」,越过lineCount: 3与lineShare: 0.1两条地板。 - 修法:新增
CODE_LINE(花括号、标记语言标签、行首 diff 标记)。命中时该行不参与「被报告的行」,但仍计入分母——是按行的种类排除,不是抬高阈值。 - 为何不抬阈值:本例
count只有 5,而真循环的count在 30 以上;lineCount要提到 6 才挡得下,6 已落在真循环占据的区间内。 - 两条无效路径(已实测):归一化前先剥 diff 标记(
STRIP本就剥-/+,结果完全相同);干脆去掉归一化(同一片段仍达count=3 · share=11.3%,照常触发)。 - 复核:3,862 条真实推理流上,修复前
periodic-run: 3+line-repeat: 3,修复后line-repeat: 2,count仍为 30 与 19;唯一少抓的那条是同形态的 HTML 误报。净收益 2、净损失 0。夹具 166 条健康流 0 误报、11 条退化流全部命中。八套测试全绿。
改动
| 文件 | 改动 |
|---|---|
lib/detector.js |
新增 CODE_LINE 常量与 lineRepeat 的按行种类排除逻辑(+73 行) |
test/test-guard.mjs |
更新因新规则失效的断言,新增代码行 / 标记语言标签 / diff 行不参与报告的用例,以及 40 次重复的分类排除对照(79 行改动) |
README.md |
精简重写:401 行 → 303 行;「为什么 line-repeat 要两个条件」改写为「三个条件」 |
README.en.md |
同步精简与对齐:401 行 → 308 行 |
CHANGELOG.md |
新增 [0.2.4] - 2026-10-09 条目 |
package.json |
版本 0.2.3 → 0.2.4 |
AI 使用说明:本版修复与文档精简由 AI 助手(DSH Agent,运行于 DeepSeek Harness)协助完成,经人工复核后发布。
v0.2.3 — 修复「自动恢复」从未生效
修复:开启「自动恢复」后仍然直接失败
打开「自动恢复」并填好纠正消息、每步重试次数之后,中断发生仍然直接以 REASONING_LOOP_GUARD 终态失败,宿主日志里连一条 recovery 记录都没有。
根因
两条可选臂(自动恢复、降低思考强度)是通过 ctx.inject(["agent"], …) 挂上去的,但 @deepseek-ai/dsh-agent 把自己的注册表注册为 super(ctx, "agents") —— 服务名是复数 agents,不是单数 agent。
于是 Cordis 的依赖永远不满足:
ctx.inject的回调一次都不会执行;- 挂在它下面的
agent/request-error(自动恢复)与agent/request(降低思考强度)两条监听器从未注册; - 而 GUI 的功能开关照旧显示「开」,没有任何提示。
单数 agent 只是该插件在 typert 里注册的 wire 类型名,不是 Cordis 服务名。对照:dsh-llm-retry 挂在同一个事件上,用的是 inject = ["agents", …]。
改动
lib/index.js:ctx.inject(["agent"], …)→ctx.inject(["agents"], …),并补上说明为什么是复数、以及为什么监听器不需要自己打 scope 标签(scopeTarget对未打标的监听器全局放行)。test/smoke/smoke.mjs:新增两组断言钉住它 —— 注入列表必须含agents、不得含agent;且当agents解析成功时两条臂必须都注册、思考强度臂必须带{ prepend: true }、注册过程不得产生任何告警。- README 两版:在「关键设计决策」里补一条服务名说明,测试表补上该项覆盖。
AI 使用说明:本次修复由 AI 助手(DSH Agent,运行于 DeepSeek Harness)协助定位与撰写,经人工复核后提交。
v0.2.2 — 界面时间改为本地时间
修复
触发时间不再按 UTC 显示,界面与工具一律渲染本地时间。
改动
- 新增唯一格式化入口
localStamp(ms):lib/journal.js导出,lib/client.js内联同构实现(浏览器端不能 import 宿主),把 epoch 毫秒渲染成本地YYYY-MM-DD HH:MM:SS。 iso字段仍按 UTC 持久化不变(它是规范、可排序的形式)。- 面板行优先使用
at(原始瞬时值,无需解析),iso仅作旧记录的兜底。 - 按天汇总(
journal.stats().byDay)同步改为按本地日切分。 - 覆盖范围:日志面板的行时间与「时间跨度」统计、
reasoning_loop_log工具的list行与stats的span:行。
验证
八套测试全绿,705 条断言。
AI 协助:本文档由 DSH Agent (DeepSeek Harness) 协助撰写。
v0.2.1 — 检测核心 GUI 开关、日志列表整体折叠、失败码命名空间与紧急止血环境变量
本版修掉一次真实故障,并补上两处使用上的缺口。建议所有用户升级。
修复:开关全关却自动续跑,点停止也停不下来
0.2.0 之前,默认失败码是通用的 REASONING_LOOP,而其他插件也会自带循环检测,并按失败码驱动自己的恢复逻辑。本机实测到一次真实串扰:dsh-our-free-model v2.0.0 在自己的 vendor/channel-pack/pack.js 里编进了 loop-recovery,用 error.code === "REASONING_LOOP" 精确匹配判断「这是不是我自己的循环失败」——于是本守卫的中断被它捕获,它随即调 agent.followup() 注入一条 source.kind: "user" 的消息(与用户手打「继续」同形)重排了一整轮。
这解释了全部症状:
- 三个功能开关全关,却看起来「自动触发了」——因为续跑根本不来自本插件;
- 点停止按钮也停不下来——按钮只中止当前流,那条已经排队的续跑消息照样会执行。
0.2.1 把默认失败码改成带命名空间的 REASONING_LOOP_GUARD,精确匹配不再命中。failureCode 仍可在 patch 里覆盖;lib/guard.js 导出的 FAILURE_CODE 是唯一权威默认值。
⚠️ 行为可见的改动:若你在别处按失败码做了匹配(告警规则、日志过滤、别的插件),升级后需要跟着改。
新增:检测核心的 GUI 开关
0.2.0 的功能开关面板能管三条可选臂,却管不到最要紧的那一个:检测本身。现在设置文件里多了一个 enabled 键,默认开启,在插件详情页的「功能开关」区块里是最上面那个「循环守卫」开关。
它由流监听器逐请求读取,所以关掉后下一次请求就生效,不需要重启。判定写成「显式 false 才算关」:缺键表示「按出厂设置」,也就是开着——这样旧的 config.json 不会被误读成关闭。关掉之后守卫不再观察任何流,三条可选臂也随之失效。
新增:日志列表整体折叠
此前的「全部展开 / 全部收起」只批量控制每条记录详情的开合,100 条行头始终铺在页面上。工具栏最左侧新增「收起列表 / 展开列表」,把整个列表连同行头一起收起,只留下工具栏与统计。两级折叠各管一层,互不覆盖。
文档:应急止血手段
这类串扰无法从原理上根除(若某个插件改成前缀匹配、或也去认 REASONING_LOOP_GUARD,冲突会回来)。因此 README 两版都补上了应急方案——用环境变量关掉第三方那一侧,不必等发新版:
| 环境变量 | 设为 | 作用 |
|---|---|---|
DSH_REASONING_LOOP_GUARD |
0 |
关掉第三方插件自带的循环检测 |
DSH_REASONING_LOOP_AUTO_RESUME |
0 |
关掉第三方插件的自动续跑 |
两者都以「未设即开」的方式读取,所以必须显式设成 0;必须设成系统/用户环境变量(DSH_ 前缀写进 ~/.dsh/.env 会被启动器拒绝);并且必须注销重登或重启系统才生效,只重启 DSH 不够。
README 开头也重写为「问题 → 做法 → 效果」三段式,并加了目录,方便第一次接触的人快速判断这个插件是否适合自己。
测试
八套测试全部通过(698 条断言,0 失败)。
AI 披露 — 本项目的源码、测试、文档与提交信息由 AI 智能体(DSH Agent,运行于 DeepSeek Harness)在人指导下起草,人类作者负责需求定义、设计决策与最终审阅。
v0.2.0 — 日志面板、元数据修复与基于真实语料的阈值重定标
AI 使用说明 —— 本项目由 AI 助手协助撰写:源码、测试、文档与提交信息均由 AI 智能体(DSH Agent,运行于 DeepSeek Harness)在人类指导下起草;需求定义、方案决策与最终验收由人类作者完成。
本版有两件事:一是把日志搬进 Web GUI 并修掉挡住它的注册错误,二是用本机真实推理语料重定标了两条判据的阈值——因为旧的定标集漏掉了一整类真实失败形态。
新增
Web GUI 日志卡片。 插件页详情里多出一个「触发日志」面板:最近的触发记录、按判据/按模型的汇总、一键清空、日志路径复制按钮。以包名为键注册 plugins.bundle.config 槽位,零构建(手写惰性 CJS bundle,运行时只向平台种子表要 react 与客户端 UI primitives)。
日志面板可折叠,一次最多 100 条。 一条记录默认只显示表头(时间 / 判据 / 度量 / 位置 / 模型),详情收在折叠里;最新一条默认展开,工具栏另有「全部展开 / 全部收起」。旧版取 50 条且全部展开,记录一多就没法看。
宿主侧日志路由。 GET /reasoning-loop-guard/log 返回 { path, enabled, version, stats, total, matched, entries },支持 limit / rule / sessionId / since;POST 只接受 {"action":"clear"}。路由自带同源回环栅栏(非回环 Host、Sec-Fetch-Site: cross-site、跨源 Origin 一律 403),因为宿主 webServer 本身不提供鉴权。
插件元数据。 icon 与 locale/en.json + locale/zh.json,因此插件页显示中文标题与描述。
新判据 filler-run(兜底)。 原始文本末尾连续 ≥ 400 个装饰字符(空白 / 标点 / 符号)即判定卡死。归一化把装饰从计数判据里剥掉之后,一个只输出装饰的流本可无限跑下去;这条判据专堵这个洞,并刻意做得很迟钝:锚定文本末尾(模型画完图接着写散文就不再计数),门槛远高于任何合法排版。
触发记录更详细。 新增 ttftMs、aborted、thresholds(触发时生效的 14 项阈值快照)、previewRaw、turn / step / attemptId / cwd / reasoningChars / elapsedMs / fromStartMs,以及 line-repeat 的 share。
修复
line-repeat 会把「边写代码边推理」当成循环。 旧判据是「同一归一化行出现 ≥ 2 次且行长 ≥ 10」,于是写 changelog 时写了两遍 ## Unreleased、重构时同一个标识符落在两行,都会中断用户的流——真实使用中一下午报出两条。
旧定标集(11 正 + 166 负)抓不到这个:它们全是散文,不含「边写代码边推理」这一失败形态,所以它报告的 0/166 对真实失效的形状毫无意义。改用本机 2799 条真实推理流(1,920 万字符,含 11 个已知真循环)重定标:旧默认触发 180 次,新默认触发 7 次且全是真循环。
line-repeat改为 count 与 share 双条件:lineCount: 2 → 3,新增lineShare: 0.1。分离度是实测的——健康流里最常见的口头禅最高只到 6.1%,最弱的真循环是 10.2%;取 8% 放进 2 条误报,取 12% 漏掉真循环。count 单独不够(健康口头禅 17 次 vs 最弱真循环 29 次,仅 1.7× 余量),也不能退回 2(一个 41 字符标识符出现两次就能把 share 顶到 19.5%)。block-repeat只需blockCount: 3 → 4。取 3 时,健康推理引用长文本(十六进制 dump、插件名清单、系统提示词复述)就会触发;取 4 后语料上只剩 3 次触发且全真。此处刻意不加 share 地板:0% 到 25% 结果都一样,加了只是无证据的复杂度。
判据 count 的语义缺陷。 两条规则原本一达标就返回,于是日志里的 count 恒等于阈值,从未反映真实重复次数。现在都改为全扫描取最大重复单元,count 才是真测量值。
filler-run 漏进阈值快照。 thresholdSnapshot() 只列了 14 项阈值里的 13 项(写于 filler-run 之前)。快照是「当时是什么配置导致这次判定」的唯一记录,缺一项就使那次判定日后无法解释——而缺的恰是最新的误报来源。已补上,并加了护栏测试:断言 DEFAULTS 的每个键都出现在快照里,而不是检查一份会悄悄落后的手写清单。
reasoning_loop_log 的输出 schema 会让 apply() 抛错。 JSON schema 校验器不接受在带类型的属性上写 required(schema.properties.action.required is not supported on type "string")。由于 output.schema 在注册时校验,这个抛出连带跳过了二十行之后注册的日志路由——也就是说卡片和路由此前都没挂上。schema 现改为只使用校验器真正支持的子集。
清洗类漏掉下划线。 旧的清洗类是一份手写字符表,从未列过 _。模型画一条 ________ 分隔线,归一化后仍是 8 个相同字符,periodic-run 以 period=8 units=6 命中,护栏中断了一条根本没在循环的流。改为 [\s\p{P}\p{S}]:_ 是 \p{Pc}、─ 这类制表符是 \p{So},新类是旧类的严格超集,在全部 11 条真实退化样本上两者输出逐字节相同,因此放宽不可能损失任何检出。
文档修正
- 「补丁会热重载,无需重启 DSH」是错的,v0.1.0 的说明里也写了这句,已改正:宿主对
node_modules的 HMR 是关闭的(dsh-hmr默认忽略**/node_modules),且 Node 在进程内永久缓存每个包的exports映射,所以本包新增locale/*.json这类 export 后,同一宿主进程里永远解析不到——表现为卡片标题一直是英文包名而图标(直读文件、不走 exports)却正常。装完或升级后必须重启 DSH,刷新页面不够。 - 两版 README 的规则表、配置表、日志示例、已知边界全部同步到新判据,并新增一节说明
line-repeat为何需要两个条件。
验证
- 八套测试全部通过;检测器定标在六种分块粒度(1 / 8 / 40 / 200 / 1000 / 4000)下仍是 11/11 检出、0/166 误报。
- 新增回归护栏:断言放宽后的清洗类不会从任何真实正样本里删掉字符。
- 发布前整棵文件树已扫描个人标识、凭据与机器相关路径,结果 CLEAN。
dsh-reasoning-loop-guard 0.1.0
首个公开发布版本。
AI 使用说明 —— 本项目由 AI 助手协助撰写:源码、测试、文档与提交信息均由 AI 智能体(DSH Agent,运行于 DeepSeek Harness)在人类指导下起草;需求定义、方案决策与最终验收由人类作者完成。
它解决什么问题
检测 DSH LLM 流里的逐字推理复读,并提前中断。当模型已经想完了、却卡在「马上要写答案了」上空转时,护栏会在几秒内停下它——而不是让用户干等几分钟、白烧几十万字符才手动按停。
主要特性
- 两条判据挂在
llm/stream瀑布上 —— 尾部周期重复(主判据,阈值 4 个单元)与 k-gram 重现(次判据,阈值 12),每 200 个字符评估一次。 - 提前终止 —— 发一个终止性
finish块,携带失败码REASONING_LOOP。该码故意不在可重试集合内,因为复读是请求本身的性质,自动重试只会重发整个 prompt 再复读一次。 - 触发日志 —— 写入
$DSH_HOME下的 JSONL(有大小与条数上限),并提供reasoning_loop_log工具用于维护与诊断。 - 零运行时依赖;工具走可选的
ctx.inject(["tools"], …)注入,因此在没有 tools 服务的主机里插件仍能正常加载。 - 四套测试 —— 覆盖检测器、触发日志、插件加载与真实流协议。
验证结果
- 检测器定标:用复现原始故障实测形状的合成夹具,在六种分块粒度(1/8/40/200/1000/4000)下 11/11 条正样本全部检出,0/166 条负样本误报。
- 判据分离度:周期判据正样本峰值 5..48、负样本 0..2(阈值 4);k-gram 判据正样本 20..162、负样本 1..1(阈值 12)。
- 端到端:护栏开启时在 7659 个字符中的第 3120 个字符处切断,判据为
周期 64 字符,重复 6 次;护栏关闭时完整回放 7659 个字符并正常结束。 - 发布前的整棵文件树已扫描个人标识、凭据与机器相关路径,结果 CLEAN。
安装
本包尚未发布到 npm,请从 GitHub 源码安装:
npm install github:himi-li/dsh-reasoning-loop-guard也可以在 profile 目录下直接克隆后取用:
git clone https://github.com/himi-li/dsh-reasoning-loop-guard然后把它注册进 profile 的 dsh.profile.bundles,并通过 cordis.patch.yml 挂载:
- insert:
- id: reasoning-loop-guard
name: dsh-reasoning-loop-guard
config: {}补丁会热重载,无需重启 DSH。16 个配置字段的完整说明见 README。