Files
dsh-context/docs/compatibility.md
T
bowenliang123 f4db11776f fix(client): ride the fetch routes as browser-relative spellings
The client POSTed every plugin route as a root-absolute /api/dsh-context/*
path, which stripped a reverse proxy's /app/dsh prefix at the gateway —
every poll 404ed at it, the panel retried on an aggressive backoff, and
the deployment fell into the issue #159 reconnect loop. The six route
constants now live in one module as their browser-relative spellings:
fetch resolves them against the page's document base, the same idiom
dsh's own stream/socket clients ride, so a prefixed proxy keeps its
prefix and a direct deployment resolves to the exact same URLs before
and after. A host/client pairing spec pins each spelling to its route
registration, and docs/compatibility.md gains the §5.8 client-seam rule.

Fixes #159
2026-10-16 23:50:58 +08:00

13 KiB
Raw Blame History

Compatibility

The rules dsh-context must hold on every supported @deepseek-ai/dsh release, and the evidence for each. Supported releases are declared in package.json (dsh.compatibility.dshReleases); baselines and their per-line seam faces are pinned in tests/baselines.ts.

Grammar. Entries are numbered rules (the bold lead is the invariant), plus Limitation (an accepted gap), Check (how a claim is proven), and Procedure (how to run a compatibility task). Evidence rides in trailing parens — paths in this repo, or dsh: paths in the harness tree read at the baseline tag — or as a table directly beneath the rule it maps. A rationale appears only where code cannot show it.

Law. Record only what no other artifact carries; delete what code, tests, or git history already say. Verify against the dsh sources before acting — source wins over this page. Less is ALWAYS better.

Last verified: 2026-10-14 (dsh-context 0.70.0, dsh 0.2.0-rc.2 npm install and 0.2.1-alpha.3 tree).

1 · Supported set

What the plugin declares it works on.

  1. Support exactly 0.1.7-rc.2 and 0.2.0-rc.2 — both the V4 durable-log generation. The floor is 0.1.7-rc.2 because it is the first 0.1.7-line release with a complete npm dependency closure; anything older gets §2's fallback composition with an upgrade prompt, never a fold.

2 · Admission

Whether the plugin loads at all.

Harness-side

  1. dsh ≥ 0.1.7 enforces a plugin's @deepseek-ai/dsh* peer ranges at startup and install (semver, includePrerelease: true; exemption dsh plugin allow-version). This plugin's peers are all >=0.1.7-rc.2 — no exemption on any supported line. (dsh: packages/boot/app-boot/src/plugin-compatibility.ts, apps/cli/src/plugin.ts)

Plugin-side

  1. Enforce the floor again at runtime — a pre-0.1.7 harness has no gate and would still load the plugin. Below the floor: zeroed fallback units plus an upgrade modal. Version undetectable: FAIL OPEN into the real units — a probe misfire must never blank a working deployment, and the home mirror (last-booted profile) never outranks the running module tree. (src/host/version.ts, src/host/fallback.ts, src/shared/version.ts)
  2. Feature-detect .volatile() at schema build time, so the same bundle loads — preferences inert — on a below-floor harness whose schemastery predates it. (src/host/config.ts)

3 · Fold

Durable events → projection state, host-side.

Shapes

  1. One shape-driven path; never branch on a detected harness version. (src/host/fold.ts, src/host/logShapes.ts)
  2. Trust the migration guarantee; carry no legacy readers. Every read path the plugin rides (live open, persistence handles, sessionQuery.observeSession) migrates pre-V4 logs to V4; an unconvertible log is refused with SessionFormatUnsupportedError, never served raw. Pre-V4 spellings exist nowhere in this repo. (dsh: packages/session/session-format-v3-to-v4, packages/session/session-format-catalog, packages/session/session-persistence/src/errors.ts, packages/session-query/session-query/src/documents.ts)
  3. developer/message folds into injected context — request composition, live surface, archives; never human inputs; an empty-content event keeps its surface node at zero tokens where the harness's deriveEventMessage projects null. (src/host/fold.ts)
  4. Read tool-result errors from message.isError alone — the generation admits event-level data.error only alongside it; a hostile envelope-only row degrades to unflagged, never a throw. (src/host/fold.ts)

Totality

  1. Folds are TOTAL — unknown event types return state unchanged; a malformed event drops whole (no partial state); no undefined-valued property is ever materialized into persisted state. The projection registry drives apply with no error boundary: one throw stalls the unit's cells and push feed forever, and one undefined property fails EVERY projection-cache write for the session.
  • Limitation — image offload (deferred): on IMAGE_OFFLOAD_REQUIRED the harness offloads images via a message projection; the fold reads raw durable events, so an offloaded message keeps its pre-offload image count and price until compaction removes it. Revisit only if it shows in practice.

4 · Metering parity

Figures the plugin prints that the harness also prints.

  1. Mirror, don't import. Re-fit when a supported line moves an implementation; verify differentially against the harness's own projections (§8). Right-hand paths are dsh: tree paths.
Plugin site dsh source mirrored
src/shared/estimate.ts packages/llm/token-meter/src/estimate.ts — estimateSystemMessage and its per-block text arm
src/shared/fileTokens.ts packages/llm/llm/src/content.ts fileHandleText — a file block costs its request handle text, never its bytes (priced via packages/llm/token-meter/src/route-pricing.ts); price the pathless arm
TPS chip first-token anchor packages/llm/llm/src/assistant-stream.ts assistantStreamFirstTokenTime
src/client/headline.ts occupancyPercent packages/client/ui-conversation/src/client/context-occupancy.ts — internal to the kernel bundle, no package export, hence mirrored
src/client/reserve.ts packages/compaction/compaction-basic/src/config.ts — only the shipped defaults (thresholdRatio 0.8, headroomTokens 65536): preset realms make a deployment's policy unreadable; when contextWindow − reservedOutput − headroomTokens is non-positive the backend refuses (TargetPressureConfigError), so the bar paints no masks
  • Limitation — declared divergence: the timing card's wait/generation boundary opens at the first OBSERVABLE instant (the first token, or an earlier block-start marker the token packing could not stamp), not at the meter's first-token anchor — so marker-tiled decode buckets and the split never double-count one window.

5 · Client surface

What the browser half may rely on.

Seams

  1. Every capability rides an OPTIONAL seam — deferred ctx.inject (never a hard inject), the face re-proved before use, the registration guarded, the module declared in dsh.client.inject. No service → no capability: no pending fiber, no throw. Per-line faces are pinned in tests/baselines.ts and probed by §8's matrix.
  2. Mount at most once per page (issue #137) — the client-plugin runner force-assigns a fresh shadowing priority to every registration, so the slot registry's same-priority duplicate guard never fires across a double mount. Read the raw ledger; mount nothing when context-jump is already seated; the read fails open. (dsh: packages/client/ui-renderer/src/client/registry.ts SlotRegistry.entries; probe ledger)
  3. Session jump uses the sidebar's own verb, uiWorkspace.openSession(SessionTarget) — never the sessions service's open(id), removed by the 0.1.6 selection refactor (issue #90). (dsh: packages/client/ui-workspace/src/client/navigation.ts; probe sessionNav)
  4. The member card's open prefers the right Sidebar's subagent chat, addressed by the row's own direct parent — a face that is absent or leaves the address unclaimed keeps rule 3's whole-view jump. (dsh: packages/client/ui-subagent/src/client/sidebar-chat/index.tsx, packages/client/ui-sidebar-right/src/client/service.ts)

Boundary

  1. Re-prove everything at the boundary — collections re-proven, scalars zeroed, whole-value absence stays null (the loading screen); structural narrowing over blind casts; skip elements that fail the shape instead of throwing. (src/client/timelineSource.ts)
  2. Isolate per-item work over hostile objects; every async read resolves to data or a retryable failure note — never an eternal spinner. (src/client/timelineSource.ts)
  3. A pushed revision decrease resets the detail ledger — stale reads never merge into the new head. (src/client/timelineSource.ts)
  4. Fetch routes ride browser-relative spellings (issue #159) — the client never issues a root-absolute /api/... URL: fetch resolves the relative form against the page's document base, so a deployment behind a path-prefixed reverse proxy (page at /app/dsh/) keeps its prefix, where a root-absolute spelling 404s at the gateway. (src/client/apiRoutes.ts)
  • Limitation — complete prompt sections: a deployment registering a complete system-prompt section keeps ONLY that section (registry design); promptPrefix/promptSuffix are dropped there. (dsh: packages/core/system-prompt)

6 · Projection cache

How fold-state changes reach existing installations.

  1. Bump a unit's stateVersion iff later events cannot backfill the change — running totals, per-step collections flushed at step/end, materialized envelopes. Serve-time-derivable fields need no bump, and new wire fields are additive-optional: a pre-field row serves without them and the client degrades per §5's boundary rules. Current versions sit at each unit's stateVersion site; past bumps live in git history.
  2. Stale rows refold from the durable log; the warm-up probes BOTH contextActivity and contextTimeline and cold-refolds missing or stale rows on first page open, so idle sessions rebuild exactly. (src/host/backfill.ts)
  3. Every cold read passes the host-wide gate (issue #121) — FIFO, one at a time, only while the V8 heap sits below two-thirds of its ceiling; a skipped read takes the caller's designed no-data path, never a failure. (src/host/coldRead.ts)
  4. The warm-up folds sessions over 2,000 events in in-memory slices (issue #145) through sessionProjections.restore/hydrate, sessions.prepare, and sessionProjectionCache.write, with one write-back at the log end and byte-identical rows to a single-shot coldSnapshot; any face absent → the session stays on the single-shot path (the faces exist on every supported line, §8's matrix probes them). (src/host/backfill.ts)
  • Procedure — force one session's refold: rm ~/.dsh/storages/session_projcache/sessions/<session-id>.json — a derived cache, rebuilt from the durable log.

7 · Packaging

Dependency declarations that look dead and are not.

  1. Declare @deepseek-ai/dsh-client-ui-primitives' bare imports as this plugin's own build/test requirements — its lib/index.js imports shiki, @shikijs/langs/*, katex, the mdast-util-*/micromark-* chain, anser, clsx, diff, and simple-icons while listing them as its own devDependencies. The client bundle never carries them (deps.onlyBundle + the dsh-client-bundle-purity plugin, tsdown.config.ts). If a primitives release starts inlining them, drop the matching entries in the same change.
  2. Declare @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-util-code-language, and the store's own zustand + immer — same reason, reached only through the primitives dist.
  3. Never add tailwindcss as a direct dependency — @tailwindcss/node pins it at the same version; a second resolving copy breaks the napi native binding (src/client/styles/tailwind.css).

8 · Verification

How every claim above is proven.

  • Check — seam matrix (the compat vitest project, every pnpm test in CI): stages the harness's REAL sources per baseline tag, boots the BUILT plugin into the tag's actual registries on its vendored cordis, probes the host and client seams, asserts retired seams ABSENT (settings.register, settings get(ns), sessions open(id)), and asserts every event family src/host/fold.ts switches on is declared by every baseline. Requires a dsh checkout (DSH_REPO, default ~/dev/deepseek-harness) and pnpm run build first; CI fails on a missing tag, a dev box skips with a printed reason.
  • Check — differential statistics: figures verified equal to the harness's own projections (sessionStats, contextBreakdown, contextPressure, tokenUsage) over real session logs — tokens, request counts, turns/steps, TTFT, generation, tool time, billed cost buckets.
  • Check — install (manual, per release): install the exact CLI into a disposable DSH_HOME (never the real ~/.dsh); dsh plugin add dsh-context → --dump-config shows exactly 1 composed row → dsh plugin remove → 0 rows. Pin an exact plugin version for a just-published build — the profile's package-manager release-age policy can lag.
  • Check — hostile fixtures: every guard branch carries them (wrong types, missing fields, throwing accessors) — an untested guard is an unverified promise.
  • Procedure — add a baseline: one Baseline entry in tests/baselines.ts (tag, vendored cordis, faces) plus the tag in both workflows' fetch step; run pnpm test and re-fit whichever seam a failing probe names.
  • Limitation — scope: these checks prove source-level seam compatibility, statistical parity, and install/uninstall per release — not full web-app runtime acceptance on a real profile.