dsh-unlimited-retry

modeltested: passnpmMIT

Retries a model request that no other recovery layer claimed, unbounded by default with an exponential backoff ceiling, never retrying a cancellation; the policy is editable from a Settings panel.

下载量 / Downloads
1,467
依赖体积 / node_modules
0.17 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-unlimited-retry

Sandbox test results

StageResult
1 install✓
2 list✓
3 mount✓
4 boot—

Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).

README

dsh-unlimited-retry

[English](README.md) · 简体中文

![CI](https://github.com/xalor888/dsh-unlimited-retry/actions/workflows/ci.yml)

![npm](https://www.npmjs.com/package/dsh-unlimited-retry)

![license](LICENSE)

![node](package.json)

![dsh](#兼容性)

DeepSeek Harness(DSH)的无限模型请求重试插件,附带一个实时设置面板。

一次长时间无人值守的运行,不该因为某个请求撞上网络抖动而整个中断。这个插件让这类请求持续重试——指数退避、有延迟上限——直到成功,或直到你叫停。

标准 DSH bundle 插件,dsh plugin 一键安装。同一个包同时支持 DSH Web 与官方桌面客户端(Electron)。

装在哪里

本仓库只维护一条产品线(main),同一个包同时支持两种界面。

你要装在哪 · 安装方式

DSH Web 界面 · 终端:dsh plugin --profile web add dsh-unlimited-retry

官方桌面客户端(Electron) · 不用终端 —— 在桌面端会话里让 DSH 用内置的 plugin_manager 安装(见下)

官方桌面端:不要用终端命令

桌面端读的是 desktop profile,而命令行按设计拒绝碰它:

error: profile "desktop" is managed exclusively by the Electron application

所以 --profile web 的命令装完,插件落在 web profile 里,桌面窗口一个字也读不到 —— 表现就是「设置里根本没有这一页,控制台也不报错」。

正确做法:在桌面客户端的会话里直接说:

请使用当前宿主内置的 plugin_manager 安装 dsh-unlimited-retry。先核对当前 profile 和是否已有同名插件;已有安装时先报告来源与版本。完成后告诉我安装结果及是否需要重启。

plugin_manager 的作用域就是当前 profile(桌面端为 desktop),由宿主用它自带的包管理器完成安装与挂载登记 —— 不需要终端、不需要构造 cookie、不需要改写宿主管理的 profile 文件。要固定版本就写 dsh-unlimited-retry@<版本>。

首次安装后如果设置里没出现,完全退出客户端(含系统托盘)再重开,而不是去堆叠另一份安装。

为什么桌面端也能用:桌面端的 profile 就是 Web 模板(base + web-app),Electron 加载的是本地 Web UI 的 URL,因此 dsh.client 声明的面板在桌面端同样被 client-modules 装载。不需要另做一套客户端代码。
命令行 dsh web 用的是自己的 web profile;更新后重启宿主,并在浏览器 Ctrl+F5(Mac 为 Cmd+Shift+R)绕过缓存。

为什么要单独做一个插件

DSH 自带的 @deepseek-ai/dsh-llm-retry 只负责执行重试,真正的预算在每家 provider 适配器的 retryPolicy 上。它默认的 mode: "normal" 在 maxRetries(本构建是 5)之后就把决定权交出去,于是 step 失败、turn 以错误结束、整轮运行终止。mode: "always" 确实存在,但它是按 provider 全有全无的,而且没有延迟上限。

本插件在所有恢复层之后再加一层。agent/request-error 是 waterfall 事件,Cordis 从最外层开始依次调用,所以最后注册的监听器在最内层:其他所有恢复先决定,只有没人接手的失败才会落到这里。

实际重试顺序

按当前默认的「provider 重试 5 次」策略,实际顺序是:

第几次尝试 · 由谁处理 · 行为

1 – 5 · provider 的 retryPolicy · 它自己的预算、错误码与延迟

6 – ∞ · 本插件 · 带抖动的指数退避,封顶 maxDelayMs

其他注册在同一事件上的层——上下文溢出压缩、图片卸载(dsh-compaction-image-offload)、别的重试插件——同样排在本插件前面。这个顺序正是本插件「最后一个注册」的全部原因,也是 provider 自身策略始终保持权威的原因。

是否所有情况都可用

这是宿主插件,不是 preset 插件,所以它对同一个 profile 启动的每个会话、每种 preset 都生效。以下都按真实契约验证过:

场景 · 是否生效 · 依据

任意 agent preset(standard / ptc / minimal / cordis) · ✅ · 宿主插件,不依赖 preset

同一组合下的多个兄弟 agent · ✅ · 各自持有独立阶梯,互不干扰

挂载在 preset 作用域内的 agent · ✅ · 真实 Cordis 作用域分发测试通过

headless / ACP / SDK 组合(无 Web 服务器) · ✅ 重试生效,无面板 · 只注册一个事件监听器,不依赖 Web 服务

DSH Web 界面 · ✅ · dsh.client 面板

官方桌面客户端(Electron) · ✅ · 桌面 profile = Web 模板,加载同一 Web UI

ABORTED(你主动取消) · ❌ 设计上永不重试 · 取消不是故障

provider 适配器没接纳的请求 · ❌ 到不了这里 · 不产生 agent/request-error

agent 循环拿到 turn 之前的失败 · ❌ 到不了这里 · 同上

特性

重试行为

  • 🔁 真正无限:默认不设次数上限,一直重试到成功或你叫停
  • ⏱️ 指数退避 + 抖动:initialDelayMs 起算,每次翻倍,封顶 maxDelayMs,每轮叠加随机抖动避免同步风暴
  • 🚧 上限是硬约束:maxDelayMs 是天花板,任何配置冲突都以它为准。设了 10ms 就是 10ms,不会因为 initialDelayMs 更大而被悄悄抬高
  • 🛑 取消永不重试:ABORTED 在任何配置下都终止(见下)
  • 🎯 provider 延迟照办但封顶:providerRetryAfterMs 会被尊重,同时被 maxDelayMs 夹住,单家 provider 无法把整轮运行无限期按住
  • 🧭 按 step 重置:一个阶梯属于一个 step,新 step 从首次延迟重新起算,不会从上一步的天花板接着跑
  • 🔌 无 provider 策略也重试:适配器没发布 retryPolicy 时同样工作(可关)

会话事件

每次排定的重试写入两条事件,顺序与 dsh-llm-retry 一致:

1. llm/retry —— 在等待之前,所以日志里记录的等待是真实发生过的;

2. llm/retry-started —— 在等待之后,请求真正重新发出的时刻。

因此被中止或插件卸载取消的重试,会留下一条 llm/retry 而没有对应的 llm/retry-started —— 这恰好就是事实。mode 记为 always,policyKey 固定为 unlimited-retry,不会与 provider 的预算冲突。

设置面板

  • 📊 实时状态:小标签显示宿主端是否有响应
  • 🔢 计数器:累计重试、等待中的请求、正在重试的会话、最近一次重试、运行时长
  • 🪜 实际重试顺序:每次尝试会发生什么;不设上限时如实显示 No limit
  • 📈 分布:按错误码、按 provider;另有一栏因错误码被判终止而主动放弃的次数
  • 🕒 最近重试:第几次、错误码、provider、延迟、时间(相对显示)
  • ⚙️ 策略编辑:开关与数字直接经 configEditor 写回 profile 补丁层,重启仍生效,并热重载宿主端
  • 🏷️ 终止错误码:逐个添加(带真实错误码下拉建议),或一键套用推荐集合
  • 🧹 清零计数:需要一次显式确认
  • 🎨 主题跟随:只用 --dsw-alias-* 变量,明暗主题与后续换肤自动生效
  • ♿ 可访问性:开关是真正的 role="switch" + aria-checked,页面不在自己组件树之外写 DOM
  • 🌏 中英双语:面板与元数据均双语

ABORTED:唯一无条件终止的码

取消不是故障——要么这一轮被叫停,要么进程正在退出。dsh-llm 在 signal.aborted || code === "ABORTED" 这两种情况下都会给出 aborted 结束态,也就是说 ABORTED 可能在循环自身的 signal 还没翻转时就到达本插件。

重试它等于把你刚刚取消的工作重新跑一遍,所以这条判断放在 excludeCodes 之前,清空该列表也不会让它恢复。两侧匹配都忽略大小写并去除空白,因此 Aborted、ABORTED、 aborted 是同一个码。

面板里会显式写出这条规则,不会让它看起来像某个设置被忽略了。

默认值,以及它的代价

autoRetry: true
maxAttempts: null        # 不设次数上限
initialDelayMs: 1000
maxDelayMs: 60000
jitterRatio: 0.2
retryWithoutProviderPolicy: true
excludeCodes: []         # 没有配置层面的终止码(ABORTED 始终终止)
recordSessionEvents: true
log: true
markerFile: null

默认没有任何「配置层面」的终止错误码——包括认证、配额、计费、上下文溢出。 这就是这里「无限」的含义,并且挂载时会明确提示一次,让它是一个可见的决定,而不是一次无声的花费。唯一的约束是 maxDelayMs:一个永久失败的请求大约每分钟重试一次,一直持续下去。

两种收口方式:

1. 设置 maxAttempts 为正整数 —— 本插件在每个 step 重试这么多次后交回上层,效果等同于没装。

2. 把终止错误码写进 excludeCodes。 面板里有一键使用推荐集合,这些码的共同点是「重发一次仍然同样失败」:

AUTH、UNAUTHORIZED、FORBIDDEN、BILLING、QUOTA、ACCOUNT_QUOTA、

MISSING_CREDENTIAL、INVALID_CREDENTIAL、MODEL_NOT_FOUND、NO_ADAPTER、

CONTEXT_WINDOW_EXCEEDED、CONTEXT_OVERFLOW、CONTEXT_LENGTH、INVALID_REQUEST

> IMAGE_OFFLOAD_REQUIRED 故意不在推荐集合里:dsh-compaction-image-offload 会靠压缩请求来恢复它,而且注册在本插件之前;它若放弃,重试才是下一步正确动作。

清空的数字字段会回落到默认值,而不是被当成 0(Number(null) 恰恰是 0,那会让延迟变忙循环、抖动变零)。

安装

从 npm(推荐)

# Web 界面
dsh plugin --profile web add dsh-unlimited-retry

官方桌面客户端请在桌面端会话里用内置的 plugin_manager 安装 ——

见 [装在哪里](#装在哪里)。命令行按设计拒绝 --profile desktop。

从本仓库

dsh plugin --profile web add github:xalor888/dsh-unlimited-retry
⚠️ npm 名与 github: 是同一个包名

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::7,最后更新 2026-11-01

Links

GitHub · Homepage · npm

Same category