xalor888/dsh-unlimited-retry

dsh-unlimited-retry

Unbounded model-request retry for the DeepSeek Harness agent loop, with a live Settings panel.

待分类deepdeepseek-harnessdsh

dsh-unlimited-retry

English · 简体中文

Unbounded model-request retry for the DeepSeek Harness agent loop, with a live Settings panel.

A long unattended run should not die because one request hit a transient network error. This plugin keeps such a request retrying — exponential backoff, with a delay ceiling — until it succeeds or you stop it.

Standard DSH bundle plugin, one-command install. The same package supports both the DSH Web UI and the official desktop client (Electron).

Where to install it

This repository maintains one product line (main). The same package supports both surfaces.

Surface · Install DSH Web UI · Terminal: dsh plugin --profile web add dsh-unlimited-retry Official desktop client (Electron) · No terminal — ask the desktop session to install it with the built-in plugin_manager (below)

Desktop client: do not use a terminal command

The desktop reads the `desktop` profile, and the CLI refuses to touch it by design:

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

A --profile web install therefore lands in the web profile, and the desktop window reads none of it — the symptom is a Settings page that simply is not there, with nothing in the console.

Instead, say this in a desktop-client session:

Please install dsh-unlimited-retry using this host's built-in
plugin_manager. Check the current profile and whether a package of that name
is already installed first; if it is, report its source and version. Then tell
me the result and whether a restart is needed.

