DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins

dsh-acp-enhanced

Acp Enhanced

Enhanced ACP server for DeepSeek Harness: block-level streaming, usage/stat telemetry (cache hit rate, token speed, input/output tokens, context length, turns, tool timing), model & reasoning-effort switching, and permission-preset control over the ACP wi

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add dsh-acp-enhanced@0.11.2
READMECompatibilityVersions
Approval config contextTool cards elicitation

Compatibility and provenance

Acp Enhanced is published as dsh-acp-enhanced and currently resolves to version 0.11.2. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
npm
Registry updated
11/4/2026

Versions

0.11.2stable
11/4/2026
0.11.1stable
10/22/2026
0.11.0stable
10/22/2026
Show 17 more versionsCollapse versions
0.10.0stable
10/1/2026
0.9.1stable
9/25/2026
0.9.0stable
9/19/2026
0.7.0stable
9/4/2026
0.6.0stable
8/31/2026
0.5.2stable
8/29/2026
0.5.1stable
8/29/2026
0.5.0stable
8/29/2026
0.4.0stable
8/29/2026
0.3.6stable
8/28/2026
0.3.5stable
8/28/2026
0.3.2stable
8/21/2026
0.3.1stable
8/16/2026
0.3.0stable
8/16/2026
0.2.1stable
8/16/2026

Related plugins

Loading related plugins…

Latest
0.11.2
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
444.9 kB
Files
17
Surface
any
License
MIT
Source
npm
GitHub
★ 7
Weekly downloads
396
Security scan
✓ v0.11.2 scan passed
Last push
11/4/2026
View source ↗Project homepage ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
0.2.0stable
8/15/2026
0.1.1stable
8/15/2026

Related plugins

More verified plugins in integrations-communication.

Im@xmanrui/dsh-im把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.Free Searchdsh-free-searchFree web search for DeepSeek Harness: 27 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel/You.com keyless; Zhihu global/site via MCP and Perplexity/SerpBase/DeepSeek (official + native tools/search)/Baidu/Kimi/Aliyun/DoubaPocketdsh-pocketPut DeepSeek Harness in your pocket: one package, one settings page, and scan a QR code on your phone to access DSH on your computer in sync (LAN + public network, real-time screen mirroring).Dafeiyudsh-dafeiyuA desktop-native BigFish companion driven by DeepSeek Harness session events.

README

中文 | English · Changelog

dsh-acp-enhanced

An enhanced Agent Client Protocol (ACP) server for DeepSeek Harness (dsh), built for ACP editors like Zed. It is a drop-in replacement for the official @deepseek-ai/dsh-acp bridge: the official bridge only streams plain text, this one exposes the Web GUI's capabilities — streaming, telemetry, model/permission control, session management, MCP — over the ACP wire.

Features

Output & telemetry

  • Token-level streaming, on by default: the reply and the model's thinking reach the editor while it is still writing (agent_message_chunk / agent_thought_chunk, coalesced on a 75 ms timer). The one cost of sending text before its block commits is that a mid-block retry cannot take its abandoned partial back: a visible _[stream interrupted — retrying]_ marker separates it from the retried attempt. Set streamDeltas: false on the acp-enhanced row (DSH_ACP_STREAM_DELTAS=false) for the older block-granularity behaviour, where cancelled/retried attempts never leak torn output at all but nothing renders until the block commits
  • Full telemetry: context usage ring plus cache hit rate / TPS / input-output-reasoning tokens / tool timing / turn counts (usage_update._meta carries the full breakdown). The ring's denominator is the routed model's declared contextWindow, folded from the session log so a resumed thread keeps it (the harness logs request/context once and does not re-emit it for an unchanged route). dsh's own dsh-compaction-basic row auto-compacts at ~80% of that window and is mounted by the standard/ptc/cordis presets (never minimal); /compact forces it manually. A route whose adapter declares no contextWindow has no denominator, so the ring reports used === size.
  • Image support (multimodal): when the dsh composition mounts an attachment store (dsh-attachment-local, mounted by default in dsh-base), promptCapabilities.image is advertised and pasted/uploaded images are ingested into the harness's durable attachment store — a vision-capable model (e.g. deepseek-v4-flash-vision-exp) reads them natively, in wire order with surrounding text. Older stacks (no attachment store) automatically downgrade: image is not advertised and an image prompt is refused with a clear error.

Model & permissions

  • Model switching: live provider/model catalog dropdown (ACP grouped-select wire shape)
  • Reasoning effort: reasoning_effort dropdown — only when the routed model exposes selectable efforts; each model remembers the effort it last used (persisted per profile), so switching back restores it, and a first-time model falls back to its own default — or its first offered effort — instead of an empty "unknown" selection
  • Permission presets: read-only / workspace-write / full-access session modes
  • Approval: native allow-once / reject-once prompts per tool call
  • Agent presets: per-session model-facing composition (tools + prompt sections) from the dsh agent-preset roster. standard is the full coding agent (default), minimal (极简模式) is a bare persistent shell with no subagent/web/todo/plan tools — nothing from the host layer leaks into a minimal agent; ptc and cordis ship alongside. Your own presets are declared in the profile (see Your own presets) — dsh 0.1.7 no longer discovers ~/.dsh/.agent-presets. Choose via the agent_preset config option, the /preset command, or the DSH_ACP_PRESET env var (per-session default); switching is only allowed while the session is still blank (no turn has run), so history never straddles two tool sets. A configured preset this roster no longer has does not close the panel: a blank session composes under the roster default and says so on stderr.

Zed deep integration

  • Tool cards: one-line summary in the collapsed header — Read <path>, the model's own intent line for shell commands (description, Codex-style — the exact command stays one click away), Search: <pattern>, Fetch: <url>, etc. The card body follows the ACP best practice: file edits render as a real diff, bash/pwsh commands as a real terminal card (codex-acp wire shape: command line + output + exit pill inside a terminal panel — no more raw-JSON cards), other executors as a syntax-highlighted code block, and touched files as clickable locations that open the file — with rawInput / rawOutput kept one click away for transparency, plus per-kind icons and a proper in-progress → completed/failed status lifecycle
  • Zed files & terminal: zed_read_text_file / zed_write_text_file / zed_terminal put file edits into Zed's "edited files" area (diff + accept/reject) and commands into a real Zed terminal
  • Native form questions: ask_user_question → elicitation/create form, click an option — or type a custom answer when none of them fit: options render with their descriptions, each option-backed question gets a free-text "Custom answer" field, and a custom answer replaces the single selection / accompanies a multi-select (same semantics as dsh's native question card)
  • Plan panel: plan mode toggle → "planning" status bar in Zed

Sessions

  • Resume & archive: session/load restores past threads (full replay); session/list lists the thread archive (titled, sorted by last activity); session/close drops the in-memory record so a later session/load resumes from the persisted log; live title updates. session/delete is deliberately not advertised — the harness declares no public persistence delete (see Compatibility)
  • Multi-root workspaces: sessionCapabilities.additionalDirectories is advertised, so Zed no longer shows "this agent doesn't currently support multi-root workspaces" and instead passes every workspace root on session/new / session/load. All roots are described to the model in the system prompt and reported on session/list; the sandbox keeps the primary cwd as its single writable root (see Known limitations)

Commands

  • Slash commands: typing / reveals the command list (available_commands_update): /status shows the route and telemetry, /model lists or switches the model, /preset lists or switches the agent preset (listings render as monospace code blocks — readable at a glance), everything else (/compact /goal /permission /plan…) runs straight through the harness command registry — all executed without a model turn. Every user-invocable skill is advertised as a command too, so /ask-matt, /code-review, /tdd, … reach the bridge instead of being rejected by the editor, and the skill's instructions are injected into the message (the @deepseek-ai/dsh-tool-skill style of user invocation). Images pasted next to a slash line ride along as command attachments (e.g. reference screenshots for a /goal objective), the same way the Web composer submits them

MCP

  • MCP servers: session/new mcpServers mount any MCP server (stdio + streamable HTTP); tools join as mcp__<server>__<tool>; a failing server never takes the session down

Preview

After picking dsh-acp-enhanced in Zed's AI Agent panel:

View image View image

Quick start

Requires dsh ≥ 0.1.6-alpha.1 (npm install -g @deepseek-ai/dsh@0.2.1-rc.2, or any version in the peer range below); the bridge consumes one declared harness surface on every supported line — 0.1.6-alpha.1 through 0.2.1 — and never probes older generations at runtime.

This package follows the official dsh plugin conventions (it declares dsh.bundle), so installation matches any official bundle: one command — auto-initializes the profile, installs the package, appends the bundle layer; no profile YAML to write.

Install (2 steps)

Step 1 — install (from the npm registry; no source checkout needed):

dsh plugin --profile acp-enhanced add dsh-acp-enhanced

When hacking on the code, use link: to a local checkout instead (live edits): dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"

Step 2 — register in Zed (under agent_servers in ~/.config/zed/settings.json; Zed spawns agents with a minimal PATH, so use the shipped launcher scripts/dsh-acp-zed.sh, which locates node/dsh itself)

The launcher ships with the package. Its absolute path depends on how you installed in Step 1:

  • npm install (default): $HOME/.dsh/profiles/acp-enhanced/node_modules/dsh-acp-enhanced/scripts/dsh-acp-zed.sh — replace $HOME with your home directory (e.g. /Users/you); Zed does not expand ~ or env vars, so write the full literal path.
  • link: dev install: <your checkout>/scripts/dsh-acp-zed.sh.

Most common: DeepSeek official API (the default route)

{
  // ...your existing settings...
  "agent_servers": {
    "dsh-acp-enhanced": {
      "type": "custom",
      "command": "/bin/bash",
      "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
      "env": {
        "DSH_ACP_PROVIDER": "deepseek-official",  // the official provider id
        "DSH_ACP_MODEL": "deepseek-v4-flash",     // the official model id
        "DSH_ACP_PRESET": "standard",             // optional: agent preset id (minimal / standard / code / cordis / yours)
        "DSH_ACP_INCLUDE_ALL_PROVIDERS": "true"   // optional: list every registered provider in the model dropdown
      }
    }
  }
}

Both env vars match the shipped patch's defaults, so they can be omitted entirely — writing them out just makes the route explicit. DSH_ACP_PRESET defaults to standard on the roster side; set it when you want every new session to start in a specific mode. The API key does not have to live in Zed: store it in ~/.dsh/.credentials.yaml (DEEPSEEK_API_KEY) and the dsh credentials service resolves it; the launcher also falls back to a running dsh web process's key.

DSH_ACP_INCLUDE_ALL_PROVIDERS=true opts the model dropdown into every provider the llm service has registered, instead of only the routed one. It is off by default for a reason: a provider without a usable credential still appears, and picking it parks the session on a route that cannot dispatch — the switch fails as an invalid-params error naming the route and the session keeps the one it had. Set it when one profile routes across several providers and you want to pick per session.

Debugging a stalled turn (is it the model request or the tool?):

"env": {
  // ...existing vars...
  "ACP_LOG": "/Users/you/.dsh/dsh-acp-enhanced.trace.jsonl"  // append-only JSONL event trace
}

Each line is one session event with wall-clock time (ms epoch); a turn that appears to hang is attributable afterwards: a model request stall shows a long gap between step/start and the first assistant/chunk, while a tool-execution stall shows a long gap between tool/call and tool/result (the result line carries elapsedMs). prompt/settled lines cover the full user-message round trip (stopReason + elapsed).

Optional: pin the panel's default config options (all still changeable in the panel):

"dsh-acp-enhanced": {
  // ...the type/command/args/env above...
  "default_config_options": {
    "model": "deepseek-official/deepseek-v4-flash",
    "agent_preset": "standard",
    "plan_mode": false,
    "reasoning_effort": "high"
  },
  "favorite_config_option_values": {
    "model": ["deepseek-official/deepseek-v4-flash", "deepseek-official/deepseek-v4-pro"]
  }
}

Extended: route through an OpenAI-Responses gateway (e.g. a company model gateway)

Same install path; only the env values change to the provider/model the gateway exposes plus the key env var it requires:

"dsh-acp-enhanced": {
  "type": "custom",
  "command": "/bin/bash",
  "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
  "env": {
    "DSH_ACP_PROVIDER": "<gateway-provider-id>",  // provider id exposed by the gateway
    "DSH_ACP_MODEL": "<gateway-model-id>",         // model id exposed by the gateway
    "<KEY_ENV_NAME>": "<key>"                      // the key env var the gateway reads
  }
}

<KEY_ENV_NAME> can also be omitted and the key stored in ~/.dsh/.credentials.yaml instead.

Zed hot-reloads settings. Open the AI Agent panel (Cmd+Shift+A) → pick dsh-acp-enhanced in the agent selector → send your first message: replies stream in real time, the status bar shows context usage, the panel exposes Model / Permission preset / Plan mode options plus three modes, and the thread archive lists and resumes past sessions.

Verify locally (no Zed needed):

node <pkg>/scripts/acp-doctor.mjs              # bundles + versions, the peer range, and one real boot
node scripts/acp-client.mjs                    # dev checkout only: full ACP e2e, expect ALL CHECKS PASSED
DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs   # only for a custom route

Web search

The bridge ships no search provider and takes no position on which one you use: the model-facing web_search tool rides on the web seam's searchProvider, so mount any ctx.web provider into the profile — a package with dsh.bundle via dsh plugin --profile acp-enhanced add <package>, or a plain package via your user-layer insert rows (see below). Which provider exists in your dsh deployment is a profile concern, not a bridge one.

Note what that costs: a provider bundle sits on the boot path of every ACP thread, so if it fails to load, the whole profile dies and Zed shows an opaque hang. Prefer a preset composition when the plugin only adds model-facing tools (see Keep the profile minimal); a provider that must configure the host web row belongs in the host composition (the profile) — mount it deliberately, and re-run the doctor after changing it.

Managing the profile's plugins

dsh-acp-enhanced runs in its own profile — acp-enhanced, created at ~/.dsh/profiles/acp-enhanced/ — inside the same dsh home as dsh web. The profile is what isolates the composition, so plugin changes here never affect your web setup, while credentials, settings, sessions and presets stay shared.

The profile composes its plugin tree from three sources, each layer patching the ones before it:

  1. Bundle layers — dsh.profile.bundles in the profile's package.json: the template's @deepseek-ai/dsh-base first, then every installed package that declares dsh.bundle (like dsh-acp-enhanced), in array order.
  2. Your user layer — ~/.dsh/profiles/acp-enhanced/cordis.patch.yml: id-targeted row config overrides, disabled: true row disables, and insert lists (how a package without dsh.bundle — e.g. a hand-mounted custom provider — gets mounted).
  3. Per-run overlays — dsh --profile acp-enhanced --patch extra.yml.

Adjust the set with:

dsh plugin --profile acp-enhanced add <package>     # install; a dsh.bundle package auto-joins the layer stack
dsh plugin --profile acp-enhanced remove <package>  # uninstall; auto-leaves the stack
dsh plugin --profile acp-enhanced update [package]  # update one/all, then reconcile
dsh --profile acp-enhanced --dump-config             # inspect the composed tree (per-layer provenance)

dsh plugin is a thin pnpm forwarder (run inside the profile directory) that reconciles dsh.profile.bundles against the installed state after every run. Two consequences worth knowing:

  • Disabling a bundle by deleting it from bundles does not stick — the package is still an installed dependency, and the next dsh plugin run appends it right back. To disable a single row without uninstalling, target it in the user layer by its row id (not the package name — find ids in the --dump-config output):

    - id: example-row
      disabled: true
    
  • A package without dsh.bundle loads nothing by itself — it installs as a plain dependency (with a one-time warning) and needs your own insert entry in the user layer. To change an existing row's config, override it with - id: <row> + config: — patch entries replace the whole row config, they do not merge.

Changes take effect in the next process: Zed spawns a fresh dsh --profile acp-enhanced for every agent thread, so open a new agent thread (or restart Zed) after editing the profile.

Keep the profile minimal

The profile is a single failure domain. cordis-plugin-loader awaits every entry and rethrows the first rejection, so one unloadable row aborts the whole plugin tree: the process may even answer the ACP initialize handshake first and die right after, which a client reports as an opaque hang, not an error.

Keep dsh.profile.bundles at exactly the two rows that cannot mismatch their own boot:

"bundles": ["@deepseek-ai/dsh-base", "dsh-acp-enhanced"]

@deepseek-ai/dsh-base ships with the CLI, so its version always matches the CLI that boots it; every other bundle is a third party whose dependency closure can drift. Mount extra plugins where a failure costs one preset instead of the whole editor session:

  • Plugin adds only model-facing tools/commands → declare its row inside a preset composition. On the 0.1.7 line that is a @deepseek-ai/dsh-agent-preset row in the profile's user layer (see Your own presets); on the 0.1.6 line it is a directory under $DSH_HOME/.agent-presets/<id>/ (agent.cordis.yml for the composition, preset.yml for the picker label). Either way the ACP agent_preset dropdown lists it, and a preset whose composition fails to load is reported as broken and simply not offered, instead of killing the process.
  • Plugin must configure a host service (e.g. a search provider overriding the host web row's searchProvider) → it belongs in the host composition, i.e. the profile. That is a deliberate trade: accept the boot-path risk, and re-run the doctor after any change.

Check the result before trusting it:

node <pkg>/scripts/acp-doctor.mjs          # bundles + versions, the peer range, and one real boot
dsh --profile acp-enhanced --dump-config   # where each row comes from

Your own presets

From 0.1.7 the roster is a registry fed by declaration rows: it scans no user directory, so $DSH_HOME/.agent-presets/<id>/ stops being discovered and a profile whose default names one of those presets fails every session/new with Unknown agent preset: <id>. Author your preset where the registry reads:

# ~/.dsh/profiles/acp-enhanced/cordis.patch.yml
- insert:
    - id: preset-my-agent
      name: '@deepseek-ai/dsh-agent-preset'
      config:
        id: my-agent              # the id DSH_ACP_PRESET / the dropdown uses
        name: My agent            # optional; 0.1.7 only localizes shipped ids
        description: …            # optional
        order: 5                  # optional
        plugins:                  # the composition, in the shipped preset's shape
          - id: persona
            name: '@deepseek-ai/dsh-persona'

Two rules the shipped presets/*.patch.yml layers (and this bridge's inlined copy) follow, because a preset is data the loader mounts:

  • restate the whole composition. A preset row carries the complete config.plugins list — there is no "patch a child of an existing preset" — so a fork of standard has to be re-baselined onto the new line's row set (0.1.6 replaced workflow-worker-thread with the workflow-ptc runtime). Prefer the host plane when your only difference is configuration (model routes, approval, sandbox): fork a preset only for a different tool set.
  • set the default where the line reads it. agent-preset-registry.config.default on 0.1.7, agent-presets.config.default on ≤ 0.1.6 (or the Settings surface, which writes selectedDefault). A profile that names a preset it no longer declares still works for a blank session — it composes under the roster default and logs the substitution on stderr — and fails to resume a session that already ran it, since composing a different tool set onto an existing transcript is exactly what the blank-only rule forbids.

Compatibility

One bridge binary, one declared harness surface: dsh ≥ 0.1.6-alpha.1 (peer range ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.2 || ^0.2.1-rc.2). All four lines in that range are boot-verified — handshake, profile settle and a real session/new — on every CI run, plus a cross-generation link check and a scripts/support-claim-test.mjs guard that pins this paragraph's claim against npm's own semver. The bridge consumes only the harness's declared surface: services in docs/capability-seams.md, events in docs/event-producer-consumer.md, published package exports. scripts/api-surface-check.mjs fails on anything else (a blocking CI step). There is no feature sniffing and no generation matrix: 0.1.7 needed exactly two adaptations — a member probe (presets.resolveMountable, private through 0.1.6 and gone in 0.1.7, replaced there by resolve() plus the row's own broken verdict) and a generation-gated row in cordis.patch.yml for the repackaged agent-preset roster, which reads the booting installation's own manifest version (below); 0.2.0 needed none (every package the bridge imports changed only by version, and its one new export is additive), and 0.2.1 needs none either — it is the first line whose range alternative narrows nothing (see below). A host without either falls back to the older shape; neither path can fail a boot.

The one thing that does fail silently is the host's own peer gate: from 0.1.7 @deepseek-ai/dsh-app-boot checks every bundle's declared @deepseek-ai/dsh* peers against the running CLI version and, when they do not match, drops that bundle's whole patch layer with a single stderr line (dsh: skipping profile bundle "…"). A range that stops one line short is therefore not a warning but a missing bridge — which is why the range above is both declared and booted per line. The install path is stricter still: from 0.1.7 the plugin manager also resolves every row name in the bundle's patch files and runs the same gate on each package, rejecting the whole installation (incompatible-version) when one disagrees — even a row that the generation gate disables on that very line. That is why this bridge declares a peer only for the packages it actually links against, and lets the ≤ 0.1.6 roster row resolve from the CLI's own installation instead.

Support policy

BridgeSupported dsh linesWhat changed
0.11.2^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.2 || ^0.2.1-rc.2no range change; an embedded text resource block (the editor caret/selection some clients send unasked) is folded into the prompt instead of failing the turn, while the binary half is still refused — the #19 crash
0.11.1^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.2 || ^0.2.1-rc.2no range change; token-level streaming is the default (streamDeltas: false opts back out); includeAllProviders reachable from the shipped row; an unroutable model pick fails as invalid params naming the route
0.11.0^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.2 || ^0.2.1-rc.20.2.1 line named and booted; nothing widened; dsh-invariants dropped; persona {{cwd}} suffix removed (it killed every 0.2.1 turn)
0.10.0^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.20.2.0 support; floor raised to 0.1.6-alpha.1; the range/matrix/generation guard added
0.9.1^0.1.5-rc.2 || ^0.1.6-alpha.1 || ^0.1.7-alpha.10.1.7 roster repackaging absorbed in one patch release
0.9.0^0.1.5-rc.2 || ^0.1.6-alpha.1declared-surface-only rewrite; floor 0.1.5-rc.2; session/delete dropped
0.8.x^0.1.0-rc.6 … ^0.1.6-alpha.1 (unreleased)0.1.3+ live-stream seam; 0.1.5 persistence handle API
0.7.x and older≤ 0.1.2-rc.1runtime probing of both generations

The rules behind that table:

  • A new dsh API line gets a new bridge release, not a wider runtime probe. Probing is how 0.7.x absorbed 0.1.1 → 0.1.5, and it is why that support rotted silently. 0.1.7 is a repackaging rather than a new API line, so 0.9.1 absorbs it in one patch release — and the probe it needs is a single member check, not a generation matrix. 0.2.0 is not even that: every consumed package changed by version only, so the release is a declaration change plus the guard that keeps the declaration honest. 0.2.1 goes one step further: its alternative changes no resolution at all (^0.2.0-rc.2 already contains 0.2.1-rc.2), so the release exists to name and boot the line — an unstated line is one the guard cannot hold, and a line that is claimed but never booted is what "support rotted silently" means.
  • The floor moves only with a bridge minor, and never silently: the launcher warns before booting a CLI outside the range, and the doctor stops with RESULT FAIL — CLI too old (below the floor) or RESULT FAIL — CLI line not supported by this bridge (beyond the ceiling).
  • A line is dropped by publishing a bridge that says so; the previous line stays on the feat/dsh-0.1.3-plus-support branch for users who cannot move.
  • Watch the next line before it is released: the scheduled canary workflow installs the alpha dist-tag and runs the guard, the link check and a boot smoke, so a breaking change shows up as a red canary rather than as user breakage.

Per-release detail — what each release changed, what it broke, and what it cost to absorb — lives in CHANGELOG.md, newest first, each entry linking to its long-form migration record in docs/plans/. This page keeps only the compatibility surface: which lines the bridge claims today, and the rules the claim follows.

Upgrading from a published ≤ 0.7.0

The pre-0.1.3 API line ends at 0.7.0, and its bridge and CLI have to move together — in either order the half-upgraded pair is broken:

OrderWhat you get
CLI first, bridge left at 0.7.0The profile boots and initialize succeeds, but every session/new fails with an Internal error (tool-subagent: modelSelectionSettings requires … in the Host scope). Nothing can warn you: that bridge copy is already installed, and it also has no agent/assistant-stream seam and no assistant/message fallback, so replies would not render either
Bridge first, CLI left behindThe profile dies while loading (… subpath './model-selection-settings' is not defined by "exports"). The launcher warns on stderr before that, and scripts/acp-doctor.mjs stops with RESULT FAIL — CLI too old
Both togetherThe supported state

Checklist:

  1. npm install -g @deepseek-ai/dsh@0.2.1-rc.2 (or any version in the peer range above).
  2. dsh plugin --profile acp-enhanced add dsh-acp-enhanced@0.11.2. Upgrading the bridge is explicit: the profile's dependency is a caret on 0.x, so dsh plugin update will not move you to a new minor by itself.
  3. Booted the profile from a checkout, or set DSH_PATH before? The old launcher moved you to ~/.dsh-acp on its own; it does not any more. Set DSH_HOME=<that home> in Zed's agent_servers.env, or recreate the profile in your default home. The launcher says so if it finds a profile there.
  4. Followed the old README and seeded subagent-model-selection-settings in your profile's user layer? Delete the row: the bridge's patch ships it now, and a duplicate id aborts the boot. scripts/init-acp-home.sh retires it for you. The launcher warns, and the doctor names the id.
  5. Check that every third-party bundle in the profile has a release for the line you are moving to (see Upgrading to 0.1.7) — the profile is one failure domain.
  6. Restart Zed (or open a fresh agent thread); node <pkg>/scripts/acp-doctor.mjs verifies the whole path first, including opening a thread.

One CLI generation per home

$DSH_HOME/profiles/node_modules is a single dependency closure shared by every profile under that home, and dsh heals it to whichever CLI booted last. So:

This is legacy-line behaviour. On 0.1.6-alpha.2 and later that shared closure no longer exists at all (the harness resolves from the CLI's own install; the profile's node_modules holds only out-of-tree plugins), which is why the launcher's drift check is line-specific and silently no-ops where the path is gone.

  • Never run two CLI generations under one home at once. The second boot flips the closure under the first process, which then lazily resolves mismatched modules mid-flight. The launcher compares the closure's dsh-agent version with the CLI it is about to boot and warns on stderr when they differ — after such a boot, restart the other dsh processes under that home (dsh web, …).
  • The escape hatch is the CLI, not the home. Pin the launcher with DSH_PATH=<dsh> (or use the repo pin below): the launcher resolves which dsh, never which home.

Current resolutions are always visible:

node scripts/compat-check.mjs   # dev checkout: installs the 0.1.6-alpha.2, 0.1.7-rc.2, 0.2.0-rc.2 and 0.2.1-rc.2 sets and imports the bridge from each
node <pkg>/scripts/acp-doctor.mjs   # CLI + closure versions, bundles, and one real boot (shipped)

Dev checkout: repo-pinned CLI, shared home

When the launcher runs from a checkout (link: install), it resolves the dsh CLI in this order:

  1. $DSH_PATH — an explicit dsh binary, or a directory whose node_modules/.bin/dsh holds one
  2. the repo-pinned CLI — <repo>/node_modules/.bin/dsh (this package's @deepseek-ai/dsh devDependency, currently 0.1.7-rc.2)
  3. global fallback — dsh on PATH / npx cache / npm prefix (a fresh clone without pnpm install degrades to it)

Whichever wins boots profile acp-enhanced in the home the launcher was started with (${DSH_HOME:-$HOME/.dsh}). The home is never rewritten. If you want the bridge on its own dependency closure, build a separate home and point the launcher at it explicitly:

DSH_ACP_HOME=~/.dsh-acp scripts/init-acp-home.sh   # optional, idempotent: creates + seeds an isolated home
# then in Zed's agent_servers env:  "DSH_HOME": "/Users/you/.dsh-acp"

The isolated home is a deliberate opt-in, not a default: your profile must exist in whichever home the launcher boots, or it exits 127 with the exact dsh plugin … add link: command that creates it. init-acp-home.sh ports your old profile's user rows verbatim, copies credentials/settings, disables the DeepSeek plugin-package inventory reporter, and retires a legacy subagent-model-selection-settings row from the user layer — that host row now belongs to the bridge's bundle patch, and a second copy aborts the boot with duplicate loader entry id.

Sessions persist under $DSH_HOME/sessions/<slug>/<id>/session.jsonl.zstd in both homes; nothing is copied between them by default, because the default home's tree also holds every web-profile session. Pass --copy-sessions (or run the rsync the script prints) when migrating an existing setup.

Troubleshooting

Start with the doctor: it boots the profile exactly as Zed does and names the failure layer, the offending bundle and the fix.

node <pkg>/scripts/acp-doctor.mjs              # installed copy
node scripts/acp-doctor.mjs                    # from a checkout (npm run doctor)
node <pkg>/scripts/acp-doctor.mjs --profile <name> --home <dsh-home> --timeout 60000

It prints the CLI + version, the home, the profile, every bundle + version, the supported peer range and the closure version, then classifies the boot into one of four layers:

LayerSignature in dsh's stderrWhat it meansFix
manifest-gateskipping profile bundle "<name>": … peerDependencies {…}the CLI's peer gate rejected a bundle's declared @deepseek-ai/dsh* peers and dropped its whole patch layer before the loader ever saw it. When the name is dsh-acp-enhanced the bridge itself is gone and there is no ACP server at all; when it is a third-party bundle, that bundle's rows never mount — its capability is gone, the boot continues, and the doctor reports DEGRADEDThe doctor's FIX names the skipped bundle — upgrade the CLI inside that bundle's declared range, upgrade the bundle itself, or take it out of the profile. Upgrading this bridge cannot help when the skipped bundle is a third party's
link-timedoes not provide an export named …, SyntaxError: The requested module …the booting CLI's closure cannot satisfy an import this bridge performsnode <pkg>/scripts/acp-doctor.mjs — if it prints LAYER link-time, align the generation: restart every other dsh process under this home (the shared closure heals to whichever CLI booted last), or pin this launcher with DSH_PATH=<matching dsh>
mount-timefailed to apply loader entry …, … requires … in the Host scope, duplicate loader entry id: …the loader rejected one entry and rethrew, so the whole plugin tree is downnode <pkg>/scripts/acp-doctor.mjs prints SUBJECT <entry> (<module>) — install the missing module, disable that row (- id: <entry> + disabled: true in the user layer), or trim dsh.profile.bundles to @deepseek-ai/dsh-base + dsh-acp-enhanced. For a duplicate id, delete the row from your user layer (the bundle patch owns it)
run-time… is not a function after a successful handshakethe bridge reached a harness service this CLI generation does not providenpm install -g @deepseek-ai/dsh@<version in the supported range> (see Compatibility)

The launcher translates the same four signatures on stderr while Zed boots (stdout is the ACP wire — and the manifest-gate line is the only one that is not a stack: the boot continues without that bundle, so the translated line is all the log gets), so the agent log already carries the layer and the fix.

SymptomLocateFix
Zed hangs with no output; the thread never answersnode <pkg>/scripts/acp-doctor.mjsPrints BOOT FAILED + LAYER/SUBJECT/FIX; follow the FIX line. A profile that answers initialize and dies right after is reported as such
exec: dsh: not found (status 127)which dshUse the shipped dsh-acp-zed.sh launcher (it locates node/dsh itself), or install the CLI
no API key for provider route "xxx"ls -l $DSH_HOME/.credentials.yamlWrite ~/.dsh/.credentials.yaml, or set env.DEEPSEEK_API_KEY on the agent_servers entry
SyntaxError: … 'PresetMountError'the bridge version in the agent logYou are running a pre-0.9.0 bridge copy against a ≤ 0.1.6 host — update this package
modelSelectionSettings requires … in the Host scopedsh --profile acp-enhanced --dump-config | grep subagent-model-selectionThe host row the standard preset needs is missing — the bridge's bundle patch inserts it, so reinstall/upgrade the bridge (dsh plugin --profile acp-enhanced add dsh-acp-enhanced), and check that nothing in your user layer sets it disabled: true
duplicate loader entry id: <row>the doctor prints LAYER mount-time and the idTwo layers ship the same row. Delete it from the profile's user layer ($DSH_HOME/profiles/acp-enhanced/cordis.patch.yml) — these host rows belong to the bundle patch. scripts/init-acp-home.sh retires the legacy subagent-model-selection-settings copy for you
Old threads start empty after a host upgradels $DSH_HOME/sessionsThe sessions live under $DSH_HOME/sessions/<slug>/; copy the old home's history in (scripts/init-acp-home.sh --copy-sessions) and the new host resumes them
An old thread fails to open: Internal error … dsh-session-format-v0-to-v1 refuses this format v0 Session: … source summary requires notice form … (raw log: <path>)the raw log path in the error

Development

pnpm install                          # install dev dependencies (repo-pinned CLI and test scripts)
node scripts/compat-check.mjs         # link check across the supported lines (0.1.6-alpha.2 / 0.1.7-rc.2 / 0.2.0-rc.2 / 0.2.1-rc.2 scratch installs)
node scripts/support-claim-test.mjs   # the declared range, the CI matrix and the helper agree with npm semver (blocking CI step)
node scripts/docs-claim-test.mjs      # the READMEs' quoted surface, the changelogs' versions and the two languages' heading parity (blocking CI step)
node scripts/boot-classify-test.mjs   # a skipped bundle on a working boot is DEGRADED, not a failure; a crash outranks the skip (blocking CI step)
node scripts/api-surface-check.mjs    # public-surface guard: no undeclared harness API (blocking CI step)
node scripts/pack-check.mjs           # package integrity: entry points, modes, shipped-file references (blocking CI step)
node scripts/acp-client.mjs           # end-to-end smoke (needs an API key)
node scripts/acp-client-tools.mjs     # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
node scripts/acp-mcp-test.mjs         # MCP mount test (no model calls)
node scripts/acp-smoke-keyless.mjs    # keyless boot smoke (CI)
node scripts/acp-resume-test.mjs      # session resume test
node scripts/codec-image-test.mjs     # image-codec unit tests (no network, fake store)
node scripts/terminal-codec-test.mjs   # terminal-card codec unit tests (no network)
node scripts/stored-titles-test.mjs   # session/list title reader: bounded reads, no per-session stat(), revision-keyed cache (no network)
node scripts/session-facts-test.mjs   # session-log folds: runningPreset/isBlank, live and stored share one contract (no network)
node scripts/context-window-test.mjs  # request/context fold: the resumed ring keeps its denominator (no network)
node scripts/tool-result-test.mjs     # tool/result id + body extraction across the supported dsh shapes (no network)
node scripts/cancel-cause-test.mjs    # cancel-cause contract: every agent.cancel() passes an AgentCancelCause union member (no network)
node scripts/replay-order-test.mjs     # replay/fallback chunk order: reasoning precedes its reply (no network)
node scripts/acp-image-e2e.mjs        # image capability e2e (vision-model leg needs an API key)
node scripts/acp-message-fallback-test.mjs  # live seam + assistant/message fallback: a seam fired and the reply arrived exactly once
node scripts/acp-launcher-test.mjs     # launcher contract: home never rewritten, drift warning, boot-failure translation
node scripts/acp-doctor.mjs            # boot the profile once and name the failing layer/bundle
scripts/init-acp-home.sh              # optional: bootstrap an *isolated* home (the launcher never switches to it by itself)

DevDependency pins for the harness packages use the line's prerelease range (currently ^0.2.1-rc.2, which resolves to the exact closure the pinned @deepseek-ai/dsh CLI itself declares), so the repo's tree and a fresh CLI install resolve one coherent family — exact patch pins here mixed with the CLI's range-resolved closure produce a split closure (two versions of one name) that breaks profile boots with export-not-found errors. After changing those pins, regenerate the whole lockfile (rm -rf node_modules pnpm-lock.yaml && pnpm install): an incremental install both leaves stale store entries poisoning the profile heal AND retains stale lockfile peer resolutions — bumping 0.1.2-alpha.2 → 0.1.2-rc.1 in place left dsh-session-persistence@0.1.2-alpha.3 (old-generation peers) wired into the rc.1 packages' snapshots, which passes boot and session/new and only breaks the first turn with TypeError: Cannot read properties of undefined (reading 'length') from PersistenceCoordinator. pnpm-workspace.yaml approves the CLI closure's build scripts (node-pty prebuilds, koffi) — they are runtime requirements when the repo CLI boots the profile.

Known limitations

Audio attachments are not supported (audio capability is not advertised), text streams token-by-token by default (streamDeltas: false restores block buffering, see Features), one in-flight prompt per session. MCP supports stdio and streamable HTTP (legacy SSE / acp transports are not advertised). session/fork / session/resume are not implemented (capabilities undeclared, compliant clients will not call them). session/delete is not advertised either: the harness declares no public persistence delete, so persisted sessions are never removed by the bridge (see Compatibility).

Multi-root workspaces are advertised and all roots are visible to the model, but dsh's sandbox policy resolves one writable root per session (the primary cwd, i.e. session.header.cwd) and the local sandboxes bind exactly that root for writes. Reads work in every root; under workspace-write a write under an additional root is denied first and needs escalation/approval, while danger-full-access writes everywhere. True multi-root write enforcement belongs in dsh core (dsh-sandbox-policy / dsh-sandbox-local would need a root list instead of a single root).

Agent presets take over the model-facing rows: the shipped cordis.patch.yml disables the dsh-base rows a preset owns (tool-bash/fs/subagent/todo/web/… — exactly the official dsh-web-app/tui list minus hmr, kept version-agnostic across generations: a row a given generation does not ship is warned and skipped by the patch applier) and mounts the agent-presets roster (standard default; ptc/minimal/cordis ship with the dsh CLI; your own presets are declared as @deepseek-ai/dsh-agent-preset rows from 0.1.7 on — see Your own presets). The bundle's own patch applies automatically (package.json dsh.bundle.patch) — do not copy it into the profile's user-layer cordis.patch.yml, or the loader rejects the duplicate entry ids at boot. When upgrading a profile that already carries a customized user-layer patch, keep only your custom row configs there (e.g. includeAllProviders: true on the acp-enhanced row, restating provider/model/preset since patch entries replace whole rows, they do not merge). A session resumes under the preset its own log records (the last agent-preset/selected event, else the creation header); only a log that recorded none falls back to the roster's default.

License

MIT — see LICENSE.

The dsh CLI's frozen v0→v1 migration refusing data that an older third-party bundle wrote into the session — every dsh client hits it, not only ACP through this bridge, and the migration never rewrites the original file. The writers of that era injected form/summary messages or message-edit/version events; a later release of the writing bundle typically ships a repair script (writes a repaired copy; the original stays). Upgrade the writing bundle so new sessions stop carrying the shape
Cannot switch modelsACP_DEBUG=1 dsh --profile acp-enhanced, then try the switchThe carried reasoning_effort is unsupported on the target: the bridge remembers the last effort per model (per-profile JSON) and falls back to the model's default rather than failing the switch. Also check the route is real — phantom providers are filtered, only config.provider's models are advertised
Context usage missing/status in the threadA "phantom provider" route was picked; point the profile's provider at a real route
Turn settles with usage but no reply text (empty panel)ACP_DEBUG=1 and look for agent/assistant-stream frame=chunkFrom 0.9.0 the only live seam is the agent/assistant-stream frames event, with the committed assistant/message as the fallback whenever a step streamed nothing. Frames present but no text = a client-side render problem; no frames at all = the fallback path (upgrade the bridge if it is older)
Unknown agent preset: <id> after a dsh upgradels $DSH_HOME/.agent-presets and the profile's cordis.patch.ymlThat preset left the roster — from 0.1.7 $DSH_HOME/.agent-presets is no longer scanned. Declare it as a @deepseek-ai/dsh-agent-preset row (Your own presets); 0.9.1 opens blank sessions under the roster default meanwhile (with a stderr note), but resuming a thread that already ran it keeps failing until the row exists
A capability the profile used to have is silently gonenode <pkg>/scripts/acp-doctor.mjs — a degraded boot prints DEGRADED <n> inactive items with the entry namesAn entry that fails to import does not take the profile down, it just is not there. The bundle was built for another line: it imports an export this line removed (0.1.7 replaced SettingsProvider in dsh-settings with SettingsForms). Upgrade that bundle for this dsh line, or remove it
dsh: skipping profile bundle "<name>": … peerDependencies {…} — a whole bundle is gone after a dsh upgradethat stderr line, the agent's stderr, or acp-doctor.mjs (a skipped bundle reports LAYER manifest-gate when the boot is down for it — the self-skip — and DEGRADED when the boot still works; either way that bundle's capability is gone)The CLI's peer gate: from 0.1.7 dsh-app-boot checks each bundle's declared @deepseek-ai/dsh* peers against the running version and drops the bundle's entire patch layer when they do not match (the install path refuses it outright as incompatible-version). Only the bundle's publisher can widen the range — upgrade it (dsh plugin --profile acp-enhanced add dsh-acp-enhanced@0.11.2), or pin the CLI to a line that bundle declares. This bridge declares all four lines above; when the skipped bundle is a third party's, it is that bundle's range that stops short, and upgrading this bridge cannot help
Session sidebar empty / slow to fill, bridge ≤ 0.9.0 on dsh 0.1.7ls $DSH_HOME/sessions | wc -l and the agent's stderr for timeout: session/listThe pre-fix list decoded every stored log (see What 0.1.7 changes about a large session store); a few hundred sessions made it exceed the 30s wire timeout. Upgrade the bridge to ≥ 0.9.1 — it answers in seconds and streams the remaining titles as session_info_update
Plugin edits seem ignoredthe profile's cordis.patch.yml mtimeChanges apply to the next process: open a new agent thread (or restart Zed)
A thread is dead after a Stop: Internal error: prompt was not queued: Cannot read properties of null (reading 'kind'), and a config switch in the same thread fails with {"details":"Cannot read properties of null (reading 'kind')"}ACP_DEBUG=1 — that thread's last turn/end carries reason=null, and loading it again fails the same wayBridge ≤ 0.9.1 passed new Error(...) as the harness cancel cause, but AgentCancelCause is a closed union ({kind:'user'|'parent'|'disposed'|'hook'}). The harness's own turn close-out then threw on assertNever and persisted turn/end {reason: null}; a session projection that folds reason.kind unguarded throws on it, so every projection read in that session (prompt, config switch, session/load) threw afterwards. The thread itself cannot be recovered — start a new one — and upgrading the bridge removes the cause (guarding reason?.kind downstream only stops the throw; the unreadable event stays in the log)
Need detailed diagnostics—ACP_DEBUG=1 (stderr lifecycle trace) and ACP_LOG=/tmp/acp.jsonl (per-event JSONL with timings)