# dsh-all-enhance [![npm](https://img.shields.io/npm/v/dsh-all-enhance)](https://www.npmjs.com/package/dsh-all-enhance) [![npm downloads](https://img.shields.io/npm/dt/dsh-all-enhance)](https://www.npmjs.com/package/dsh-all-enhance) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg) [![dshfind](https://dshfind.com/api/badge/rangdl/dsh-all-enhance?lang=en)](https://dshfind.com/zh/plugins/rangdl/dsh-all-enhance?ref=badge) DSH (DeepSeek Harness) enhancement plugin: session delete, model request headers, and model fallback chains — all toggleable and configurable from the "Enhanced Settings" section of the Web settings page. > English | [中文](./README.md) ## Quick start Requires dsh (DeepSeek Harness). One command installs into a target profile (e.g. `web`); restart to take effect: ```sh dsh plugin --profile web add dsh-all-enhance ``` Pinning the stable version is recommended, so a later release cannot silently change the code that actually runs: ```sh dsh plugin --profile web add dsh-all-enhance@latest ``` After installing, the three features (session delete, model request headers, model fallback chains) can be toggled and configured from **Settings → Plugins / Enhanced Settings** in the Web UI. See [Installation](#installation) for more options. ## Features ### Session Delete Adds a "Delete Session" item to the session list's more-menu: - Cold (non-running) sessions are permanently removed from local storage, including log files and workspace accounting; the sidebar row disappears immediately. - Running sessions are refused: server-side 409 + TOCTOU re-check — a running task is never accidentally deleted. - Deletion requires a second explicit confirmation; directory removal is guarded by a session-id name check so a DSH layout change cannot wipe unrelated directories. ### Model Request Headers Custom headers for outbound model requests (e.g. `User-Agent`), stored in the plugin's own settings namespace (nothing is written to `llm-pi-ai`, so any route — including built-in providers — can be configured): - **Precedence**: per-model (keyed `provider/model`) > per-provider > global; same-name headers override case-insensitively. - **Injection scope**: chat streams, model discovery, and the underlying `fetch` — one `AsyncLocalStorage` scope spans the whole request chain. - **Built-in User-Agent presets** (claude-cli / claude-code, …) available as one-click fills, still freely editable. - Zero injection when the master switch is off; headers are validated before saving (RFC 9110 token names, no line breaks in values — CRLF injection guard). ### Model Fallback Chains When a chat request fails, automatically switch to the next model in a chain and keep going — no manual model swapping: - **Trigger**: any failure code matching the trigger codes. Defaults cover invalid credential / quota / rate limit / dropped connections (`TRANSPORT`) / stream idle timeouts (`TIMEOUT`); stream interruptions are covered, user-initiated aborts are not. - **Multiple chains + virtual routes**: each chain is registered as a virtual route shown in the model picker as `EnhanceChain/` — selecting one makes that chain the primary; the built-in `auto` chain aggregates every registered provider, sits at the top, and doubles as the default chain for plain-model failures. - **Chain editor**: entries picked from the model catalog dropdown (`provider/model`, `*` wildcards) or typed manually; per-entry up/down reordering; chain ids are letter-first plus `. _ -` digits. - **Stability**: failed routes are cooled down (timer release, or half-open probing with exponential escalation up to 16×); per-step switch caps prevent flapping. - **Observability**: switch toasts in real time (SSE push, degrading to a shared poller); a runtime status panel shows cooldown snapshots and switch history. - **Call log** (optional, `fallbacksLogEnabled`): records every failure — code, response message, fallback target (even when fallbacks are off) — with a failure-code histogram and one-click add-to-trigger-codes for tuning; persisted via a `storageDomain` domain, surviving restarts. - Fallback decision state is in-memory only; nothing is written to session logs. ## Installation As a bundle with a `dsh.bundle` patch layer: ```sh dsh plugin --profile add dsh-all-enhance ``` Or, from a checkout with `pnpm dsh`: ```sh pnpm dsh plugin --profile add ./dsh-all-enhance ``` The bundle registers one Cordis entry (`id: all-enhance`) that mounts both the Host half and the Web client half. ### Manual install without pnpm (restricted environments) When `dsh plugin add` (which goes through pnpm) is unavailable, install manually (nothing needs to be removed): ```sh # 1. Link the plugin into the profile's node_modules by package name # (junction; follows source changes). Replace # with your local dsh-all-enhance checkout directory. node -e "require('fs').symlinkSync('', process.env.USERPROFILE + '/.dsh/profiles/web/node_modules/dsh-all-enhance', 'junction')" # 2. Append to the profile's cordis.patch.yml (use the package name so the # client half is discovered): # - insert: # - id: all-enhance # name: dsh-all-enhance # 3. Start directly (do not also pass --patch, or you get a duplicate entry id) dsh web ``` > Note: setting `name` to a **file path** (e.g. `file:///.../src/index.js`) loads only the Host half — the client module system scans `dsh.client` declarations by *package*, so path-loading skips the UI; use the **package name** to get the UI. ## How it works / API surface The plugin uses official public service APIs wherever possible. Two official-private implementations (detaching an attached session from the in-memory SessionStore, and cleaning the `session_projcache` table) are reached through **capability probes**: if a DSH upgrade renames/removes them, the delete is degraded to a 409 refusal instead of crashing. | Concern | Public API used | |---|---| | Settings namespace registration | `installSettingsSection()` from `@deepseek-ai/dsh-settings` (lifecycle-managed) | | Running-session guard | `ctx.sessions.get(id)` (SessionStore) | | Session existence | `ctx.sessionPersistence.list()` | | Log-file location | `ctx.sessionPersistence.locate(header)` — the backend resolves the physical path; no hard-coded path encoding | | Workspace accounting | `ctx.workspaceRegistry.list()` + `Workspace.detachSession(id)` | | HTTP routes | `ctx.webServer.register()` inside `ctx.effect()` (auto-unregister on unload) | | Settings writes (client) | `api.settings.mutate` with `expectedRevision` + conflict retry | | Request-header injection | own settings namespace + `ctx.on('llm/stream')` + scoped `fetch` / `ctx.llm.discoverModels` wrappers (`ctx.effect`-managed, restored on unload) | | Fallback decision | `ctx.on('agent/request-error')` → `{ kind: 'retry' }`, pending switch applied on `ctx.on('agent/request')`, per-agent state cleaned on `agent/disposed` | | Virtual chain routes | `ctx.llm.registerAdapter(['EnhanceChain'], …)` — one `listModels` row per chain, `stream()` is a thin delegation to the chain head | | Fallback runtime status | GET `/dsh-all-enhance/fallbacks/status` (cooldown snapshot + switch history, in-memory only) | | Fallback call log | GET/DELETE `/dsh-all-enhance/fallbacks/log` — failure observation log (ring of 200, persisted via a `storageDomain` domain, fail-soft to memory) | | Model catalog | GET `/dsh-all-enhance/fallbacks/models` — aggregates `ctx.llm.listProviders()` + `listModels()`, data source for the chain editor dropdowns | | Fallback status push | GET `/dsh-all-enhance/fallbacks/events` (SSE) — pushes snapshots on switch/cooldown changes; degrades to a shared poller (~5 s, exponential backoff on consecutive failures) | ## Security - Delete/status routes are loopback-only: Host + `sec-fetch-site` + `origin` checks reject cross-site requests (CSRF/DNS-rebinding protection). - POST body is capped at 64 KiB and must be `application/json`. - A running session is never deleted (server-side 409; the client re-checks before showing the confirmation dialog). - Cold-session deletion is destructive and irreversible; the UI requires a second explicit confirmation. - Request headers are validated before saving: names must be RFC 9110 tokens and values must not contain line breaks (CRLF header-injection guard). ## Compatibility & known limitations - Requires the services: `webServer`, `settings`, `workspaceRegistry`, `sessionPersistence`, `sessions`. - The Web client currently has **no official extension slots** for the settings nav list or the session-list menu; the client injects DOM using internal hashed class names (`.VOzbGW_*`, `.YDXeBa_*`) and React fiber introspection. A DSH UI update may require selector maintenance. Tracked slots to migrate to: `settings.plugin.item`, `conversation.chat.node`. - No public API exists for the archive set (`archivedSessionIds` has no unarchive); deleting an archived session leaves a stale id in the archive set. - In-process header-index caches are not cleaned (no public API); a restart heals them via bootstrap. - `locate()` returns the primary log artifact; compaction snapshots and other backend artifacts are out of the public interface and are not removed. - Fallback chains depend on the `agent/request-error` / `agent/request` event contract and `ctx.llm.registerAdapter` of the current DSH; a host update that changes failure codes or the adapter registry may need a plugin update (unknown codes simply never trigger a fallback — safe degradation). - Stream-interruption fallback covers adapter-level failures (connection drops, idle timeouts — normalized to `TRANSPORT`/`TIMEOUT` finish chunks). Consumer- or middleware-level failures inside the `llm/stream` pipeline bypass `agent/request-error` entirely and are terminal in the host loop — no plugin can recover them. - Switch toasts prefer SSE push; when the stream is unavailable the client degrades to a shared ~5 s poller, so notifications may lag by up to one poll interval. ## References - [btspoony/dsh-llm-fallbacks](https://github.com/btspoony/dsh-llm-fallbacks) (MIT) — the fallback decision core this plugin learned from: `agent/request-error` takeover, cooldown suppression, and half-open recovery (this plugin drops its role/schedule/subagent policies and aligns with dsh 0.1.1-rc.2 failure codes and event contracts; its issue #52 session-event pitfall is avoided here). - [dsh-custom-provider-settings](https://github.com/topics/dsh-plugin) (community plugin) — the request-header injection path reference: the same-shaped `llm/stream` event + `AsyncLocalStorage` + fetch wrapper approach (this plugin is a TypeScript rewrite with its own settings namespace and a distinct runtime key, so the two coexist). - [dsh-market](https://github.com/dsh-market/dsh-market) — the build layout this plugin mirrors: `tsc` host + `tsdown` client, structural service-subset types, bundle/patch-layer organization. - [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) — the host framework (DSH) this plugin targets. ## Development Sources are TypeScript; the build mirrors [dsh-market](https://github.com/dsh-market/dsh-market) (`tsc` compiles the host, `tsdown` bundles the client): ``` src/ ├── host/ # Host half (TypeScript, tsc → lib/) │ ├── index.ts # plugin entry apply (composes the modules below) │ ├── ctx.ts # structural service-subset types (incl. the two private penetrations) │ ├── constants.ts # constants + settings schema (settings namespace) │ ├── http.ts # HTTP / CSRF helpers │ ├── delete-session.ts # delete pipeline (detach/cleanup/guards) │ ├── request-headers.ts # request-header injection bridge (global + per-provider/per-model) │ ├── fallbacks.ts # fallback aggregate entry (re-exports) │ ├── fallbacks-decision.ts # fallback decision core (config trimming/chain parsing/state-machine pure functions) │ ├── fallbacks-bridge.ts # fallback event bridge (agent events + call log + status snapshots) │ ├── virtual-adapter.ts # virtual EnhanceChain adapter (registerAdapter) │ ├── call-log-store.ts # call-log persistence (storageDomain domain storage) │ └── routes.ts # HTTP routes (delete/status/fallbacks status/log/models/events) ├── client/ # Client sources (TypeScript, tsdown → client/client.js) │ ├── index.ts # entry apply (settingsScope bind / slots / menu injection) │ ├── types.ts # client structural context types │ ├── selectors.ts # constants + official UI selectors │ ├── locales.ts # zh/en locale dicts + makeT (keys constrained by the Dict interface) │ ├── toast.ts # toast │ ├── dialog.ts # delete confirmation dialog │ ├── shared-styles.ts # shared style constants │ ├── collapsible.ts # collapsible card component (settings sections) │ ├── settings-section.ts # "Enhanced Settings" section component (settings.section slot) │ ├── session-menu.ts # session menu injection (DOM hack) │ ├── delete-flow.ts # delete flow (status → confirm → delete → refresh) │ ├── request-headers-editor.ts # request-headers editor (global/provider/model layers) │ ├── fallbacks-editor.ts # fallback chain editor (multi-chain + histogram + runtime status) │ ├── fallbacks-poll.ts # shared status feed (SSE-first / poller fallback) │ ├── fallbacks-notify.ts # fallback switch toasts (history diffing) │ └── styles.ts # injected styles ├── lib/ # Build output (tsc: host JS + *.d.ts, gitignored; produced by prepare/build) └── client/client.js # Build output (tsdown: __ModuleLoader__ single bundle, committed — loader contract, must stay byte-stable) ``` ```sh pnpm install # prepare runs `pnpm build` automatically pnpm typecheck # tsc across both projects (host + client) pnpm build # tsc host → lib/ + tsdown client → client/client.js pnpm test # node --test (75 cases: manifest/delete/headers/fallbacks/routes/persistence/consistency) pnpm check # typecheck + build + test + preflight (full publish gate) ``` > The Web client is loaded as a single file by the dsh module system > (`/plugins//client.js`), so the split sources are bundled into one file > with tsdown (mirroring the official client packages; the artifact is a > `window.__ModuleLoader__.load({ id, factory })` factory, guarded by > `scripts/normalize-client-banner.mjs` + `scripts/preflight.mjs`). > `lib/` is gitignored: after a fresh clone run `pnpm install` (prepare > auto-builds) or `pnpm build`, otherwise the host half (`main: lib/index.js`) > cannot load. After editing `src/**/*.ts`, run `pnpm build` then restart dsh. See `FIX-PLAN.md` for the issue log and `QA-REVIEW.md` for the compliance review.