# Troubleshooting
English | [中文](TROUBLESHOOTING.zh.md)
## Current recovery steps
This section describes version `2.3.1`. Check release availability and the [exact-host results and acceptance limits](CURRENT_COMPATIBILITY.md) before updating. The numbered incident archive below preserves old observations and commands in their historical context, not a current setup recipe.
The protections below describe published `2.3.1`. Its exact tag/npm identity and downloaded-asset verification are recorded separately from runtime acceptance limits in the compatibility record.
1. Record the host version (`dsh --version`), installed plugin version shown in Settings → Custom mode, platform, and the failing step: installation, page opening, save/application, assistant selection, or first request. Desktop uses its in-app Plugins page; these CLI steps target the Web profile.
2. Run `dsh --profile web --dump-config`. A `dsh-custom-mode` row only proves that the composed configuration contains the plugin. It does **not** prove activation, page rendering, preset health, or a model request. No matching row calls for checking the selected profile and installed package; a restart alone does not install a missing package.
3. Restart only the process serving that profile through its normal launcher. Read its activation errors and the page's typed warning. Follow [the README first-request check](../README.md#verify-the-whole-path); do not stop at an open settings page.
4. Keep synthetic reproduction steps and a sanitized error/version report. Remove token URLs, credentials and private prompts/history before sharing. Do not copy a live user home into a test environment.
| Symptom | Recovery and success check |
| --- | --- |
| Wrong plugin version installed | Use the exact published version below and verify the version in the page after restart. A bare name or successful package-manager exit is insufficient. |
| Cannot find New assistant | In `2.3.1`, select New assistant… in the page header to move to the existing form and focus its name field. Published `2.3.0` has that form at the page bottom. Creation does not select the assistant for an existing/new conversation. |
| Unsaved edits after leaving Settings | Version `2.3.1` keeps drafts only in the current plugin context/browser document. Returning to the section restores them; reloading, closing the tab or disposing the plugin can clear them. Copy or export important text before confirming a leave-page warning. |
| Save reports a conflict | Copy/export the complete held draft first. Then explicitly select Discard edits and reload, compare disk content and reapply the intended edits. Ordinary refresh preserves the dirty baseline; repeatedly pressing Save cannot resolve the conflict. In `2.3.1` the checked state includes prompt, metadata and composition, even when history writes fail. |
| Duplicate while edits are unsaved | In `2.3.1`, confirmation explains that duplication copies the saved configuration. Confirm only if that is intended; the existing unsaved draft is retained. Cancel and save first when the copy should include those edits. |
| Update check unavailable | If an older host lacks the native query API, open plugin details or the [update guide](../README.md#plugin-updates). On a capable host, check the reported permission/network state. Do not remove a working installation merely to query its version. |
| Update status unknown, no recommended version, or permission refused | A query result is not an installation result. Keep the actual error/filtered/partial status; query and update permissions are separate on capable alpha hosts. Continue only through the official Plugins controls available to your connection. |
| Installed version differs from running version | Read both labels. A package on disk may await host restart/application; follow the official result and verify the running version afterward. Do not repeatedly reinstall based only on the displayed disk version. |
| Update navigation is blocked | Save or deliberately discard every assistant draft and unsubmitted new-assistant input, then finish any pending operation, Retry activation or Retry refresh. If an assistant left the current list, use View and keep draft to copy its complete held content before selecting Discard this draft… and confirming. That discard removes only this page's memory copy; it does not change disk files and cannot be undone in the page. Navigation does not silently save or discard drafts. |
| `unknown prompt variable "{{cwd}}"` | Keep the complete section name and exact host version. Upgrade to the accepted repair release and restart so the managed reader is refreshed. Do not delete unrelated prompt/suffix text. If an unknown variable remains, preserve your text and inspect that exact section; the current code's compatibility behavior is recorded with its tests. |
| File saved but application failed | Keep any current unsaved draft. Retry activation only reapplies the saved configuration. If this follows a native update/restoration involving 2.2.6, use the [one-host-restart recovery path](#native-226-recovery) below instead of repeatedly saving. Confirm the warning clears before starting a new session. |
| Plugins restored 2.2.6 but a later save is not active | A restored package does not prove the assistants have registered again. Keep the saved data and follow the [one-host-restart recovery path](#native-226-recovery); do not delete assistant data or repeat the save. |
| Operation succeeded but page refresh failed | Select Retry refresh. Do not repeat create/delete/save, which already completed; the retry only reads the current state. |
| Assistant missing after an interrupted save | Stop further edits and preserve the assistant directory, backups and recovery records. The repair release attempts recovery before discovery. If it still fails, retain the evidence for diagnosis; do not delete backups or assume permanent data loss. |
| Clone installer reports failure | Read the printed transaction directory. A verified rollback reinstalls the local old package and restores managed code. Exit 5 means recovery is incomplete: keep the old archive/logs and reinstall that actual archive through the host's plugin manager before restarting. Copying only package.json does not restore an installation. |
| Clone uninstaller reports failure | The `2.3.1` script stops cleanup/purge if the host removal fails or cannot be verified. Preserve the remaining installation and assistant files, resolve that reported error and retry through the same host. Do not erase profile files as a workaround; desktop removal belongs to the app's Plugins page. |
| `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` | Check the profile's pnpm policy and the requested exact version. If you choose a package-specific exception, keep one `minimumReleaseAgeExclude` entry (`- dsh-custom-mode`), not accumulating version entries. Do not disable the release-age policy for every package. The script does not edit this policy for you. |
### After native recovery restores 2.2.6
If Plugins reports **Update failed; restored 2.2.6**, then Custom mode says **Files are saved, but these assistants are not active yet** after an edit, first copy any unsaved drafts to a safe place before restarting. The save already wrote your data; deleting an assistant, repeating the save or reinstalling on that message is not the recovery step.
Restart only the corresponding Web host once through its normal launcher. Reopen Custom mode, check the running version, select the assistant in the new-session menu and read back its saved text. A deliberate test edit should save without the not-active warning. Desktop users must use their app's own restart path rather than Web profile commands.
This path was observed on isolated DSH Web `0.2.2-alpha.2` with the real published `2.2.6`: a target-tar download failure restored the old package and preserved the saved files, but subsequent saves did not activate. One host restart restored registration of both test assistants (2/2), selection and a subsequent save. The package recovery and later intentional edits were checked separately; no model request was made. This evidence does not establish a seamless hot migration or desktop/model/GPU acceptance.
For published `2.3.1` accepted on your exact host, the manual Web update command is:
```sh
dsh plugin --profile web add dsh-custom-mode@2.3.1
```
Restart and repeat the first-request check. Existing prompt, preset metadata and composition are user data. Clone installation requires Bash, Node.js and npm; it prepares local packages with offline `npm pack --ignore-scripts` and never downloads a newer rollback version. A missing or unarchivable old installation stops replacement before mutation. First-install failure has no old package to restore: the script removes the partial plugin and verifies the profile state, or reports incomplete cleanup.
## Historical incident archive
The following numbered entries record the versions and hosts investigated at the time. A fix's minimum historical version is not today's installation recommendation. Old logs, API assumptions and environment-variable workarounds are evidence, not current guarantees. In particular, current multi-assistant discovery does not require the old section 8 prompt-path override. For present behavior use the section above, [architecture](ARCHITECTURE.md) and [compatibility](CURRENT_COMPATIBILITY.md).
### 1. [Critical] After installing, dsh no longer starts and only a single `pending` line remains
#### Symptoms
`./install.sh` prints "installation complete" and exits with 0. Then dsh is restarted:
```
dsh: warning: 1 entry did not activate
custom-mode (dsh-custom-mode): pending (waiting for services: webServer, agentPresets)
```
The interface does not open, nothing is listening on the port, and the agent is gone too — the whole harness is left with this one plugin waiting for two services that will never appear.
The output of `dsh --profile web --dump-config` is suspiciously short:
```yaml
# == dsh-custom-mode
- id: custom-mode
name: dsh-custom-mode
```
#### Cause (already fixed, but machines that were already installed on need self-recovery)
`install.sh` used to contain a piece of logic that "cleans up ghost entries that cannot be resolved"; it probed every bundle through two hardcoded paths:
```
$DSH_HOME/profiles//node_modules//package.json
$DSH_HOME/profiles/node_modules//package.json
```
The in-box bundles shipped with the profile **template** (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`) are not in
these two locations — they are resolved by dsh's own installation anchor — so they were judged to be ghosts and **deleted from `dsh.profile.bundles`**. Once the base bundle is gone, `llm` / `session` / `webServer` / `agentPresets` are all no longer assembled.
This is not a guess, it was measured: running the old `install.sh` against a fresh `DSH_HOME`, the profile's bundles go from
`["@deepseek-ai/dsh-base","@deepseek-ai/dsh-web-app"]` to `["dsh-custom-mode"]`.
dsh's own `reconcilePlugins` explicitly states that in-box bundles "are never touched", so the correct approach is
**add only, never delete**. The current `install.sh` does exactly that, and adds a post-install self-check:
if the composition tree is shorter than 20 lines it errors out immediately, instead of letting you discover the problem on the next restart.
#### Self-recovery (for machines already broken by an old version)
Just add the missing names back to the profile manifest; nothing needs to be reinstalled:
```sh
# 1. See what is missing right now
node -p "JSON.stringify(require(process.env.HOME + '/.dsh/profiles/web/package.json').dsh.profile.bundles)"
# 2. Add back the two shipped with the template (varies by profile template; web/tui both use these two)
node -e '
const fs = require("fs");
const p = process.env.HOME + "/.dsh/profiles/web/package.json";
const j = JSON.parse(fs.readFileSync(p, "utf8"));
const want = ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"];
j.dsh.profile.bundles = [...new Set([...want, ...(j.dsh.profile.bundles ?? [])])];
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
console.log("bundles =", j.dsh.profile.bundles);
'
# 3. Confirm the recovery: there should be hundreds of lines
dsh --profile web --dump-config | wc -l
```
After restarting dsh, normal operation should be restored.
> If `$DSH_HOME` was backed up **before** installing the plugin, this `package.json` can also be recovered directly from the backup.
---
### 2. On WSL, `dsh plugin add` fails: pnpm's Rust panic
#### Symptoms
```
==> 2/2 安装设置页插件 "dsh-custom-mode" 到 profile "web"
Error: × Main thread panicked.
├─▶ at pnpm\crates\config\src\defaults.rs:51:39
╰─▶ current dir is an absolute path with drive letter
dsh: pnpm failed in profile directory /home//.dsh/profiles/web
```
#### Cause
`dsh plugin` **forwards to the `pnpm` on the PATH** (`spawnSync("pnpm", …)`, cwd = the profile directory).
WSL's PATH includes Windows directories by default, so it very easily picks up the `Windows` version of
`pnpm.cmd`/`pnpm.exe` first. It interprets the cwd `/home/...` in the Windows way, and therefore panics.
#### Fix
Make the pnpm on the PATH the **Linux version**:
```sh
corepack enable pnpm # generate the pnpm shim in corepack's directory
which pnpm # must be /home/... or /usr/..., not /mnt/c/...
```
Or use nvm/apt to install a Linux version of Node and then `npm i -g pnpm`.
The current `install.sh` points this situation out at step 0, instead of waiting until pnpm reports an obscure panic.
---
### 3. `client.js` was changed, but the page does not change
First work out which half was changed — the two halves have completely different reload mechanisms:
| File changed | How it takes effect |
| --- | --- |
| `client.js` (browser half) | **Automatic**: `dsh-client-hmr` stat-polls the bundle file every ~500ms; about 1 second later the page updates by itself, with **no restart and no refresh** |
| `index.mjs` / `composition.mjs` / `meta.mjs` / `paths.mjs` (host half) | **`dsh web` must be restarted**: these are rows in the main process |
Prerequisites for HMR: `dsh-client-hmr` is enabled in the profile's composition (`dsh --profile web --dump-config | grep -A2 'id: hmr'`),
and **the page is still open** — if the SSE connection drops there will be no pushes, and in that case refreshing the page once is enough.
> Earlier documentation contained the sentence "changing client.js requires a restart", which is wrong (it contradicts the measurements in `ARCHITECTURE.md` §10).
> It has been corrected.
---
### 4. There is no "Custom mode" item in the settings panel
Check in order:
1. **Is the plugin in the composition tree**: `dsh --profile web --dump-config | grep custom-mode`.
2. **Was dsh restarted after installing**: the bundle's client-side content only enters the client graph during **startup assembly**.
3. **Are `dsh.client` and `exports["./client"]` both present** in the root `package.json`
(if either is missing, the browser half will not be discovered, and it **will not report an error**).
4. **Are the host half's service dependencies satisfied**: the host half has `inject = ["webServer", "agentPresets"]`,
and when either service is missing the plugin stays in `pending` (the startup log will say waiting for services).
5. **Does the browser Console contain an error starting with `dsh-custom-mode:`** — this page's failure policy is
"rather have the whole page not appear than ever crash the page", so it only ever speaks to the Console.
---
### 5. There is no "Custom mode" in the mode selector
A preset is a **file**, and the usual reason dsh cannot see it is that the file itself is broken:
- `$DSH_HOME/.agent-presets//preset.yml` has broken YAML → that mode **silently disappears** (no error).
If it was edited with an editor, confirm that `name:` is a single-line scalar with quotes. The settings page escapes it automatically when writing, but manual editing does not.
- The directory is missing `agent.cordis.yml`.
- The directory name cannot be `standard` / `ptc` / `minimal` / `cordis` — those are the names of the built-in modes.
- The mode is only read when a **new session** is created: an already open session will not switch modes midway.
---
### 6. Saving the prompt is rejected
The renderer performs **strict interpolation**: a complete `{{...}}` group must be a registered variable, otherwise rendering **throws**,
and the consequence is not "this section does not take effect" but **every model request in that mode fails**. So both write paths validate first:
| Text | Result |
| --- | --- |
| Plain text | Accepted |
| `{{model}}` / `{{cwd}}` / `{{provider}}` | Accepted |
| `{{}}`, `{{ model }}`, `{{Model}}`, `{{foo}}` | Rejected, with the reason given |
| A single unclosed `{{` | Accepted (the renderer treats it as a literal) |
To write literal braces, use a single `{` or an unclosed `{{`.
**What to do if it has already been written badly**: edit the file directly, bypassing validation:
```sh
$EDITOR ~/.dsh/.agent-presets/custom/prompt.md
```
Or temporarily clear the entire prompt down to a single line of plain text, then change it in the settings page.
---
### 7. Version mismatch
This plugin has **its own stable version line** (`x.y.z`; this guide covers `2.2.6`); *which* dsh releases it supports is declared in
`engines.dsh` + the peer range (`>=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0 || >=0.1.7-alpha.1 <0.2.0-0 || >=0.2.0-0 <0.3.0-0 || >=0.2.1-alpha.1 <0.3.0-0`). (Until `1.0.0` the version mirrored dsh's — that
was retired on 2026-09-18, because some directories only auto-install a plain `x.y.z`.) After upgrading dsh, if the settings page is blank or startup reports
"the current DSH version is missing a required API", then a coupling point has broken: check [ARCHITECTURE.md](ARCHITECTURE.md) and [CURRENT_COMPATIBILITY.md](CURRENT_COMPATIBILITY.md) against the exact host.
Step 0 of `install.sh` prints `dsh --version` together with the adaptation version declared by the plugin, and warns when they do not match.
---
### 8. A non-default preset directory name was used, and the settings page cannot read the prompt
The editor only recognizes `$DSH_HOME/.agent-presets/custom/prompt.md` by default. If it was installed with `--preset-id mine`,
the **dsh process** must carry the environment variable (`export` in your own shell does not count — dsh has to be started from an
environment that has this variable):
```sh
export DSH_CUSTOM_PROMPT_PATH="$DSH_HOME/.agent-presets/mine/prompt.md"
```
For a systemd service, write it into the unit's `Environment=` instead.
---
### 9. Security: the settings page's private route must pass the platform's authentication
#### Symptoms (measured on 0.1.6-alpha.1; this repository has already fixed it at the source level)
Before the fix, `/custom-mode` was **readable and writable without authorization**:
```sh
curl http://127.0.0.1:3080/custom-mode # 200,整份系统提示词被读走
curl -X POST http://127.0.0.1:3080/custom-mode \
-H 'content-type: text/plain' --data '{"mode":"standard","overrides":{},"prompt":"PWNED"}'
# 200,prompt.md 真的被改写
```
On the same machine, official routes all return 401. The cause: the platform's Host/Origin fence and browser authentication only apply to
channels mounted by `@deepseek-ai/dsh-client-connection` (`/`, `/api`…), and routes that a plugin registers directly on
`ctx.webServer` **do not pass through** it.
The `text/plain` detail is crucial: **ordinary form-style cross-site requests do not trigger a preflight**, so any web page the user visits can POST to
this port. The response cannot be read, but the write has already happened — for an agent holding shell and file tools,
this is remotely triggerable prompt injection.
#### Current behavior
The route asks the platform before handling any request:
```js
ctx.connection.fetch.register({ path, methods, requestBody, fetch })
```
The route is mounted on the platform's **fenced `/api` channel**, which applies the trust and authentication
policy (loopback/`trustedHosts` Host check, `Sec-Fetch-Site`, `Origin`, the signed browser-session cookie)
**before** dispatching — the check no longer depends on this plugin remembering to call it.
Measurements after the fix:
| Request | Result |
| --- | --- |
| Unauthorized GET | `401 unauthorized` |
| Unauthorized POST (`text/plain`) | `401`, file not rewritten |
| POST + `Origin: https://evil.example` + `Sec-Fetch-Site: cross-site` | `403 forbidden` |
| With a valid `dsh-auth-*` cookie (normal browser session) | `200`, functionality unchanged |
**Which version you have installed yourself**: check whether `index.mjs` registers on the raw
`webServer` table (`webServer.register(`). `1.0.3` and later register on the platform's `/api` channel
(`connection.fetch.register`) instead, where the carrier applies the fence before dispatch.
If it does not, either upgrade this repository, or for now do not expose this settings page on a port that others can reach
(by default it binds only to `127.0.0.1`; the risk is mainly on **multi-user machines** and **cross-site requests from inside a browser**).
When the `connection` service cannot be obtained (DSH version mismatch), **nothing is registered at all** —
there is no unfenced fallback route to fall back to, and the scoped `inject` simply never runs (the row stays
`active`, so no misleading `pending` warning is printed either).
---
### 9.5 The plugin's dependency is installed but the bundle entry is gone
Measured: when `node_modules` still holds the package but `profiles/web/package.json` no longer lists
`dsh-custom-mode` in `dsh.profile.bundles`, **`dsh plugin --profile web add dsh-custom-mode` short-circuits**
(pnpm reports "resolution skipped") and does **not** restore the bundle entry — the settings page stays absent
while the command looks successful. Self-rescue, in order:
1. `dsh plugin --profile web remove dsh-custom-mode` then add it again;
2. or edit `profiles/web/package.json` and put `"dsh-custom-mode"` back into `dsh.profile.bundles`;
3. `./install.sh` does exactly this bookkeeping for you (it snapshots the bundle list before installing and
asserts nothing was lost), which is why it is the fallback when the one-liner does not take.
### 9.6 `0.1.6-alpha.1` cannot boot at all — that is the host, not this plugin
Measured (and the reason the `alpha` dist-tag moved to `alpha.2`):
```text
SyntaxError: The requested module '@deepseek-ai/dsh-app-boot' does not provide an export named 'watchUserPatches'
```
The process exits during `profile-boot`, **before any plugin is loaded**. Nothing about this plugin is
involved; if you are on `0.1.6-alpha.1`, move to the preview line `0.1.7-rc.2` (or the stable line `0.1.5-rc.3`, which is
also supported — see the README's Versioning section).
### 10. Installing into a profile such as tui / headless: less works than expected
Both `./install.sh --help` and the two READMEs mention `--profile tui`, but **measurement** shows that one thing must be understood first:
```sh
dsh --profile web --dump-config | grep agent-presets # 有
dsh --profile tui --dump-config | grep agent-presets # 无
dsh --profile headless --dump-config | grep agent-presets # 无
```
The `agent-presets` service **only appears in the web composition**, and "Custom mode" is precisely what it mounts. So in tui it is
not that "the settings page cannot be seen", but that **this mode does not exist at all**: it cannot be selected when creating a new session. The settings page additionally requires
`webServer` (it is a web page), which tui also does not have.
Before the fix, after installing into tui every startup printed this line:
```
dsh: warning: 1 entry did not activate
custom-mode (dsh-custom-mode): pending (waiting for services: webServer, agentPresets)
```
It is **exactly the same** as the "dsh turns into a brick after installing" error in [§1](#1-critical-after-installing-dsh-no-longer-starts-and-only-a-single-pending-line-remains),
which makes it highly misleading — a normal installation into the wrong profile looks no different from catastrophic corruption.
The plugin now uses **scoped service waiting** (`ctx.inject`), so it no longer leaves the whole row stuck in `pending`, and `install.sh` also
states on the spot, right after installing, that no settings page is available in this profile.
**Conclusion**: to use the graphical settings page, install into the web profile. Installing into another profile will not break anything, but it cannot be used either.
---
### 11. The settings page says it cannot find the composition file
Installed from npm (`dsh plugin --profile web add dsh-custom-mode`) and the page reports
```
{"ok":false,"error":"找不到组成文件:/…/.agent-presets/custom/agent.cordis.yml"}
```
The plugin seeds the preset when it activates, so this means seeding did not run or could not write.
Check the startup log for a line beginning with `custom-mode:`:
- **Nothing in the log at all** — the plugin row never activated. See §4.
- `custom-mode: preset 播种未完成 —— 无法创建 … EACCES` — `$DSH_HOME` (or the preset directory) is not
writable by the user running dsh. Seeding reports this and lets dsh start anyway; the settings page
is the only thing that breaks. Fix the permissions, or run `./install.sh`, or write the files by hand:
```sh
ls "$DSH_HOME/.agent-presets/custom" # expect agent.cordis.yml, preset.yml, prompt.md,
# prompt-reader.mjs, prompt-tool.mjs
```
Seeding runs once per activation and only fills in what is missing — it never overwrites a `prompt.md`
you wrote or an `agent.cordis.yml` the settings page generated. So a missing file that keeps coming
back means something is deleting it between activations.
### 12. One-shot verification checklist (run through it after making changes)
```sh
# 1. The two test suites (63 of those items are the composition-file compiler)
DSH_SHIPPED_PRESETS_DIR="$(node -e '
const {createRequire}=require("module");
const r=createRequire(process.argv[1]);
console.log(require("path").join(require("path").dirname(r.resolve("@deepseek-ai/dsh-agent-presets/package.json")),"presets"));
' "$PWD/index.mjs")" node test/composition.test.mjs
node test/locales.test.mjs
# 2. The plugin row entered the composition tree
dsh --profile web --dump-config | grep custom-mode
# 3. The composition tree did not collapse (should be hundreds of lines, not 1 line)
dsh --profile web --dump-config | wc -l
# 4. The preset is seen by dsh (after restarting dsh, when creating a new session)
dsh --profile web --dump-config >/dev/null && ls "$HOME/.dsh/.agent-presets/custom"
# 5. The settings page can read status (replace with the one printed when dsh web starts)
curl -s "http://127.0.0.1:3080/custom-mode" \
-H 'cookie: dsh-token=' | head -c 300
```
> The cookie name and the way to obtain the token in item 5 may differ between dsh versions; when opening the settings page in a browser, look at the
> `/custom-mode` request in the Network panel — that is the least effort.
### 13. Several assistants: the list, creation and deletion
#### 13.1 A mode does not show up in the assistant list
The settings page manages **only presets this tool created**. The test: the directory carries
`prompt.md`, and it either carries `prompt-reader.mjs` or its `agent.cordis.yml` references
`'./prompt-reader.mjs'`.
So these deliberately do **not** appear:
- a mode copied from a shipped one (`standard`, …) with the official picker's "copy preset", whose
identity goes through `@deepseek-ai/dsh-persona`;
- any hand-authored preset.
The reason is that a save **regenerates the composition from a base mode** (that is how the per-row
switches work), and doing that to a hand-written composition would destroy it. To make one managed:
have its composition inject identity through `./prompt-reader.mjs` and provide a `prompt.md`, or create
an assistant on the page and paste the prompt into it.
#### 13.2 A deleted assistant comes back after a restart
Only old versions did that: `apply()` used to seed `custom` unconditionally on every activation. The
rule now is that `custom` is created only when there is no `.custom-mode.json` marker under the root
**and** no managed assistant exists at all; otherwise only the marker is written.
If it does happen, check whether `$DSH_HOME/.agent-presets/.custom-mode.json` exists; if not, create it
(any `{"seededAt":"…"}` body) and restart.
#### 13.3 Creating an assistant fails
The page shows the backend's reason verbatim. Three common ones:
- **the name is empty** — the backend refuses it (an empty name renders the mode as its bare directory
id everywhere);
- **the directory already exists** — there is a same-named directory under the user preset root that
discovery does not count as a preset (no `agent.cordis.yml`, say). Pick another name, or deal with
that directory first;
- **copying the mode template failed** — the packaged `preset/` is missing files, or the target
directory is not writable.
#### 13.4 An assistant was renamed, but `custom_prompt`'s description in its sessions still shows the old name
The tool description comes from the composition row's `config.modeName`, while the **module file**
(`prompt-tool.mjs`) lives in the user's directory and is not overwritten at activation (the user may
have edited it). An assistant installed by an older version has a module that does not know that key.
Running `./install.sh` refreshes the module files (it keeps `prompt.md`). This only affects the
description text; the tool's read and write behaviour was always correct.
### 14. The page opens, but its controls look plainer than the official settings pages
That is not a bug — the **degradation path** is active. This page's buttons, inputs, switches, tags,
confirmation dialog and icons come from the shell's shared atom library
`@deepseek-ai/dsh-client-ui-primitives`, which the **shell** has to register as a seed word (beside
`react`). When the shell does not provide it, the page falls back to built-in plain controls —
identical behaviour, simpler look.
To confirm, open the page and check the Console for:
```
dsh-custom-mode: 当前壳没有在种子表里提供 @deepseek-ai/dsh-client-ui-primitives,
改用内置的朴素控件(功能一致,外观更简)。
```
To restore the unified look, move dsh to a version that registers that package in its seed table
(search `dsh-client-ui-primitives` in `dsh-web-frontend/dist/assets/index-*.js`). This plugin needs no
extra declaration: `dsh.client.external` is for bundles that are themselves boot records, whereas this
package is a seed word that any bundle may `require` (see `docs/ARCHITECTURE.md` §14).
### 15. The page renders raw keys such as `assistant.heading` / `btn.create`
**This should no longer happen from 0.1.6-alpha.2 on.** It was caused by the `settings.section`
contract change (the `locale:` option was removed); the page now carries its own zh/en dictionaries as a
floor (see `docs/ARCHITECTURE.md` §15). If you still see keys, the editor package is out of date.
How to tell: open the page and read the Console.
```
dsh-custom-mode: 当前壳没有在种子表里提供 … ← a different matter (plain controls, see §14)
dsh-custom-mode: 词典注册被拒,改用内置词典:… ← the namespace is taken (usually a double apply under HMR)
dsh-custom-mode: 词典注册后宿主仍查不到 …,页面已改用内置词典。
```
In all three cases the page's copy is correct (the inlined dictionaries back it); the warnings only say
what happened on the host side. If the copy really is raw keys, first confirm `client.js` is
current (`grep -c assistant.heading client.js` should be > 0, and the same key must exist in
`locales.mjs`), then restart dsh and refresh the page.
### 16. The picker order did not change after reordering
The order lives in each assistant's `preset.yml` (`order: 1..N`). If it did not take effect, look at the
files first:
```
grep -n '^order:' "$DSH_HOME"/.agent-presets/*/preset.yml
```
- **none at all**: the move up/down call did not actually succeed — check the page's status line.
- **only some have it**: that is the writer preserving on-disk values; move any assistant up or down once
more and it fills in 1..N for **every** managed assistant.
- **the files are right but the picker did not change**: the picker reads the roster when a **new session**
starts; sessions already open are unaffected.
### 17. A freshly published version does not install — `dsh plugin add ` takes the previous one
You published (or read about) `0.1.6-alpha.2`, but the bare name installs the older version, and the new
behaviour is missing. With this plugin that is visible: a version that predates seeding leaves
`$DSH_HOME/.agent-presets/` empty, and the settings page then reports a missing composition file.
```
$ dsh plugin --profile web add dsh-custom-mode
+ dsh-custom-mode 0.1.6-alpha.1 ← not the version the registry calls `latest`
```
**Cause: pnpm ≥ 11 delays newly published versions by default, and the official pipeline is subject to it.**
Measured twice, with different results depending on how the version is resolved: a bare-name
`dsh plugin --profile web add dsh-custom-mode` **38 minutes after 1.3.0 was published installed 1.0.3**
(official pipeline, pnpm 12), while the same install in a different profile/cache resolved the new version
directly. Treat the cooldown as version- and config-dependent, and **verify what actually landed**
(`npm ls dsh-custom-mode` inside the profile, or `cat profiles/web/package.json`) instead of trusting the
command's exit code. An exact pin selects the intended version; compatibility and profile policy may still reject it. `minimumReleaseAge` defaults to
`1440` minutes (one day), so a version published less than a day ago is not eligible for a bare-name
resolution and pnpm falls back to the newest version that is. Measured: alpha.2 was 13 hours old and was
skipped; alpha.1, 24.3 hours old, was installed. The registry was correct the whole time — `latest` and
both packuments pointed at alpha.2, and `npm install dsh-custom-mode@latest` resolved to it.
Fix — choose the exact accepted version; see the current recovery steps. pnpm records that pin as an exception in the profile's
`pnpm-workspace.yaml`. Keep **one** exception for this package. pnpm 11.7.0 reads only the first
`minimumReleaseAgeExclude` entry that matches the package name ([pnpm#12463](https://github.com/pnpm/pnpm/issues/12463)),
so a growing list of `dsh-custom-mode@1.11.0`, `@1.12.2`, … does not exempt the later versions. The
install then fails with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` even though you pinned.
```yaml
# One line. A bare name exempts every version of this package.
minimumReleaseAgeExclude:
- dsh-custom-mode
```
Or one disjunction, still a single entry: `dsh-custom-mode@1.13.0 || 2.1.0`. Do not set
`minimumReleaseAge: 0`; that turns the cooldown off for every package in the profile. This plugin
does not edit that file.
A failed install can be half-finished: `node_modules` may already contain the new version while
`package.json` and `pnpm-lock.yaml` still name the old one. After you merge the exception to one
line, run the install again. The version on the settings page is the one that is actually running,
and only after you restart dsh.
```sh
dsh plugin --profile web add dsh-custom-mode@2.2.6
```
and check what actually landed instead of what you typed:
```sh
node -p "require('$DSH_HOME/profiles/web/package.json').dependencies"
```
---
### 18. The page says "Unexpected host error: 无法定位出厂基础模式", or the base mode and the switches are greyed out
#### Symptoms
The settings page opens and the assistant list is there, but:
- **before 1.9.13** — a red line reading `宿主内部错误:无法定位出厂基础模式…` where the mode name, the base-mode
pills, the plugin switches and the prompt editor should be; `GET /api/custom-mode/state?id=…` answers **500**
(`internalError`), and `POST /api/custom-mode/state` answers **400** (`renderFailed`);
- **from 1.9.13** — the same page shows a warning line and the base mode / switches are displayed as unavailable,
while the system prompt still loads and **still saves**.
Either way the mode itself is fine: it is in the new-session picker and a session started with it works.
#### Cause
The plugin composes a mode by editing the *shipped* composition of its base mode, and it could not find that
text. There are two ways a dsh line publishes it: the file-based `@deepseek-ai/dsh-agent-presets` package
(0.1.2 … 0.1.6) and the host's own declaration, `agentPresets.readDocument().content` (0.1.7+). 1.9.12
only knew the first one, so on a stock `0.1.7-rc.2` — the line the official desktop app bundles — every read
threw. See `docs/ARCHITECTURE.md` §18.
From 1.9.13 the failure is a typed `baseCompositionUnavailable` instead of a 500, and the page degrades: the
prompt saves on its own, and a save that carries row switches is refused with a message rather than silently
dropped.
#### What to do
1. **The historical minimum fix was 1.9.13; choose the current accepted release** — that is the whole fix:
`dsh plugin --profile web add dsh-custom-mode@2.2.6`, then restart dsh.
2. **Read the host log for the list of routes that were tried.** The plugin names every attempt:
`custom-mode: 取不到这些基础模式的出厂组成 —— standard: …`. If `agentPresets.readDocument() 不可用` appears,
the host is older than the declarative registry and you are on a line this plugin does not claim to support.
3. **Only if you must stay on 1.9.12**: point `DSH_SHIPPED_PRESETS_DIR` at any legacy presets directory that
contains `/agent.cordis.yml` (the old package or a copy of it). It is honoured first and, when set,
discovery is skipped — so a wrong path is not silently ignored, it fails loudly in the log.
4. **If you maintain this plugin**: this is what the suite could not see for three releases. The fixture was
injected by `test/run.mjs`, so the 0.1.7 path was never exercised; and CI could not even install the fixture
on 0.1.7 (`ERESOLVE`), so those legs failed before any test ran. Both are fixed in 1.9.13 — but the general
lesson stands: **a green suite with an injected fixture is not evidence about a real installation.**