From 5d7c5d0983b2c22b8d764d56a5c7ecfafa4f1afe Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:07:49 +0800 Subject: [PATCH 01/11] ci: add isolated DSH candidate source-graph qualification --- scripts/qa-dsh-candidate-source.sh | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 scripts/qa-dsh-candidate-source.sh diff --git a/scripts/qa-dsh-candidate-source.sh b/scripts/qa-dsh-candidate-source.sh new file mode 100644 index 0000000..c2e0df8 --- /dev/null +++ b/scripts/qa-dsh-candidate-source.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +stage() { + printf '\n==> qa:dsh-candidate-source: %s\n' "$1" +} + +CANDIDATE_VERSION="$(node - <<'NODE' +const fs = require('node:fs') +const manifest = JSON.parse(fs.readFileSync('compatibility-candidates.json', 'utf8')) +const candidate = manifest.candidates.find((item) => item.runtime === '@deepseek-ai/dsh' && item.state !== 'ready') +if (!candidate?.version) throw new Error('no active @deepseek-ai/dsh candidate') +process.stdout.write(candidate.version) +NODE +)" +TESTED_VERSION="$(node -e "const c=require('./compatibility.json'); process.stdout.write(c.tested.dshPackages)")" + +if [[ "$CANDIDATE_VERSION" == "$TESTED_VERSION" ]]; then + echo "candidate must differ from tested baseline ($TESTED_VERSION)" >&2 + exit 1 +fi + +stage "prepare transient package graph for DSH $CANDIDATE_VERSION" +node - "$CANDIDATE_VERSION" <<'NODE' +const fs = require('node:fs') +const candidate = process.argv[2] + +function read(path) { return JSON.parse(fs.readFileSync(path, 'utf8')) } +function write(path, value) { fs.writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) } +function pinDshDependencies(manifest) { + for (const bucket of ['dependencies', 'devDependencies', 'peerDependencies']) { + const deps = manifest[bucket] + if (!deps) continue + for (const name of Object.keys(deps)) { + if (name.startsWith('@deepseek-ai/dsh-')) deps[name] = candidate + } + } +} + +const dshPath = 'packages/dsh/package.json' +const dsh = read(dshPath) +pinDshDependencies(dsh) +for (const obsolete of [ + '@deepseek-ai/dsh-client-runtime', + '@deepseek-ai/dsh-client-ui-conversation', + '@deepseek-ai/dsh-client-ui-slots', +]) delete dsh.dependencies?.[obsolete] +dsh.dependencies['@deepseek-ai/dsh-api-session-controller'] = candidate +dsh.dependencies['@deepseek-ai/dsh-client-ui-renderer'] = candidate +dsh.dsh.client.inject = [ + '@deepseek-ai/dsh-api-session-controller', + '@deepseek-ai/dsh-client-ui-renderer', +] +write(dshPath, dsh) + +const cliPath = 'packages/cli/package.json' +const cli = read(cliPath) +pinDshDependencies(cli) +// DSH 0.1.5's pi-ai adapter and upstream workspace are qualified on 0.85.1; +// DSHelm CLI directly consumes providers/all for OAuth discovery, so the +// transient graph aligns that direct dependency instead of testing a mixed +// 0.82.1/0.85.1 catalog generation. +cli.dependencies['@earendil-works/pi-ai'] = '0.85.1' +write(cliPath, cli) +NODE + +stage "resolve transient candidate graph" +# The committed lockfile and package manifests remain the verified rc.7 graph. +# This install mutates only the disposable Actions checkout. Lifecycle/native +# scripts are excluded: this lane proves package availability + source/type +# compatibility; clean runtime installation is a separate evidence gate. +pnpm install --no-frozen-lockfile --ignore-scripts + +stage "assert candidate graph is coherent" +node - "$CANDIDATE_VERSION" <<'NODE' +const fs = require('node:fs') +const candidate = process.argv[2] +for (const path of ['packages/dsh/package.json', 'packages/cli/package.json']) { + const manifest = JSON.parse(fs.readFileSync(path, 'utf8')) + for (const bucket of ['dependencies', 'devDependencies']) { + for (const [name, version] of Object.entries(manifest[bucket] ?? {})) { + if (name.startsWith('@deepseek-ai/dsh-') && version !== candidate) { + throw new Error(`${path}: ${name}=${version}, expected ${candidate}`) + } + } + } +} +const dsh = JSON.parse(fs.readFileSync('packages/dsh/package.json', 'utf8')) +if (dsh.dependencies['@deepseek-ai/dsh-client-runtime']) throw new Error('legacy dsh-client-runtime survived candidate transform') +const expectedInject = ['@deepseek-ai/dsh-api-session-controller', '@deepseek-ai/dsh-client-ui-renderer'] +if (JSON.stringify(dsh.dsh.client.inject) !== JSON.stringify(expectedInject)) { + throw new Error(`unexpected current client inject graph: ${JSON.stringify(dsh.dsh.client.inject)}`) +} +NODE + +stage "typecheck candidate workspace" +pnpm typecheck +stage "build candidate workspace" +pnpm build +stage "run compatibility-sensitive contracts" +pnpm vitest run \ + packages/dsh/tests/dsh-request-contract.test.ts \ + packages/dsh/tests/host-composition.test.ts \ + packages/dsh/tests/vertical-slice.test.ts \ + packages/dsh/tests/keyless-vertical-slice.test.ts \ + packages/dsh/tests/session-log-compat.test.ts \ + packages/dsh/tests/subagent-015-compat.test.ts \ + packages/dsh/tests/client-native-panel.test.ts \ + packages/model-knowledge/tests/knowledge.test.ts \ + packages/cli/tests/user-commands.test.ts +stage "verify built client artifact" +node scripts/verify-client-bundle.js + +echo "qa:dsh-candidate-source OK (candidate $CANDIDATE_VERSION; tested baseline remains $TESTED_VERSION)" From 2f217d56d0cb1a8ce24ae1c6a6b6bd2431c955bc Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:08:27 +0800 Subject: [PATCH 02/11] ci: qualify DSH candidate source graph without baseline promotion --- .github/workflows/dsh-candidate-source.yml | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/dsh-candidate-source.yml diff --git a/.github/workflows/dsh-candidate-source.yml b/.github/workflows/dsh-candidate-source.yml new file mode 100644 index 0000000..807e315 --- /dev/null +++ b/.github/workflows/dsh-candidate-source.yml @@ -0,0 +1,44 @@ +name: dsh-candidate-source + +on: + pull_request: + paths: + - compatibility-candidates.json + - compatibility.json + - packages/dsh/** + - packages/cli/** + - packages/model-knowledge/** + - scripts/qa-dsh-candidate-source.sh + - .github/workflows/dsh-candidate-source.yml + push: + branches: + - main + paths: + - compatibility-candidates.json + - compatibility.json + - packages/dsh/** + - packages/cli/** + - packages/model-knowledge/** + - scripts/qa-dsh-candidate-source.sh + - .github/workflows/dsh-candidate-source.yml + workflow_dispatch: + +concurrency: + group: dsh-candidate-source-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +jobs: + candidate-source-graph: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6.1.0 + with: + version: 11.7.0 + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: pnpm + - name: DSH candidate source-graph qualification + run: bash scripts/qa-dsh-candidate-source.sh From fc5288cc16d95453784d5017fe41f2e9f85c2571 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:16:06 +0800 Subject: [PATCH 03/11] ci: qualify candidate inside exact DSH host cohort --- scripts/qa-dsh-candidate-source.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/qa-dsh-candidate-source.sh b/scripts/qa-dsh-candidate-source.sh index c2e0df8..634c378 100644 --- a/scripts/qa-dsh-candidate-source.sh +++ b/scripts/qa-dsh-candidate-source.sh @@ -40,6 +40,18 @@ function pinDshDependencies(manifest) { } } +// Qualify the plugin inside the exact published DSH host cohort. Current DSH +// prerelease packages publish workspace peers as stable-looking ranges such as +// `>=0.1.5 <0.2.0-0`; those ranges do not themselves select 0.1.5-rc.1 from +// the `next` dist-tag. A real DSH installation provides those peers through +// the host package graph, so the transient workspace must model that host +// rather than asking pnpm to invent a future stable peer. +const rootPath = 'package.json' +const root = read(rootPath) +root.devDependencies ??= {} +root.devDependencies['@deepseek-ai/dsh'] = candidate +write(rootPath, root) + const dshPath = 'packages/dsh/package.json' const dsh = read(dshPath) pinDshDependencies(dsh) @@ -78,6 +90,10 @@ stage "assert candidate graph is coherent" node - "$CANDIDATE_VERSION" <<'NODE' const fs = require('node:fs') const candidate = process.argv[2] +const root = JSON.parse(fs.readFileSync('package.json', 'utf8')) +if (root.devDependencies?.['@deepseek-ai/dsh'] !== candidate) { + throw new Error(`root host=${root.devDependencies?.['@deepseek-ai/dsh']}, expected ${candidate}`) +} for (const path of ['packages/dsh/package.json', 'packages/cli/package.json']) { const manifest = JSON.parse(fs.readFileSync(path, 'utf8')) for (const bucket of ['dependencies', 'devDependencies']) { From c5013098631742fc71ed9a7ce57a64990d4c32e6 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:22:17 +0800 Subject: [PATCH 04/11] ci: seed exact candidate release-age exclusions --- scripts/qa-dsh-candidate-source.sh | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/qa-dsh-candidate-source.sh b/scripts/qa-dsh-candidate-source.sh index 634c378..8c55158 100644 --- a/scripts/qa-dsh-candidate-source.sh +++ b/scripts/qa-dsh-candidate-source.sh @@ -79,6 +79,41 @@ cli.dependencies['@earendil-works/pi-ai'] = '0.85.1' write(cliPath, cli) NODE +stage "seed exact candidate release-age exclusions" +# The repository intentionally reviews fresh dependencies before allowing them +# into the verified graph. This candidate lane has a different purpose: inspect +# the exact, explicitly selected same-day DSH prerelease. pnpm can auto-add new +# package names to minimumReleaseAgeExclude during a non-frozen install, but +# names already represented by an older verified-version entry are not always +# duplicated for the candidate. Add only those exact candidate counterparts in +# this disposable checkout. Never disable minimum-release-age globally and +# never use a wildcard that would approve future DSH versions. +node - "$TESTED_VERSION" "$CANDIDATE_VERSION" <<'NODE' +const fs = require('node:fs') +const [tested, candidate] = process.argv.slice(2) +const path = 'pnpm-workspace.yaml' +const input = fs.readFileSync(path, 'utf8') +const lines = input.split('\n') +const additions = [] +const seen = new Set(lines.map((line) => line.trim())) +const escaped = tested.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +const pattern = new RegExp(`^(\\s*-\\s*['\"]?)(@deepseek-ai/dsh-[^@'\"]+)@${escaped}(['\"]?\\s*)$`) +for (const line of lines) { + const match = line.match(pattern) + if (!match) continue + const candidateLine = `${match[1]}${match[2]}@${candidate}${match[3]}` + if (!seen.has(candidateLine.trim())) { + additions.push(candidateLine) + seen.add(candidateLine.trim()) + } +} +if (additions.length === 0) { + throw new Error(`no verified DSH release-age exclusions found for ${tested}`) +} +fs.writeFileSync(path, `${lines.join('\n').replace(/\n+$/, '')}\n${additions.join('\n')}\n`) +console.log(`seeded ${additions.length} exact ${candidate} release-age exclusions`) +NODE + stage "resolve transient candidate graph" # The committed lockfile and package manifests remain the verified rc.7 graph. # This install mutates only the disposable Actions checkout. Lifecycle/native From 5af1db11b1022a6de78f1d39092c036f80c4ff52 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:22:53 +0800 Subject: [PATCH 05/11] ci: keep candidate exclusions in policy section --- scripts/qa-dsh-candidate-source.sh | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/qa-dsh-candidate-source.sh b/scripts/qa-dsh-candidate-source.sh index 8c55158..a105d8b 100644 --- a/scripts/qa-dsh-candidate-source.sh +++ b/scripts/qa-dsh-candidate-source.sh @@ -94,23 +94,26 @@ const [tested, candidate] = process.argv.slice(2) const path = 'pnpm-workspace.yaml' const input = fs.readFileSync(path, 'utf8') const lines = input.split('\n') -const additions = [] -const seen = new Set(lines.map((line) => line.trim())) const escaped = tested.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const pattern = new RegExp(`^(\\s*-\\s*['\"]?)(@deepseek-ai/dsh-[^@'\"]+)@${escaped}(['\"]?\\s*)$`) +const candidates = [] for (const line of lines) { const match = line.match(pattern) - if (!match) continue - const candidateLine = `${match[1]}${match[2]}@${candidate}${match[3]}` - if (!seen.has(candidateLine.trim())) { - additions.push(candidateLine) - seen.add(candidateLine.trim()) - } -} -if (additions.length === 0) { - throw new Error(`no verified DSH release-age exclusions found for ${tested}`) + if (match) candidates.push(`${match[1]}${match[2]}@${candidate}${match[3]}`) } -fs.writeFileSync(path, `${lines.join('\n').replace(/\n+$/, '')}\n${additions.join('\n')}\n`) +if (candidates.length === 0) throw new Error(`no verified DSH release-age exclusions found for ${tested}`) + +// Insert the candidate entries at the end of minimumReleaseAgeExclude instead +// of appending them at EOF, where YAML would interpret them as belonging to a +// later top-level key if one is added in the future. +const keyIndex = lines.findIndex((line) => line.trim() === 'minimumReleaseAgeExclude:') +if (keyIndex < 0) throw new Error('minimumReleaseAgeExclude section missing') +let end = keyIndex + 1 +while (end < lines.length && (lines[end].trim() === '' || /^\s+-\s/.test(lines[end]))) end += 1 +const existing = new Set(lines.slice(keyIndex + 1, end).map((line) => line.trim())) +const additions = candidates.filter((line) => !existing.has(line.trim())) +lines.splice(end, 0, ...additions) +fs.writeFileSync(path, lines.join('\n')) console.log(`seeded ${additions.length} exact ${candidate} release-age exclusions`) NODE From 3d25ad11b692188a44eb1735dafbff28e92d9ad2 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:25:09 +0800 Subject: [PATCH 06/11] ci: express candidate age exemptions as exact disjunctions --- scripts/qa-dsh-candidate-source.sh | 39 +++++++++++------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/scripts/qa-dsh-candidate-source.sh b/scripts/qa-dsh-candidate-source.sh index a105d8b..8e7db00 100644 --- a/scripts/qa-dsh-candidate-source.sh +++ b/scripts/qa-dsh-candidate-source.sh @@ -82,39 +82,28 @@ NODE stage "seed exact candidate release-age exclusions" # The repository intentionally reviews fresh dependencies before allowing them # into the verified graph. This candidate lane has a different purpose: inspect -# the exact, explicitly selected same-day DSH prerelease. pnpm can auto-add new -# package names to minimumReleaseAgeExclude during a non-frozen install, but -# names already represented by an older verified-version entry are not always -# duplicated for the candidate. Add only those exact candidate counterparts in -# this disposable checkout. Never disable minimum-release-age globally and -# never use a wildcard that would approve future DSH versions. +# the exact, explicitly selected same-day DSH prerelease. For package names that +# already carry a verified-version exclusion, pnpm's version-policy parser must +# see the two allowed versions as one disjunction; duplicate exact entries for +# the same package are not a stable multi-version representation. Keep both +# versions exact so this never becomes a wildcard approval for future releases. node - "$TESTED_VERSION" "$CANDIDATE_VERSION" <<'NODE' const fs = require('node:fs') const [tested, candidate] = process.argv.slice(2) const path = 'pnpm-workspace.yaml' -const input = fs.readFileSync(path, 'utf8') -const lines = input.split('\n') +const lines = fs.readFileSync(path, 'utf8').split('\n') const escaped = tested.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const pattern = new RegExp(`^(\\s*-\\s*['\"]?)(@deepseek-ai/dsh-[^@'\"]+)@${escaped}(['\"]?\\s*)$`) -const candidates = [] -for (const line of lines) { - const match = line.match(pattern) - if (match) candidates.push(`${match[1]}${match[2]}@${candidate}${match[3]}`) +let rewritten = 0 +for (let i = 0; i < lines.length; i += 1) { + const match = lines[i].match(pattern) + if (!match) continue + lines[i] = `${match[1]}${match[2]}@${tested} || ${candidate}${match[3]}` + rewritten += 1 } -if (candidates.length === 0) throw new Error(`no verified DSH release-age exclusions found for ${tested}`) - -// Insert the candidate entries at the end of minimumReleaseAgeExclude instead -// of appending them at EOF, where YAML would interpret them as belonging to a -// later top-level key if one is added in the future. -const keyIndex = lines.findIndex((line) => line.trim() === 'minimumReleaseAgeExclude:') -if (keyIndex < 0) throw new Error('minimumReleaseAgeExclude section missing') -let end = keyIndex + 1 -while (end < lines.length && (lines[end].trim() === '' || /^\s+-\s/.test(lines[end]))) end += 1 -const existing = new Set(lines.slice(keyIndex + 1, end).map((line) => line.trim())) -const additions = candidates.filter((line) => !existing.has(line.trim())) -lines.splice(end, 0, ...additions) +if (rewritten === 0) throw new Error(`no verified DSH release-age exclusions found for ${tested}`) fs.writeFileSync(path, lines.join('\n')) -console.log(`seeded ${additions.length} exact ${candidate} release-age exclusions`) +console.log(`rewrote ${rewritten} DSH exclusions as exact ${tested} || ${candidate} disjunctions`) NODE stage "resolve transient candidate graph" From fafd7cc99d649f0d8d02d6ceb0e79063af132c36 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:30:46 +0800 Subject: [PATCH 07/11] fix(dsh): bridge current settings namespace registration --- packages/dsh/src/config-files.ts | 79 ++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/dsh/src/config-files.ts b/packages/dsh/src/config-files.ts index 3fcc138..4d0e33a 100644 --- a/packages/dsh/src/config-files.ts +++ b/packages/dsh/src/config-files.ts @@ -4,17 +4,20 @@ * * Precedence (tested): defaults → user → project → request → runtime * validation. The user layer comes from `ctx.settings` when a settings - * provider is composed (official settings-namespace seam); the project layer - * is the committed file; request layers come from the resolve call. + * provider is composed; the project layer is the committed file; request + * layers come from the resolve call. */ import { readFile } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' +import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' import { loadPolicyLayers, type PolicyDocument, type PolicyLayerValue } from '@dshelm/core' -import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings' export const DSHELM_CONFIG_DIR = '.dshelm' export const DSHELM_CONFIG_FILE = 'config.jsonc' -export const DSHELM_SETTINGS_NAMESPACE = settingsNamespace('dshelm') +// rc.7 exported settingsNamespace(); current DSH validates the literal inside +// SettingsProvider.register(). The branded literal works with both type surfaces +// without depending on the removed value export. +export const DSHELM_SETTINGS_NAMESPACE = 'dshelm' as SettingsNamespace /** Schema-backed user-level override document (optional fields). */ export interface DSHelmUserSettings { @@ -23,6 +26,32 @@ export interface DSHelmUserSettings { readonly categories?: Record } +/** + * Minimal callable schema accepted by both settings generations. The settings + * service has already merged its composition/user layers before invoking this + * function; full DSHelm policy validation remains owned by @dshelm/core. + * `toJSON` is the descriptor surface used by settings UIs. + */ +const dshelmUserSettingsSchema = Object.assign( + (value: unknown): DSHelmUserSettings => { + if (value === undefined) return {} + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError('dshelm settings section must be an object') + } + return value as DSHelmUserSettings + }, + { + toJSON: () => ({ + type: 'object', + properties: { + profiles: { type: 'object' }, + agents: { type: 'object' }, + categories: { type: 'object' }, + }, + }), + }, +) + /** * Load the project layer from `/.dshelm/config.jsonc` when present. * A missing file yields `undefined` (no project override); malformed @@ -39,32 +68,34 @@ export async function loadProjectPolicyLayer(cwd: string): Promise DSHelmUserSettings | undefined { - let source: DSHelmUserSettings = base - installSettingsSection( - ctx, - DSHELM_SETTINGS_NAMESPACE, - // Minimal schema: free-form policy sections layered on the base document. - // Full schema validation happens in @dshelm/core at load time. - { profiles: { type: 'object' }, agents: { type: 'object' }, categories: { type: 'object' } } as never, - base, - { - setSource: (current) => { source = current() }, - onChange: () => {}, - }, - ) - return () => source + let current: () => DSHelmUserSettings = () => base + + ctx.inject(['settings'], (settingsCtx) => { + const scope = settingsCtx.settings.register( + DSHELM_SETTINGS_NAMESPACE, + dshelmUserSettingsSchema as never, + { base }, + ) + current = () => scope.get() as DSHelmUserSettings + settingsCtx.effect(() => () => { + current = () => base + }) + }) + + return () => current() } /** From b4bdd6081f11ecf06b2bbbd8c59732f4fba32e4f Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:30:54 +0800 Subject: [PATCH 08/11] fix(dsh): augment current projection state map --- packages/dsh/src/projection-types.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/dsh/src/projection-types.ts b/packages/dsh/src/projection-types.ts index eedd210..321c46e 100644 --- a/packages/dsh/src/projection-types.ts +++ b/packages/dsh/src/projection-types.ts @@ -1,16 +1,23 @@ import type { ControlPlaneProjectionValue } from './session-events.ts' /** - * Host-only augmentation for the merge-extensible DSH projection map. + * Host-only augmentation for the merge-extensible DSH projection maps. * * Kept out of `session-events.ts` because that shared wire-types module is also - * compiled into the browser client declaration graph. Both rc.7 and 0.1.5 - * publish this augmentation target from `@deepseek-ai/dsh-session-projection/types`. + * compiled into the browser client declaration graph. rc.7 uses only + * `SessionProjectionMap`; current DSH separates internal fold state into + * `SessionProjectionStateMap`. Declaring both keeps one DSHelm projection key + * valid across the two generations without leaking host augmentation into the + * browser declaration graph. */ declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { 'dshelm.controlPlane': ControlPlaneProjectionValue } + + interface SessionProjectionStateMap { + 'dshelm.controlPlane': ControlPlaneProjectionValue | undefined + } } export {} From 60820cbc3344d4b8499212dcf0d5fd7c044abdb8 Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:31:13 +0800 Subject: [PATCH 09/11] fix(dsh): bridge projection definition generations --- packages/dsh/src/projection.ts | 71 ++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/dsh/src/projection.ts b/packages/dsh/src/projection.ts index fac7517..1c3c6fc 100644 --- a/packages/dsh/src/projection.ts +++ b/packages/dsh/src/projection.ts @@ -3,11 +3,10 @@ * * The host publishes whole-value `dshelm/control-plane` events; this unit * folds them into the `dshelm.controlPlane` projection key, which the - * official `session/projection` wire + client projection face deliver - * to the browser. Registered through `ctx.sessionProjections.register`. + * official session-projection wire + client projection face deliver to the + * browser. Registered through `ctx.sessionProjections.register`. */ import { z } from 'zod' -import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import type { SessionEvent } from '@deepseek-ai/dsh-session' import './projection-types.ts' import type { ControlPlaneProjectionValue } from './session-events.ts' @@ -18,7 +17,7 @@ import type { ControlPlaneProjectionValue } from './session-events.ts' * The annotation is `z.ZodType`: zod's inferred * output spells optional fields as `T | undefined` while the interfaces use * exact optional properties, so the annotation is a variance-only cast — the - * runtime `parse` (the projection drive's only runtime consumer) is unchanged. + * runtime `parse` is unchanged. */ export const controlPlaneSchema: z.ZodType = z.object({ version: z.literal(1), @@ -60,23 +59,55 @@ export const controlPlaneSchema: z.ZodType = z.obje /** Internal fold state: the latest whole snapshot. */ type ControlPlaneState = ControlPlaneProjectionValue | undefined -export const dshelmControlPlaneProjection: ProjectionDefinition<'dshelm.controlPlane', ControlPlaneState> = { - key: 'dshelm.controlPlane', - schema: controlPlaneSchema, - init: () => undefined, - apply: (state, event: SessionEvent) => - event.type === 'dshelm/control-plane' ? event.data : state, - view: (state) => { - if (state === undefined) { - return { - version: 1, - request: { category: '' }, - roles: [], - inspector: { request: 'no-delegation', trace: { version: 1, request: { category: '' }, category: '', agent: '', profile: '', candidates: [], fields: [] } }, - source: 'projection:empty', - } +const controlPlaneStateSchema = controlPlaneSchema.optional() as unknown as z.ZodType + +function viewControlPlane(state: ControlPlaneState): ControlPlaneProjectionValue { + if (state === undefined) { + return { + version: 1, + request: { category: '' }, + roles: [], + inspector: { + request: 'no-delegation', + trace: { + version: 1, + request: { category: '' }, + category: '', + agent: '', + profile: '', + candidates: [], + fields: [], + }, + }, + source: 'projection:empty', } - return state + } + return state +} + +/** + * Dual-generation projection definition. + * + * rc.7 reads the top-level `schema` + `view` fields. Current DSH separates the + * persisted fold state (`stateSchema`) from the client wire surface (`wire`). + * Keeping both structural contracts on one object lets each registry consume + * only the fields it owns while the actual fold/view semantics stay identical. + */ +export const dshelmControlPlaneProjection = { + key: 'dshelm.controlPlane' as const, + // rc.7 projection contract. + schema: controlPlaneSchema, + view: viewControlPlane, + // Current projection contract. + stateSchema: controlPlaneStateSchema, + wire: { + viewSchema: controlPlaneSchema, + view: viewControlPlane, }, + // A no-argument initializer remains assignable to the current initializer, + // whose SessionHeader/inheritedEventCount inputs are not needed here. + init: (): ControlPlaneState => undefined, + apply: (state: ControlPlaneState, event: SessionEvent): ControlPlaneState => + event.type === 'dshelm/control-plane' ? event.data : state, stateVersion: 1, } From a414676ed9293ce5feda088fec74027d112612fa Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:34:52 +0800 Subject: [PATCH 10/11] fix(dsh): anchor projection module augmentations across releases --- packages/dsh/src/projection-types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/dsh/src/projection-types.ts b/packages/dsh/src/projection-types.ts index 321c46e..16d1c49 100644 --- a/packages/dsh/src/projection-types.ts +++ b/packages/dsh/src/projection-types.ts @@ -1,3 +1,5 @@ +import type {} from '@deepseek-ai/dsh-session-projection' +import type {} from '@deepseek-ai/dsh-session-projection/types' import type { ControlPlaneProjectionValue } from './session-events.ts' /** @@ -9,6 +11,12 @@ import type { ControlPlaneProjectionValue } from './session-events.ts' * `SessionProjectionStateMap`. Declaring both keeps one DSHelm projection key * valid across the two generations without leaking host augmentation into the * browser declaration graph. + * + * The explicit type-only imports are load-bearing: the root import activates + * DSH's `Context.sessionProjections` augmentation, while the `/types` import + * makes this declaration an augmentation of the published module rather than + * a new ambient module after `projection.ts` stopped importing the + * generation-specific `ProjectionDefinition` type. */ declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { From 0ae6a4dffa2e4de3a8f506b6c4c025ecdb8921fd Mon Sep 17 00:00:00 2001 From: Altair Li <137127498+Altairpaca@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:37:55 +0800 Subject: [PATCH 11/11] fix(cli): use cross-version empty system prompt config --- packages/cli/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 27afb50..bd275f0 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -90,7 +90,11 @@ async function probeRuntime(): Promise<{ rows: Row[]; failed: boolean }> { const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) - await ctx.plugin(SystemPrompt, { persona: '' }) + // Empty config is valid in both the verified rc.7 SystemPrompt generation + // (`persona?`) and current DSH (`personaPrefix?` / `personaSuffix?`). The + // doctor probe registers its scoped prompt section separately, so it does + // not need a deployment persona here. + await ctx.plugin(SystemPrompt, {}) await ctx.plugin(ToolRuntime) await ctx.plugin(AgentRegistry) await ctx.plugin(AgentLoop, { agents: [] })