plugin_manager is scoped to the current profile (the desktop's is desktop) and uses the host's own package manager to install and register the plugin. No terminal, no cookie construction, no hand-editing a host-managed profile file. Pin a version by asking for dsh-unlimited-retry@.

If Settings does not show the page after a first install, fully quit the client (including the tray) and reopen it rather than stacking a second install.

Why the desktop works: the desktop profile *is* the Web template (base +
web-app), and Electron loads the local Web UI URL. The dsh.client panel is
therefore loaded by client-modules there too — no separate client build.
The dsh web command line uses its own web profile: restart the host after an
update, then hard-reload the browser (Ctrl+F5, or Cmd+Shift+R on macOS) to
bypass the cache.

Why a separate plugin

DSH ships @deepseek-ai/dsh-llm-retry, but it only *executes* retries — the budget lives on each provider adapter's retryPolicy. Its default mode: "normal" gives up after maxRetries (5 in this build) and then delegates, so the step fails, the turn closes with an error, and the run ends. mode: "always" exists but is all-or-nothing per provider and has no delay ceiling.

This plugin adds a layer *behind* all of them. agent/request-error is a waterfall event and Cordis runs listeners outermost-first, so the last-registered listener is the deepest: every other recovery gets the first word, and only a failure that nobody recovered reaches this plugin.

The retry ladder

With the stock 5-retry provider policy the effective order is:

Attempt · Handled by · Behaviour 1 – 5 · the provider's retryPolicy · Its own budget, codes, and delays 6 – ∞ · this plugin · Exponential backoff with jitter, capped at maxDelayMs

Other layers registering on the same event — context-overflow compaction, image offload (dsh-compaction-image-offload), any other retry plugin — sit ahead of this one too. That ordering is the entire reason this plugin registers itself last, and it is what keeps the provider's own policy authoritative.

Does it work everywhere?

It is a Host plugin, not a preset plugin, so it applies to every session the profile starts, under every preset. All of the following was verified against the shipped contracts, not assumed:

Situation · Applies · Basis Any agent preset (standard / ptc / minimal / cordis) · ✅ · Host plugin, preset-independent Sibling agents in one composition · ✅ · Each keeps its own ladder An agent mounted inside a preset scope · ✅ · Real Cordis scope-dispatch test headless / ACP / SDK composition (no Web server) · ✅ retries, no panel · One event listener; no Web service needed DSH Web UI · ✅ · dsh.client panel Official desktop client (Electron) · ✅ · Desktop profile = Web template, same Web UI `ABORTED` (you cancelled) · ❌ never retried, by design · A cancellation is not a fault A request the adapter never admitted · ❌ never reaches the event · No agent/request-error is produced A failure before the loop has a turn · ❌ never reaches the event · Same

Features

Retry behaviour

  • 🔁 Genuinely unbounded: no attempt limit by default; it retries until success or until you stop it
  • ⏱️ Exponential backoff with jitter: starts at initialDelayMs, doubles, capped at maxDelayMs, with per-delay randomization so synchronized storms cannot form
  • 🚧 The ceiling is a hard constraint: maxDelayMs wins any conflict. Ask for 10 ms and you get 10 ms — it is never silently raised because initialDelayMs was larger
  • 🛑 A cancellation is never retried: ABORTED is terminal at any configuration (see below)
  • 🎯 Provider delays are honoured but clamped: providerRetryAfterMs is respected and capped by maxDelayMs, so one provider cannot park the run indefinitely
  • 🧭 Per-step ladders: a ladder belongs to one step, so a new step restarts at the initial delay instead of resuming at the previous ceiling
  • 🔌 Works without a provider policy: also retries when the adapter published no retryPolicy (toggleable)

Durable events

Each scheduled retry appends two events, in the same order as dsh-llm-retry:

  1. llm/retry — before the wait, so the log shows a wait that really happened;
  2. llm/retry-started — after the wait, when the request is actually re-sent.

A retry cancelled by an abort or a plugin unload therefore leaves a llm/retry record with no matching llm/retry-started, which is exactly what happened. mode is recorded as always, and policyKey is the fixed namespace unlimited-retry, which cannot collide with a provider budget.

Settings panel

  • 📊 Live status: a pill shows whether the host half answers
  • 🔢 Counters: total retries, waits in flight, sessions retrying, last retry, uptime
  • 🪜 Effective ladder: what happens on each attempt, with an honest No limit instead of a fabricated cap
  • 📈 Breakdown: retries per failure code and per provider, plus the failures declined because their code is terminal
  • 🕒 Recent retries: attempt, code, provider, delay, and when, with relative times
  • ⚙️ Policy editor: switches and numbers write straight back to the profile patch layer through the Harness configEditor, surviving a restart and hot-reloading the host half
  • 🏷️ Terminal codes: add one at a time (with suggestions from the real Harness vocabulary), or apply the recommended set in one click
  • 🧹 Reset counters: behind an explicit confirmation
  • 🎨 Theme-driven: only --dsw-alias-* tokens, so light/dark and future rethemes follow automatically
  • ♿ Accessible: switches are real role="switch" controls with aria-checked; no DOM outside the component tree
  • 🌏 Bilingual: panel and display metadata, English and Chinese

ABORTED: the one unconditional terminal code

A cancellation is not a fault — the turn was stopped, or the process is shutting down. dsh-llm produces an aborted finish when signal.aborted || code === "ABORTED", which means an ABORTED failure can arrive while the loop's own signal is still live.

Retrying it would re-run work you just cancelled, so the guard runs before excludeCodes and clearing that list does not re-enable it. Code matching is case-insensitive and whitespace-trimmed on both sides, so Aborted, ABORTED and aborted are the same code.

The panel states this rule explicitly, so it never looks like a setting was silently ignored.

Defaults, and what they cost

autoRetry: true
maxAttempts: null        # no attempt limit
initialDelayMs: 1000
maxDelayMs: 60000
jitterRatio: 0.2
retryWithoutProviderPolicy: true
excludeCodes: []         # no configured code is terminal (ABORTED always is)
recordSessionEvents: true
log: true
markerFile: null

No configured failure code is terminal by default — including auth, quota, billing, and context-overflow codes. That is what *unlimited* means here, and it is announced once at mount so it is a visible decision rather than a silent spend. The only bound is maxDelayMs, so a permanently failing request costs roughly one request per minute, forever.

Two ways to bound it:

  1. Set `maxAttempts` to a positive integer — this plugin then stops after

that many tries per step and delegates, exactly as if it were not installed.

  1. List terminal codes in `excludeCodes`. The panel has a one-click

*Use recommended set* action for the codes where a retry re-sends a request that fails identically:

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 is deliberately absent: dsh-compaction-image-offload
recovers it by shrinking the request and registers ahead of this plugin. If
it declines, retrying is the correct next move.

A cleared numeric field falls back to its default rather than being read as zero — Number(null) is 0, which for a delay is a busy loop and for jitter is no jitter at all.

Install

From npm (recommended)

# Web UI
dsh plugin --profile web add dsh-unlimited-retry

For the official desktop client, use the built-in plugin_manager from a desktop session instead — see Where to install it. The CLI refuses --profile desktop by design.

From this repository

dsh plugin --profile web add github:xalor888/dsh-unlimited-retry
⚠️ The npm name and the github: spec are the same package name — **don't mix
them**; installing both leaves conflicting entries. If you installed via
github:, run dsh plugin --profile web remove dsh-unlimited-retry first.

Local development

dsh plugin --profile web add link:/absolute/path/to/dsh-unlimited-retry

Then open Settings → Retry.

Verifying a mount

If the panel does not appear, check the host half and the client half separately:

  1. Host half: ~/.dsh/adapters/unlimited-retry/mounted.json exists and its

version is the version you meant to install;

  1. Client half: /plugins/??dsh-unlimited-retry/client.js&rev=… returns 200

in the browser (or the desktop's devtools).

Host half present but client half missing is a cache problem — hard-reload (Ctrl+F5, or Cmd+Shift+R on macOS). If the host half is absent, the plugin never mounted: go back to the install step and confirm which profile you installed into.