# dsh-request-manager [δΈ­ζ–‡](./README.md) | English Manage LLM API request policies for DeepSeek Harness β€” override the default request behavior. The plugin adds a **Request Manager** button to the session header; click it to open the **Request Manager panel**. All settings are global across sessions and need no separate on/off switch β€” each feature engages based on its configured value. ![Request Manager panel](assets/screenshot_en.png) ## Features ### Extra request delay Inserts an extra delay (milliseconds) between API requests. Each session's first request is exempt; automatic and manual retries are delayed as well. `0` = off. ### Retries Overrides the number of automatic retries after a failed API request (the DSH default is 5 in current versions). Leave empty for no override. Some kinds of API failures are never retried by DSH by default (e.g. `SSE stream ended without [DONE]`, `NETWORK ERROR`); this option does not affect the retry policy of such failures. ### Intercept "request failure" When an API request fails, the Request Manager panel expands automatically and shows a card for handling the intercepted failure: ![Intercept "request failure"](assets/screenshot2_en.png) - **πŸ” Retry**: re-sends the same request in place (still subject to the extra delay); if it fails again, the card shows again β€” retry as many times as you like. - **🚩 Give up**: ends the turn through DSH's native failure path. This option applies uniformly, including failures DSH would never retry by default. The timeout is configurable in seconds: after the configured duration the selected action runs automatically β€” retry or give up; `0` disables the timeout. Pressing stop or disposing the session releases it immediately. With auto-retry selected, a persistently failing request re-sends every timeout cycle. ## Install ```sh dsh plugin --profile web add github:tree-func/dsh-request-manager ``` Restart the DSH process after installing. ## Uninstall ```sh dsh plugin --profile web remove dsh-request-manager ``` ## Architecture A single-package bundle plugin with a host half and a client half: ``` dsh-request-manager/ β”œβ”€β”€ package.json # dsh.bundle.patch + dsh.client.platform: web manifest β”œβ”€β”€ cordis.patch.yml # inserts this bundle at the profile root layer β”œβ”€β”€ index.js # host half β”œβ”€β”€ client.js # browser half └── test/ β”œβ”€ smoke.mjs # smoke regression: all three paths in a real cordis app └─ persistence.mjs # persistence regression: panel ↔ localStorage ↔ host ``` - **Host (index.js)**: mounted at the web profile root layer as a bundle; listens at the root scope to every session's `agent/request` (extra delay) and `agent/request-error` (retry-count override, failure interception) events, and registers a resident `/reqmgr-api` route for the browser to read/write global config and submit interception decisions. - **Client (client.js)**: loaded through the official ModuleLoader; registers the Request Manager button and dropdown panel into the `conversation.session.header.utilities` slot, polls interception state once per second, and styles everything with `--dsw-*` semantic tokens so light and dark themes both work. Settings persist in the browser's localStorage and are restored to the host on page load. ## Compatibility Targets **DeepSeek Harness 0.1.6-alpha.2**. A harness upgrade can break a plugin *silently* (a renamed event, a changed payload shape or a removed reader raises no error β€” the feature just stops working), so the repository ships a smoke test: ```sh npm test ``` It reports one of two outcomes: - `ALL PASS` β€” all three paths (extra delay / retry override / failure interception) work in a real cordis app; - `SKIP ... no resolvable DeepSeek Harness runtime` β€” no harness packages are resolvable on this machine, so the whole test is skipped (still exit code 0). > **Past regression**: the `Session.events` getter was removed upstream (harness commit `5660f44d29`). The plugin used to walk it to count "N automatic retries"; after the upgrade it threw `TypeError: agent.session.events is not iterable`, taking down the whole `agent/request-error` waterfall so the interception card never appeared. Counting is now maintained by the plugin itself (any `{kind:'retry'}` handed back downstream counts once) and no longer reads the session event log. The smoke test covers exactly this. ## Settings persistence The configuration is a **global in-memory singleton on the host** (a DSH restart resets it to defaults); the browser's localStorage is the only durable copy. The panel, local storage and host must therefore never drift apart. The client half tracks three pieces of state: | State | Meaning | | --- | --- | | `dirty` | Fields the user changed that still need to reach the host. **A write pushes only these** β€” never a full-field overwrite β€” so a stale panel value can't clobber what another tab just wrote | | `pinned` | Fields whose displayed value is owned locally: being edited, or a startup restore the host hasn't accepted yet. Host polling never refills pinned fields, so a late response can't erase what you're typing | | `seeded` | Whether a source of truth exists yet. The first host config seen with no local storage is written straight to storage, so a read-only browser still persists it | Supporting behaviour: - **Debounced saves use `ctx.timeout` (plugin-fiber scoped)** β€” switching sessions or unmounting no longer discards what you typed; only unloading the plugin cancels; - **Writes push just the changed fields**, validated one at a time: a field in an editing intermediate state (e.g. `abc`) is skipped on its own without swallowing the other fields saved in the same batch; - **A failed write keeps your input and stays `dirty`**, retries on the next poll tick, and shows one visible toast; - **A rejected startup restore** is surfaced the same way and retried until the host accepts it; - **Reading localStorage only accepts real `number`/`boolean`/`null`** β€” `Number(null) === 0` would silently turn a timeout into `0`, which means "timeout disabled"; all such coercions are gone. `test/persistence.mjs` covers all of the above (34 assertions); every scenario corresponds to a defect that was actually reproduced. ## Known Limitations - **Conversations are isolated from each other**: interception notices (the alert dot and the auto-expanded panel) appear only in the conversation where the failure happened; staying in another conversation shows no hint. If you don't switch back in time, the hold is handled automatically according to the timeout settings. - **The automatic-retry count includes non-provider retries**: `compaction-basic` and `compaction-image-offload` also return `{kind:'retry'}` on `agent/request-error` (after repairing context/images they re-send in place without spending provider retry budget). Those are counted too, because from the user's point of view the request *was* automatically re-sent. ## Localization UI copy is registered with the official locale service (namespace `requestManager`) and switches between Chinese and English automatically following the DSH language setting. ## Build The plugin is plain JavaScript β€” the source is the artifact, no build step: ```sh npm test # syntax check + persistence regression (34) + smoke regression (real cordis app) ``` Development loop: refresh the browser page after editing `client.js`; restart the DSH process after editing `index.js`. ## License [MIT](./LICENSE) --- This plugin was built with AI assistance.