# Design: dsh client version gate (issue #46) > Status: **SHIPPED** (branch `feat/46-dsh-0.2`, 2026-10-13). User directive > (verbatim intent): dsh 本身迭代很快,我们不做兼容,只按最新的来;在合适的地方 > 对 Agent 客户端版本做限制,不在兼容范围内不连接,提示升级。 > Context: `design-phase-9-w7-native-sessions.md` § #46 addendum (the v4 > session-log switch this gate rides with), `design-phase-9-w7.1-dsh-event- > tap.md` § #46 addendum (tap v2). ## Problem dsh's integration surfaces (session-log format, streaming buses, ACP dialect) move with the client, and #46 chose NO back-compat: the daemon reads one ground-truth version line (0.2.0-rc.2). Without a gate, an out-of-range client meets the integration as silent misbehavior — an empty session rail, dropped history, no token stream, or garbage tool output — with nothing telling the user their client is simply too old. ## Decision **Refuse to CONNECT below a per-target floor, with an actionable upgrade hint.** The floor lives on the ACP adapter table (one place; targets that iterate slowly carry none and are unaffected): - `adapters.ts` — `AcpAdapterSpec.minVersion` (`deepseek: '0.2.0-rc.2'`). - `acpMinVersion(target, env)` — the floor to enforce, or null: no floor, or an `HN_ACP_COMMAND_` override owns the command line. An operator-pinned install is managed by hand and the daemon must not gate it (this also keeps test fixtures riding the override ungated — a fixture is not a dsh). **Check** (`acp/version-gate.ts`): `checkAcpVersion(target, env)` probes ` --version` through the W1 runtime probe (`probeRuntime`, 4s bound, `pathEnv` from the caller's env) and compares with shared's `compareVersions` (prerelease-aware: `0.2.0-rc.1` < `0.2.0-rc.2` ≤ `0.2.1-alpha.1` ≤ `0.2.0` non-prerelease). The first whitespace-separated token is parsed. **Absent or unparseable versions never block** — the spawn itself then fails with its own, more specific error; the gate only rules on versions it can actually read. Refusal message: `dsh is not supported (minimum ) — upgrade with: npm install -g @deepseek-ai/dsh@latest` (`HARNESS_PACKAGES` supplies the package name). **Wiring** (`chat.ts`): - Channel open (`chat:session.start`): the gate runs BEFORE any spawn — right after the adapter command resolves, before prewarm consumption. A refusal answers `chat:session.ready {error: message}` (the same channel the spawn failures use), so the web session page shows the upgrade hint as the open error. No adapter process is ever spawned, no ledger entry written. - Prewarm pool: `spawn` gates too — an out-of-range client is never warmed (the pool is a cache: a miss is silent; the channel open surfaces the message). - Injectable `ChatHandlersOptions.versionGate` for tests (the default is the real check). ## Non-goals - **Not** a semver range engine: one floor per target, compared with the existing `compareVersions`. A ceiling is deliberately absent — newer dsh is expected to work until proven otherwise; if a release breaks the integration, the floor bumps and the gate carries the upgrade hint. - **Not** applied to targets without a floor (claude-code/codex/opencode/ hermes/pi today). Adding one later is a table row + tests. - **Not** an inventory-side warning (yet): the gate speaks at the point of failure, where the user is. The harness job's own hints (`dshNodeWarning`) keep their separate role. ## Verification - `test/version-gate.test.ts` (7 cases): floor declaration + override skip; prerelease-aware refusals across the 0.1.x/0.2.0-rc.1 line and passes from 0.2.0-rc.2 up; decorated version strings; absent/unparseable pass-through; no probe for override/ungated targets. - `test/chat.test.ts` (#46 describe): an out-of-range client is refused at open AND never pre-warmed (injected gate + zero ledger entries), and the DEFAULT gate refuses a real (fake) old `dsh` on PATH end-to-end with the upgrade hint in the ready error. - Repo typecheck + all four package suites green; `pnpm format`.