DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

zuaonew-ops /

zuaonew-ops/dsh-loop-guard

Verified

DSH 插件:拦住推理循环 / 流卡死 / 输出退化,并止住把它重试 5 遍的重试风暴

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@2a68bc46

dsh-loop-guard

推理循环守卫 —— 模型陷入「只出思考、不出结果」的死循环、整条流卡住不动、或者反复吐同一小段文本时,提前掐断这一次尝试,并止住把它反复重试的风暴。

一句话:把一次本来要卡几十分钟到几小时的会话,压缩成几秒钟的一次干净中止。

项目 值
包名 dsh-loop-guard
版本 0.5.1
许可 MIT
形态 DSH 插件:宿主半区(index.js)+ 设置页(client.js)+ 中英双语界面文案(locale/*.json)
第三方依赖 无(只用 Node 内置模块)
可选依赖 webServer —— 缺了守卫照常工作,只是没有设置页
不碰什么 不改写、不丢弃、不重排任何上游数据块;统计只放内存,不写盘
发布方式 未发布到 npm(private: true)。请从 GitHub 仓库安装

配套文档:安装 / 升级 / 卸载 / 回滚 · 配置详解(每个键调大调小会怎样) · 常见问题 19 条


1. 它解决什么现象

用户报的是三句话:「对话无限重复」「卡会话」「一个子会话运行时间过长」。

查会话记录,根因是同一个:

模型陷入推理循环 / 流卡死
  → 这次尝试以错误收场
  → 宿主按默认重试策略把它再试 5 次(同一次确定性失败)
  → 但每次尝试本身要跑 1–5 分钟,甚至几小时
  → 会话卡死数十分钟到数小时;子会话整个时间预算被烧光后被杀,零产出

重试把一次本来几秒就能判定的失败,放大成了几十分钟到几小时的卡死。

三种坏形态都来自真实会话(数字取自作者的会话记录,会话标识已隐去):

形态 现象 实测数字
分片洪水 同一步重试 5 次,每次都只出思考、不出正文 5 次尝试分别产出 20,704 / 16,902 / 3,163 / 21,103 / 11,827 个推理分片,正文分片 0 个;整步 18.2 分钟;重试退避 522 / 1001 / 1949 / 3850 / 7259 ms
卡住不动 某一步耗时 3.48 小时 6 次尝试里 5 次推理分片为 0,时间戳之间是 1.95 小时 + 1.5 小时的「一个分片都没来」空档
输出退化重复 思考里同一句话重复几千次 单块 85,705–102,796 字符、非空行 5,947–7,074 行、唯一行占比 0.5%–3.8%、最高频行出现 2,931–3,332 次

放大效应还有规模证据:扫一台开发机的 333 个会话,其中 37 个 depth=1 / turn=1 的子会话总时长恰好 18.4 分钟 —— 它们撞在同一个硬顶上。

还有一种「慢速渗流」形态:5,450 个推理分片 / 260 秒 / 产出 0,全程没有任何 ≥180 秒的空档。只盯「分片数量」或「有没有空档」的规则都抓不到它 —— 这正是 noOutput 规则存在的原因。


2. 它拦哪几类问题

四种坏形态,五条规则(第 5 条是兜底天花板,不是一种「形态」)。任一命中就掐断,优先级从上到下:

# 规则 坏形态 判据(源码 detectTrip 的真实条件) 默认阈值
1 chunk 分片洪水:一直在思考,一个字的正文都不出 推理分片数 ≥ reasoningChunkLimit 且 产出分片 = 0 20,000 片
2 stall 卡住不动:一个分片都不来 距上一个分片(或起始时刻,即首块之前也算)≥ stallTimeoutMs 180,000 ms(3 分钟)
3 noOutput 慢速渗流:分片慢慢来,但一直不出结果 已耗时 ≥ noOutputMs 且 产出分片 = 0 600,000 ms(10 分钟)
4 repeat(行判据) 输出退化重复:在重复同一小段文本 窗口 ≥ repeatMinChars,且某个子窗内:非空行数 ≥ repeatMinLines、唯一行占比 ≤ repeatUniqueRatio、最高频那一行出现次数 ≥ repeatCycles 1,000 字符 / 30 行 / 0.2 / 6 次
4 repeat(片段兜底) 同上,但整块几乎只有一行(行判据结构上抓不到) 窗口 ≥ repeatMinChars,且整窗非空行数 < repeatMinLines,且最坏子窗的 8 字符不重叠片段唯一率 ≤ repeatGramRatio 0.10
5 absolute 兜底:任何单次尝试都不该跑这么久 已耗时 ≥ attemptBudgetMs(不看产出) 1,800,000 ms(30 分钟)

两个关键定义:

  • 产出分片 = text-delta + tool-call-delta。reasoning-delta(思考)不算产出。
  • 窗口 = 最近 repeatWindowChars(默认 6000)个字符,只吃推理分片与文本分片(工具调用分片不参与)。判据不是看整个窗口,而是在窗口上枚举若干子窗(尺度 = repeatMinChars × 2^k,再加上窗口本身),取最坏的那个值 —— 这样「前半段正常、后半段开始循环」也不会被前面的正常内容稀释掉。

2.1 触发之后会发生什么

步骤 行为
① 掐断 立刻停止向上游取值,向下游吐一个终结块:{type:'finish', reason:{kind:'error', failure:{code:'LOOP_GUARD', message:'…'}}}。触发之前已经到达的每个块原样透传
② 止住重试风暴 失败码 LOOP_GUARD 回到宿主重试层时,本插件只认自己这个码:同一个 turn/step 允许 maxGuardedRetries 次(默认 1)纠正重试,之后返回「终结」—— 不再把同一次循环重试 5 遍。其它任何失败码一律交给默认策略,绝不干预
③ 给它一条走得通的路 最近 120 秒内循环过的 agent,下一次请求把 reasoningEffort 降一档(max → high → low → off)。当前档位不在这个阶梯里时是空操作(宁可不降,也不瞎猜档位名)

这条链路在真机上验证过(一次空转 4.3 分钟、零产出的尝试):

22:18:23  step/start
22:18:27  起连续 5,450 个推理分片、产出 0
22:22:43  守卫触发,注入 finish{failure.code:'LOOP_GUARD'}(已耗时 260 秒)
          失败到达重试层 → 守卫放行 1 次纠正重试(retriesAllowed=1)
22:23:56  该步正常产出 reasoning + text + tool-call + tool-call,step 正常收尾,后续 step 继续

也就是说:一次空转 4.3 分钟、零产出的尝试被拦下,一次纠正重试就恢复了正常产出。

口径说明:这一次是早期版本用一条更激进的规则(「已耗时 ≥ 240 秒且产出为 0」)拦下的;那条规则后来被删掉了,因为它会误杀「先长时间思考、再开始作答」的正常请求。它在这里的价值是证明了整条链路可用(掐断 → 纠正重试 → 恢复正常产出)。

2.2 防误杀设计(这是本插件最重要的取舍)

规则 会不会误杀「正常的长回答 / 长思考」 为什么
chunk 不会 带「产出为 0」这个必要条件 —— 只要模型真的在出正文或调工具,它绝不插手,不管思考了多久
noOutput 这是五条里最可能误杀的一条 同样要求「产出为 0」,所以「合法地思考超过 10 分钟才吐第一个字」会被它掐掉。默认给到 600 秒,实测没有这种用例(见 §8.3),如果你确实有超长思考的正常请求,优先调大它
stall 基本不会 沉默就是沉默,有没有出过文本都拦。默认 3 分钟,离实测的 1.95 小时空档有两个数量级余量
repeat 不会(但有边界) 只在「行数远多于唯一行数」时命中,正常叙述每行都不一样。边界是「常量模板 + 只变计数器」这种形状,余量只有 1.06×(见 §8.2)
absolute 可能 不看产出,跑到 30 分钟无条件切断 —— 这是故意的最后兜底

2.3 它不做什么(边界)

  • 不修复模型的循环。 循环是上游模型的退化行为;本插件做的是「尽早掐断 + 一次纠正重试 + 降一档推理档位」。真正的产出恢复取决于模型下一次尝试是否正常。
  • 不保证一定解开卡死的上游连接。 上游彻底挂住时,iterator.return() 未必能打断那个挂起的 next()(取决于适配器实现)。保证的是下游不会再被吊住:超时那一刻已经吐了终结块并结束自己的流。
  • 不改写、不丢弃、不重排任何上游数据块,也不写任何会话数据。
  • 不干预别人的失败。 只接管自己产生的 LOOP_GUARD,其它失败码一律 next()。
  • 不做遥测、不发网络请求。 唯一的落盘是一个设置文件(见 §6)。

3. 快速开始

前置:DSH 宿主能正常启动,且你的环境里有 plugin_manager 工具(带插件系统的 DSH 都有)。

方式一:从 GitHub 安装

dsh plugin add github:zuaonew-ops/dsh-loop-guard

这条链路未实测(见 §10 第 10 条);下面的方式二已实测。

方式二:克隆到本地,按目录安装(已实测)

git clone https://github.com/zuaonew-ops/dsh-loop-guard.git

然后在宿主里:

plugin_manager action=install_bundle target="<克隆下来的绝对路径>"

重启一次宿主

装完必须重启一次 DSH 宿主。 宿主对浏览器半区(dsh.client)的扫描是增量的,否定结论会缓存到进程重启;不重启的话守卫可能已经在工作,但设置页进不了浏览器的 boot graph(表现为左侧导航里没有「循环守卫」)。

确认装上了

  1. 打开 设置 → 左侧导航「循环守卫」;
  2. 或者问宿主:
curl -s http://127.0.0.1:<宿主端口>/loop-guard/state

期望看到:

字段 期望
ok true
value.enabled true
value.config 12 个键,值与 §5.2 的默认值一致
value.stats.attempts 随便发一轮对话后再查,数字会变大 —— 这就是「监听真的挂上了」的证据
value.stats.guarded 通常是 0(只有真的出现那四种坏形态才会加一)
value.recent 通常是 []

完整的安装 / 升级 / 卸载 / 回滚 / 排障 / 端到端自检步骤见 docs/INSTALL.md。


4. 设置页

位置:设置 → 左侧导航 「循环守卫」(每 5 秒自动刷新)。

区块 内容
总开关 开启/关闭守卫(关掉后完全不干预,但设置页仍然可用)
阈值 11 个数值输入框(时间类的按秒填,宿主内部按毫秒记;0–1 的比例直接填小数)
降档开关 「重试时降低推理档位」
统计 尝试次数 / 拦下的循环 / 放行的重试 / 终止的重试 / 省下的时间(估算)
按规则分布 五条规则各自的命中次数:分片洪水 / 卡住不动 / 慢速渗流 / 输出退化重复 / 绝对上限
最近触发 每次触发的时间 / turn·step / 推理分片数 / 已耗时 / 产出分片数(默认保留 50 条)
操作 保存、清零统计

HTTP 端点(设置页用的就是这三个):

方法 路径 用途
GET /loop-guard/state 当前开关 + 12 个可改配置项 + 统计 + 最近触发
POST /loop-guard/config 改配置(只接受 13 个可改键的子集,未知键 400)
POST /loop-guard/reset 统计清零

统一信封 {ok:true, value} / {ok:false, error:{code, message}};请求体上限 64 KiB;方法不对返回 405 + Allow;路径不对返回 404。三道安全闸见 §6.3。


5. 配置

5.1 三层配置,谁覆盖谁

层 位置 生效时机 优先级
① bundle 补丁 包内 cordis.patch.yml 的 config: 宿主启动 / 该行重载时 最低
② 设置文件 $DSH_HOME/loop-guard-settings.json(DSH_HOME 未设时是 ~/.dsh/loop-guard-settings.json) 只在插件挂载时读一次 覆盖 ①
③ 设置页 / POST /loop-guard/config 内存 + 原子写回 ② 立即 覆盖 ① ②

由此推出三条使用规则:

  • 界面里改过的值,重启后仍然生效 —— 因为文件优先于 yml。
  • 想回到 yml 的值:删掉设置文件,再重启宿主。
  • 运行中直接改设置文件没有用 —— 文件只在挂载时读一次。要么用设置页,要么重启宿主。

5.2 全部 16 个键

「界面可改」= 能在设置页里改、并且会被写盘记住。共 13 个:总开关 + 11 个阈值 + 降档开关。剩下 3 个只能写在 cordis.patch.yml 里。

键 类型 默认值 取值范围 界面可改 什么时候该动它
enabled boolean true true / false ✅ 想彻底关掉守卫(一条监听都不装,完全不干预)时设为 false
reasoningChunkLimit number 20000 整数 ≥ 100 ✅ 抓「分片洪水」的灵敏度。调小=更早掐断(更激进);调大=更宽容
stallTimeoutMs number 180000(3 分钟) 整数 ≥ 10000 ✅ 抓「一个分片都不来」。调小:网络慢/长排队时可能误杀;调大:真卡死时等更久
noOutputMs number 600000(10 分钟) 整数 ≥ 10000 ✅ 抓「慢慢来但一直不出结果」。五条里最可能误杀的一条:有超长思考的正常请求就调大它
repeatWindowChars number 6000 整数 1000–200000 ✅ 重复检测的观察窗口。调大:能发现更长的循环周期,但 CPU 开销上升、判定更迟;调小:更快更省,但长周期循环可能看不出来
repeatMinChars number 1000 整数 500–repeatWindowChars ✅ 判据的最低字符门槛(窗口短于它就不判)。设成等于 repeatWindowChars = 退回旧语义(只判满窗),这是「灵敏度回退」开关
repeatMinLines number 30 整数 5–5000 ✅ 窗口里最少要有多少非空行。调大:只抓「很多行在重复」;调小:单行/少行退化更容易走片段兜底
repeatCycles number 6 整数 2–100 ✅ 最高频那一行至少要重复几次。调小=更早判定;调大=要重复更多轮才算
repeatUniqueRatio number 0.2 0.01–0.9 ✅ 唯一行占比上限。调大=更激进(更容易命中);调小=更保守
repeatGramRatio number 0.10 0.01–0.9 ✅ 单行/少行退化的片段唯一率上限。调高会更容易误杀长单行自然语言(见 §5.4)
attemptBudgetMs number 1800000(30 分钟) 整数 ≥ 10000 ✅ 绝对天花板,不看产出。调小:长任务会被切断(实测有一条一直在正常出文本的长流会在 12 分钟处被切);调大:真循环的兜底时间变长
maxGuardedRetries number 1 整数 0–5 ✅ 掐断后允许宿主再纠正重试几次。0 = 拦到就终结;调大 = 多给几次机会(但每次都可能再烧时间)
lowerEffortOnRetry boolean true true / false ✅ 纠正重试时是否把推理档位降一档。关掉它就没有第 ③ 道防线
settingsPath string ''(空) 绝对或相对路径 ❌ 只能写 yml 想换设置文件的落盘位置时填;留空 = $DSH_HOME/loop-guard-settings.json
maxRecent number 50 整数 1–500 ❌ 只能写 yml 设置页「最近触发」保留多少条
debug boolean false true / false ❌ 只能写 yml 排障:把每次拦截写进宿主日志

GET /loop-guard/state 的 value.config 里是 12 个键(不含 enabled,它在 value.enabled 里单独给)。「12」和「13」不矛盾:13 = 可改且可持久化的键,12 = 其中「阈值与行为」的部分。

每个键的默认值来历、调大调小的副作用、以及 repeat 判据全貌见 docs/CONFIG.md。

5.3 校验规则(三处输入源严格程度不同)

情况 结果
cordis.patch.yml 里写了不存在的键 / 类型不对 / 数字越界 / repeatMinChars > repeatWindowChars 抛错并点名,这一行加载失败
设置页里填了非法值 只在本页提示,不发请求(11 条针对性文案,中英双语)
POST /loop-guard/config 收到未知键 / 类型错 / 越界 400 + unknown-key / bad-type / bad-range
设置文件里单键数值越界或不是整数 忽略该值 → 退回 cordis.patch.yml 的值,并打一条告警(不抛错,插件照常挂载)
设置文件里 repeatMinChars > repeatWindowChars(跨字段,合并后判定) 夹紧到窗口大小 + 告警。判越界看的是「生效窗口」,不是出厂默认的 6000
设置文件是坏 JSON / 内容不是对象 告警 + 忽略整个文件(退回 cordis.patch.yml 的值)
设置文件里有不认识的键 / 类型不对 静默丢弃该键(不告警)

一句话:设置文件里写什么都不会让插件挂载失败,设置页一定打得开;坏值只会被「退回 yml 的值」或「夹紧到生效窗口」并留一条告警(一个坏键一条)。而 cordis.patch.yml 是「严进」的 —— 那是你手写的正式配置,写错就该报错。

5.4 三条「不要踩」的

别踩 为什么
别把 repeatGramRatio 往上调 它一调高就更容易误杀长单行自然语言:实测中文自然散文单行约 0.36、英文约 0.45,而真退化单行只有 0.007–0.009。默认 0.10
别把 stallTimeoutMs / noOutputMs / attemptBudgetMs 调到 10000 以下 在 cordis.patch.yml 里会被配置校验直接拒绝(抛错并点名);在设置页里会被本地拦下(不发请求);在设置文件里会被忽略并退回 yml 的值 + 告警
别在 cordis.patch.yml 里写不存在的键 未知键直接抛错,会让这一行加载失败(不静默兜底)

6. 安全与隐私

6.1 它接触什么、写什么

项 说明
网络 不出网:无第三方依赖、无 HTTP 客户端、无遥测/统计上报
上下文数据 只在内存里逐块计数与做窗口指标;不改写、不丢弃、不重排、不落盘任何数据块
会话数据 不读写任何会话文件/数据库
落盘 只有一个设置文件($DSH_HOME/loop-guard-settings.json,13 个键,原子写)
统计 只在内存(尝试数 / 命中数 / 最近 50 条触发的计数与规则名;不记录对话正文)
请求参数 降档只在最近 120 秒内循环过的 agent 的下一次请求上生效,且只改 reasoningEffort 一个字段

6.2 安装与卸载的写入面

  • 安装(install_bundle)会在 profile 的 cordis.yml / cordis.patch.yml 里落一行 insert(id: loop-guard / name: dsh-loop-guard / config: {}),并把包内容放进插件目录。
  • 卸载(remove_bundle)会移除那一行;设置文件不会自动删(见 §7)。

6.3 HTTP 端点的三道安全闸

设置页的三条路由统一过闸:

闸 不满足时
Sec-Fetch-Site: cross-site 403 + cross-site
Origin 存在但与本机不同源 403 + bad-origin(Origin 缺失时放行,所以 curl 能用)
POST 的 content-type 不是 application/json 415 + unsupported-media-type

为什么需要这三道闸:DSH 的 webServer 路由契约是 {kind, path, handler},没有全局 Origin/CSRF 防护,插件路由也不在 GUI 的登录门后面。没有闸的话,任意网页都能用 content-type: text/plain 发跨站简单请求打到这里并让服务端真的执行。

另:请求体 > 64 KiB → 413;body 不是合法 JSON → 400;路径对但方法不对 → 405 + Allow。


7. 卸载与回滚

7.1 临时停用(最轻)

设置页把总开关关掉,或在 cordis.patch.yml 的 config 里写 enabled: false 后重载该行。三条监听一条都不装,完全不干预。

7.2 卸载

plugin_manager action=remove_bundle target="dsh-loop-guard"

然后重启宿主。残留与清理:

残留 位置 要不要清
设置文件 $DSH_HOME/loop-guard-settings.json(DSH_HOME 未设时是 ~/.dsh/loop-guard-settings.json) 想彻底干净就删掉它
profile 里的那一行 profile 的 cordis.yml remove_bundle 会处理;若手工改过 yml,请自行确认
统计 无 统计只在内存里,不落盘,重启即消失

7.3 回滚到旧版本

  1. 把旧版本的 dsh-loop-guard 目录放回原处(或从仓库切到旧的 tag/commit);
  2. 重新 install_bundle 并重启宿主;
  3. 设置文件是「宽进」的:旧版本不认识的键会被静默丢弃,越界值会被退回 yml 的值 —— 降级不会让插件挂载失败(见 §5.3)。

恢复出厂配置:删掉设置文件 + 重启(之后就完全按 cordis.patch.yml 里的值走)。完整流程见 docs/INSTALL.md §5–§7。


8. 实测数据

所有数字都来自作者开发机上已经发生过的真实会话记录,用插件自己的判定函数做离线重放得到;重放脚本是外部工具,不在本仓库内(见 §11.4),因此这些数字无法在仓库里一键复现。语料含真实对话,属于个人数据,没有随仓库发布。

8.1 全语料重放(判定正确性)

项 值
会话 / 记录 / 块 / 字符 340 / 14,764 / 21,072 / 42,413,941
解码错误 0
命中记录数 222(216 + 6,全部走 repeat 的行口径)
块级命中 218
非退化会话命中 0(记录级 0 / 块级 0)
7 个已知真退化短块 7 / 7 全部命中(触发窗口 1,024 / 1,024 / 2,560 / 2,048 / 1,536 / 1,024 / 2,048)
旧判据(v0.4)的 23 个命中 一个不丢(记录级与块级都 0 丢失)

不同次重放的记录/块/字符数会略有差异(14,346 → 14,764),因为语料在持续增长(同一台机器上的会话还在追加),不是判定变了 —— 命中数(222)、分布(216 + 6)、非退化(0)、短块(7/7)每次都相同。

8.2 误杀余量(最关键)

剔除两个已知退化会话之后,正常文本离阈值还有多远:

判据 正常文本里最接近阈值的值 阈值 余量
片段唯一率(k-gram 兜底) 0.4032 0.10 4.03×
唯一行占比(行判据) 0.2574(一个 markdown 围栏块) 0.20 1.29×
「常量模板 + 只变计数器」这种形状 0.1060 0.10 1.06×

另外 6,483 个非退化块里,落在「危险区间」(0.10, 0.20] 的有 0 个。

不能只报一个数:全语料口径是 4.03×,但「模板 + 计数器」这种形状只剩 1.06×,而且子窗有 ±30% 的相位噪声(同一段文本平移 0–255 字符,最差子窗的片段唯一率在 0.1060–0.1720 之间漂移)。所以:

  • 语料里 0 误杀是真的;
  • 但「正常文本至少离阈值 3.92×」不成立 —— 灵敏度在 v0.5 提高了(分段取最坏值恒 ≤ 整窗值),「常量模板 + 只变计数器」这类输出会比以前更早被判死。回退办法:把 repeatMinChars 设成等于 repeatWindowChars,即退回旧语义(只判满窗)。

8.3 时间类规则的误杀面上界

指标 有产出的真实 LLM 调用 零产出的调用
样本数 14,750 16
p50 / p90 / p99 0.13 / 0.31 / 0.95 分钟 0.24 / 2.76 / 3.39 分钟
最长 7.48 分钟 3.39 分钟
≥ 180 秒 8 次 —
≥ 300 秒 3 次 —
≥ 600 秒(noOutputMs 默认线) 0 次 0 次
≥ 1800 秒(absolute 默认线) 0 次 0 次

结论:600 秒这条线高于全部观测样本,观测误杀面为 0;但余量只有 1.34×(600 ÷ 449 秒)—— 真正会被误杀的只有「思考超过 600 秒才吐出第一个正文/工具分片」的调用,14,750 次里一次都没有。

8.4 性能开销(CPU)

口径 修复前 现在 倍数
默认窗口(6000 字符),每 1000 字符输出 — 3.25 ms —
真实流分片(中位 2 字符,即最坏喂料粒度),每 1000 字符 106 ms 3.34 ms 约 30×
大窗口 200,000 字符,每 1000 字符 307 ms 14.33 ms 约 21×
全语料重放墙钟 445.5 s 165.6 s 约 2.7×
指标重算次数 301,756 173,716 约 0.58×

另一条要分开看的开销:分段判据本身比「整窗等价实现」贵 —— 窗口 6000(19 个子窗)时 recomputeRepeat 平均 1,034.2 µs/次,而整窗等价实现是 150.6 µs/次(6.87×)。这是 v0.5「取最坏子窗」换来的灵敏度代价,不是退化;上表说的是相对更早版本的整体提速。

8.5 验证强度

  • 17 个套件 = 1,126 条断言 PASS / 0 FAIL(unit 106 / packaging 43 / host 85 / contract 51 / contract-f11 24 / gram-falsify 23 / falsify 12 / client 108 / http 67 / v5-checks 41 / throttle 11 / endgate 15 / rename-diff 33 / 作者单测 407 / 冒烟 71 / live 17 / corpus 12)。跑动前后各取一次 index.js 哈希,两次相等 → 跑动中没有第三方写入。
  • 作者单测 407 PASS / 0 FAIL(含「设置文件里的坏值怎么处理」「逐键校验基准用生效窗口、不用默认 6000」等回归钉)。
  • 一轮独立复现:复现者自己重写了一遍判据(只读状态字段,不复用插件内部逻辑),在 5 个样本 × 4 组配置 × 每个节流点共 2,980 个采样点上与插件的 detectTrip 逐点一致;并独立重放了全语料,命中数与作者侧逐项一致。
  • 同一轮复现用 Proxy 追踪状态字段读取,机械证明了「stall / noOutput 判定路径一次都不读 repeat 的字段」(返回这两条规则时读取次数 = 0,不是靠论证);并在 135 种文本形状 × 3 组配置 = 405 次流尾补算里,对「行口径与片段口径都还有 >1.5× 余量」的形状误判 0 次。
  • 上述套件与离线重放脚本都不在本仓库内,是作者的验证工具(见 §11.4)。

8.6 数字口径提醒

  • 各次重放的记录数/块数/字符数不一样(14,346 → 14,764),原因是语料在持续增长;命中数与分布每次都相同。
  • 「221 / 222」两个命中数并存:222 = 含「流尾补算」的最终口径,221 = 不含它的旧口径(只有 1 条靠流尾补算才命中)。
  • 界面上的「省下的时间」是估算:每次触发时按「这次尝试本来还会跑到 attemptBudgetMs(30 分钟)」算差值再求和(absolute 那条已跑满预算,按 0 计,不虚报)。它不是实测省下的墙钟时间。
  • 「chunk 档约 4.9 分钟命中」是按整步平均速率推算的,不是实测。

9. 已知限制

照实写,不修饰:

# 限制
1 它治不好模型的循环,只能快速止损。 真正的产出恢复取决于模型下一次尝试是否正常
2 它不能保证一定解开卡死的上游连接。 上游彻底挂住时,iterator.return() 未必能打断那个挂起的 next();保证的是下游不会再被吊住
3 noOutputMs 是五条里最可能误杀的一条。 余量只有 1.34×(600 ÷ 449 秒)。有超长思考的正常用法请调大
4 repeat 的灵敏度在 v0.5 提高了。 「常量模板 + 只变计数器」这种形状的余量只剩 1.06×,且子窗有 ±30% 相位噪声。要退回旧语义就把 repeatMinChars 设成等于 repeatWindowChars
5 全部命中都来自两个会话。 语料里 222 次命中只落在两个已知退化会话上(216 + 6),证据强度受这两个会话的多样性限制
6 统计不落盘。 只放内存:宿主重启、或点「清零」,统计就归零。只有那 13 个可改配置项会写盘
7 absolute(30 分钟)对「一直在正常出文本的超长任务」是硬切。 观测数据里有产出的调用最长 7.48 分钟、没有 ≥30 分钟的,但这是观测上界,不是保证
8 内部调用(压缩会话、生成标题等)同样受约束。 它们的正常输出是文本(产出 > 0),所以 chunk / noOutput 不会命中;但若某次压缩合法地零产出超过 10 分钟会被掐(观测里零产出调用最长 3.39 分钟)
9 降档只对广告了推理档位的模型有效。 当前档位不在 off/low/high/max 阶梯里时是空操作(宁可不降,也不瞎猜)
10 多开宿主会共用同一份设置文件。 设置文件按 $DSH_HOME 定位、与 profile 无关;而且只在挂载时读一次。所以 A 实例保存的设置不会立刻影响已在运行的 B 实例,两者各自写盘、互相覆盖

10. 未验证项

以下条目没有实测证据,单独列出(不要当成已验证):

# 未验证的事 现状
1 真实浏览器里的设置页交互 只做过桩测试(真执行模块工厂 + mini-React 渲染 + 真点击,108 项断言)。真实浏览器里的人工点击没验证过
2 分段判据在真机上的触发 真机历史里的命中记录是旧版本的片段兜底命中(2 条)。本轮的 222 次命中全部是离线重放,不是活的流
3 stall 在其它 provider 上的误杀面 只在一个 provider 上观测过。残余风险:适配器自身没有空闲超时 且 模型合法地 >3 分钟不吐分片
4 repeatWindowChars 调到 200000 的真实会话行为 只测了 CPU 开销与单元级判定,没有真实长会话跑过这个配置
5 流尾补算路径的内存/GC 影响 只测了判定正确性与 CPU 时间,没有测内存
6 「恰好结束于 noOutputMs 边界」的活流 单测覆盖了,真实复现做不到(阈值下限 10000 ms)
7 纠正重试的成功率 只有 1 个真机样本(就是 §2.1 那条链路)。「1 次纠正重试通常够不够」没有统计意义
8 repeat 的 lineTrip 分支在真机上的记录 旧版本报告里有一次文字记录,但不在当前 recent 里、无法独立复核
9 真实收益的墙钟数字 §8.6 的「省下的等待」里,触发点是按速率推算的;界面上的「省下的时间」也是估算值
10 打包/发布链路本身 本文描述的是本地目录安装与离线验证;从 GitHub 公开仓库安装(dsh plugin add github:…)、版本升级路径未实测

11. 开发与测试

11.1 仓库结构

文件 作用
index.js 插件宿主半区:三条监听(llm/stream / agent/request-error / agent/request)、五条判定规则、设置文件读写、HTTP 路由、settings.section 界面注册
client.js 设置页(浏览器半区):渲染 UI、校验输入、调三个端点。不在浏览器里调用任何第三方库(DICT 内置中英双语文案)
cordis.patch.yml bundle 补丁:往 cordis.yml 里插一行(id: loop-guard / name: dsh-loop-guard / config: {})
locale/{zh,en}.json 设置页的文案(meta + ui.loopGuard 两段)
docs/ INSTALL.md / CONFIG.md / FAQ.md / MANIFEST.sha256(交付清单)
icon.svg 插件图标

没有任何构建步骤:index.js 与 client.js 都是 ESM 源文件,改了直接生效(刷新生效方式见 §11.2)。仓库里没有 node_modules、没有打包器、没有第三方依赖 —— 装上就能用,也不引入新的供应链面。

11.2 改完代码怎么生效

改了什么 怎么生效
index.js 重载那一行插件,或直接重启宿主
client.js / locale/*.json 重启一次宿主 + 刷新页面(浏览器半区是启动时扫描的)
cordis.patch.yml 里的 config 重载该行即重新挂载 → 会重新读一次设置文件(所以「改 yml 让文件重新生效」也是一招)
设置文件 重新挂载(重启或重载该行)才读得到新值 —— 运行中改文件无效

重载命令(entry id 是 include:<补丁 id>):

set_plugin target="include:loop-guard" enabled=false
set_plugin target="include:loop-guard" enabled=true

写裸的 loop-guard 会报 unknown-plugin —— 补丁插入的行是 include: 前缀的。

11.3 静态检查

node --check index.js
node --check client.js
node -e 'for (const f of ["locale/zh.json","locale/en.json","package.json"]) JSON.parse(require("fs").readFileSync(f,"utf8")); console.log("json ok")'
node -e 'const fs=require("fs"),c=require("crypto");let bad=0;for(const l of fs.readFileSync("docs/MANIFEST.sha256","utf8").split(/\r?\n/)){const m=l.match(/^([0-9A-F]{64}) +(\d+)  (.+)$/);if(!m)continue;const b=fs.readFileSync(m[3]);if(b.length!==+m[2]||c.createHash("sha256").update(b).digest("hex").toUpperCase()!==m[1]){bad++;console.error("BAD "+m[3])}}console.log(bad?"FAIL":"manifest ok");process.exit(bad?1:0)'

(交付清单是三列格式:哈希 / 字节数 / 路径,sha256sum -c 不适用;最后一条是等效的逐条校验。)

11.4 关于测试

本仓库不含测试套件,也没有「一键跑完」的入口。上文 §8 的验证是这样做的:

  1. 用插件自己的判定函数,对已经发生过的真实会话记录做离线重放(外部脚本,不在仓库);
  2. 加上作者自己的单测与套件(unit / falsify / contract / host / packaging 等 17 个套件,也是外部工具,不在仓库);
  3. 另请他人独立重写判据做交叉复现(见 §8.5)。

所以如果你是来改判定逻辑的:建议先搭一个「已确认无退化」的会话反例集,改完用 detectTrip 对着它跑一遍,确认 repeat 命中数与非退化命中数都没变,再上真机。


12. 兼容性

项 要求 不满足时
DSH 宿主 有 plugin_manager(bundle 安装)与 settings.section 插槽 装不上 / 设置页不出现
设置页 宿主启用了 webServer 守卫照常工作,只是没有 /loop-guard/* 路由、设置页打不开
Node 宿主自带的 Node(无需额外安装) —
平台 只用 Node 内置模块(fs / path / os / crypto 等),无原生扩展 —
模型 / provider 完全无关:只看 llm/stream、agent/request-error、agent/request 三个事件 —

两种半区各自独立:只装宿主半区也能拦截,只是没界面;只装浏览器半区没有拦截、没有数据源。


13. 许可

MIT © 2026 zuaonew-ops。见 LICENSE。

用之前请先读 §9「已知限制」与 §10「未验证项」—— 这是一个止损工具,不是质量保证。

—/ 5

No ratings yet

Verified DSH bundle

Commit 2a68bc46b037

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout