# dsh-retry-boost Auto-retry transient gateway failures until the task completes — a DSH plugin that hot-applies a resilience `retryPolicy` to every `llm-pi-ai` provider (also re-applied automatically when providers are added at runtime). No more manual **"continue"** after rate-limit disconnects. [中文说明](./README.md) --- ## The problem When a gateway answers with **`429 insufficient_quota`** — the exact wording SenseNova (and many other LLM gateways) uses — DSH's error classifier maps it to the **`QUOTA`** failure code ([`isQuotaExceededError`](https://github.com/deepseek-ai/deepseek-harness)) *before* the generic 429 → `RATE_LIMIT` mapping. DSH's built-in request retry (`dsh-llm-retry`) only retries failures whose code is listed in the provider's `retryableCodes`. The **default** policy is: ```text retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT] // ← no QUOTA maxRetries: 5 backoff: 500ms → 10s ``` `QUOTA` is **not** in that list, so a transient `429 insufficient_quota` fails fast: no retry at all, the turn dies instantly, and every manual "continue" hits the same wall. Relevant upstream report: [deepseek-ai/deepseek-harness discussion #892](https://github.com/deepseek-ai/deepseek-harness/discussions/892) (0 replies so far — the fix is not upstream yet). ## The fix This plugin writes a widened `retryPolicy` into the `llm-pi-ai` settings namespace on startup. llm-pi-ai's settings `onChange` hot-applies it — **no restart needed**. The injected policy: ```text retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, QUOTA, SERVER, TIMEOUT, TRANSPORT, PI_AI_ERROR, UNKNOWN] maxRetries: 50 backoff: 1s → 60s, jitter 0.2 (exponential) ``` Result: a transient 429 / stream drop / timeout is retried with a widening backoff **until the task completes** — same as the settings.yaml workaround used in production against SenseNova, but packaged so anyone can install it. ## Coverage and known limits This plugin injects into providers of the `llm-pi-ai` settings namespace — **all direct primary routes are covered**. The following are **out of reach** (the plugin has no configuration surface to influence them): - **Image-input variant routes** (e.g. `vision-toolkit-`): separate adapters registered by vision plugins (e.g. dsh-vision-toolkit) via `registerAdapter`. Their `retryPolicy` is captured once at registration and never reads settings. DSH falls back to the built-in default for such routes — whose `retryableCodes` do **not** include `QUOTA` — so a SenseNova 429 still fails fast on that route. - The plugin auto-**detects and logs a warning** for such routes (`warnUnprotectedVariants`, on by default, re-checked on adapter updates). If you see `variant route "..." retries WITHOUT QUOTA`, that is this detector. - Way out: patch the variant adapter upstream (delegate `providerRetryPolicy` to the upstream route), switch to a natively image-capable model, or disable paste auto-switching to variants. - **Self-registered routes of other third-party LLM adapter plugins**: same story — they bypass the llm-pi-ai namespace, so injection cannot reach them. ## Why a plugin and not just settings.yaml? - **One command install** — works for every provider, not just the one you hand-edited. - **Follows new providers** — add a provider tomorrow; the policy is applied on the next start. - **Configurable** — `fill` / `boost` / `force` strategies, adjustable budget. - **Transparent** — the injected policy shows up in `settings.yaml`, fully editable and visible; the plugin is idempotent and never fights your edits. ## How it differs from `dsh-chat-continue` | | dsh-retry-boost (this) | [dsh-chat-continue](https://github.com/Chu-m/dsh-chat-continue) | | --- | --- | --- | | Layer | **First line of defense** — makes DSH's own retry actually work | **Second line** — retries *after* the built-in retry is exhausted | | Mechanism | Injects `retryPolicy` into every `llm-pi-ai` provider (official retry chain) | Intercepts `agent/request-error` and re-issues the failed request itself | | Backoff | Exponential, 1s → 60s, jittered | Fixed interval | | Manual confirm | — | Optional notification popup | | Install | `dsh-retry-boost` (npm) | `@wuxjs/dsh-chat-continue` | They are **complementary**: run this one alone, or stack it with dsh-chat-continue as a last-resort net. ## Requirements - DSH `>= 0.1.1-rc.1` (host plugins via `dsh.bundle.patch`) - Node.js `>= 20` ## Install Option 1 — from npm (recommended): ```bash dsh plugin --profile add dsh-retry-boost ``` Option 2 — straight from GitHub (tracks the main branch): ```bash dsh plugin --profile add "github:hhb1028/dsh-retry-boost#main" ``` Then restart DSH. Check the log for lines like: ``` dsh-retry-boost: injected retry policy for "sensenova" (mode=normal retries=50 backoff=1000ms->60000ms codes=8) ``` The injected policies are then visible in your profile's `settings.yaml` under `llm-pi-ai.providers.*.retryPolicy` — edit them freely. ## Configuration Tune the plugin in the roster row (`cordis.patch.yml`) merged into your profile, or through your DSH plugin-config surface: | Key | Default | Meaning | | --- | --- | --- | | `strategy` | `fill` | `fill` = only providers **without** an explicit `retryPolicy`; `boost` = also merge missing codes and raise caps on explicit ones; `force` = replace every provider's policy | | `mode` | `normal` | `normal` = bounded retries; `always` = unbounded (DANGER: retries forever) | | `maxRetries` | `50` | retry budget in `normal` mode | | `initialDelayMs` | `1000` | first backoff delay | | `maxDelayMs` | `60000` | backoff ceiling | | `jitterRatio` | `0.2` | jitter ratio (0–1) | | `retryableCodes` | the 8 codes above | failure codes to retry | | `warnUnprotectedVariants` | `true` | log a warning when an unprotected variant route is detected (see "Coverage and known limits") | ## Uninstall ```bash dsh plugin --profile remove ``` The injected `retryPolicy` sections stay in `settings.yaml` after removal — delete them by hand (or tell the plugin to `force` before uninstalling, then restore your own values) to return to DSH defaults. ## How it works 1. On startup the plugin reads the `llm-pi-ai` settings namespace. 2. For each provider it plans a `retryPolicy` per `strategy` (pure functions in `lib/policy.js`, fully unit-tested). 3. It merges the patch through DSH's settings provider: schema-validated by llm-pi-ai (the same `RetryPolicySchema` from `dsh-llm`), persisted to `settings.yaml`, hot-applied by `installSettingsSection`'s `onChange`. 4. Writes are idempotent: re-running with an already-boosted config produces an empty patch. 5. Variant-route watch: listens for `llm/adapters-updated` and re-checks the captured `retryPolicy` of `vision-toolkit-*` style variant routes; warns once per route while QUOTA is missing (re-armed after a fix). Warn-only — an honest statement of the reach boundary. ## Development ```bash npm test # unit + schema-integration + apply-flow tests (node --test) npm run check # syntax check ``` No build step — the plugin is plain ESM. The schema-integration test passes generated policies through the *real* `RetryPolicySchema` / `resolveRetryPolicy` from your framework checkout, so an injected patch can never be rejected by the settings validator. ## Roadmap - [x] Publish to npm (`dsh-retry-boost`, v1.2.0 live) - [x] Friendlier reload: re-run the plan when `llm-pi-ai` settings change (v1.2.0: listens on `llm/adapters-updated`, debounced 300ms, idempotent — providers added at runtime are protected without a restart) - [ ] Optional settings page (Settings → Retry Boost) reusing a generic form ## Changelog ### 1.2.0 - Providers added at runtime are now protected automatically: the plugin re-sweeps on `llm/adapters-updated` (shared with the variant-route warning), debounced 300ms. The plan is idempotent — sweeps triggered by this plugin's own writes never write again, so there is no update loop. Startup behavior is unchanged from v1.1.0. - Useful when you add a new provider to settings.yaml (e.g. a second key as a backup channel) or via the settings UI while DSH is running. ## License MIT