@aiwayds/dsh-dcp 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -105,6 +105,28 @@ npx dsh-dcp-setup # safe: date-stamped backup → append-only
105
105
  > mount it. The web profile does not bundle the TUI, so it keeps the official
106
106
  > backend and is unaffected.
107
107
 
108
+ ## Uninstall
109
+
110
+ **Bundle path** (`dsh plugin add`, or listed under `bundles`):
111
+
112
+ ```sh
113
+ dsh plugin --profile <name> remove @aiwayds/dsh-dcp
114
+ ```
115
+
116
+ The host reconciles automatically: the bundles entry is spliced, the patch layer drops with the package, and the stock LLM summarizer (`compaction-basic`) re-enables.
117
+
118
+ **Setup-script path** (the patch block `npx dsh-dcp-setup` wrote):
119
+
120
+ ```bash
121
+ npx dsh-dcp-setup --remove # default home patch
122
+ npx dsh-dcp-setup --remove --profile tui # a named profile
123
+ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
124
+ ```
125
+
126
+ `--remove` strips only the setup-written mount block (including any config you tuned), with the same date-stamped backup before modifying; hand-written mounts are left alone; the file is deleted when nothing else remains. Two WARN lines matter: a leftover `compaction-basic` disable entry keeps the stock summarizer off (remove it by hand if it was only for dsh-dcp), and another dsh-dcp mount in the same file is untouched.
127
+
128
+ Removing the package without the reverse step leaves the mount pointing at the vanished absolute entry path, and the profile boot fails with module-not-found.
129
+
108
130
  ## /dcp command
109
131
 
110
132
  | Command | Effect |
@@ -154,6 +176,15 @@ All optional, defaults work out of the box:
154
176
 
155
177
  > **Upgrade note (0.5.0)**: the `roundInterval` counter switched from completed turns to assistant messages — the same value now triggers more often (a single turn usually contains several assistant messages).
156
178
 
179
+ ## Bundled skill
180
+
181
+ The package registers a `dsh-dcp` skill (`skills/dsh-dcp/SKILL.md`, served via
182
+ `ctx.skills.registerProvider`): ask the agent to tune compaction, configure dcp,
183
+ or debug compaction behavior and the guide loads automatically — it carries an
184
+ interactive `ask_user_question` tuning wizard (collect the expectation first,
185
+ then map it to keys) and the persistent recipe for the `config:` section of the
186
+ cordis.patch.yml mount block.
187
+
157
188
  ## Design reference
158
189
 
159
190
  - [Opencode-DCP/opencode-dynamic-context-pruning](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)
package/README.md CHANGED
@@ -84,6 +84,28 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
84
84
 
85
85
  > dsh-dcp 挂在 dsh 的压缩接口上,只对挂载了它的 profile 生效。web profile 没挂 tui,继续用官方压缩,不受影响。
86
86
 
87
+ ## 卸载
88
+
89
+ **Bundle 方式**(`dsh plugin add` 或列在 `bundles`):
90
+
91
+ ```sh
92
+ dsh plugin --profile <name> remove @aiwayds/dsh-dcp
93
+ ```
94
+
95
+ 宿主自动收敛:bundles 条目移除、patch 层随包消失,官方 LLM 压缩后端 `compaction-basic` 自动恢复。
96
+
97
+ **setup 脚本方式**(`npx dsh-dcp-setup` 写入的 patch 块):
98
+
99
+ ```bash
100
+ npx dsh-dcp-setup --remove # 默认 home patch
101
+ npx dsh-dcp-setup --remove --profile tui # 指定 profile
102
+ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
103
+ ```
104
+
105
+ `--remove` 只删除 setup 写入的挂载块(含你调过的 config),修改前同样带日期备份;手工写的挂载块不受影响;删完文件为空时自动删除文件。两条 WARN 要留意:文件里若还留有 `compaction-basic` 的 disable 条目,官方压缩后端会保持关闭 —— 不是给 dsh-dcp 用的就手动删掉;同一文件里另一处 dsh-dcp 挂载不会被碰。
106
+
107
+ 不执行反向步骤直接卸包,patch 里的绝对路径会指向已消失的安装目录,profile 启动将以 module-not-found 失败。
108
+
87
109
  ## /dcp 命令
88
110
 
89
111
  | 命令 | 作用 |
@@ -126,6 +148,10 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
126
148
 
127
149
  > **升级提示(0.5.0)**:`roundInterval` 的计数单位由 completed turn 改为 assistant message——同值下触发会更频繁(一个 turn 内往往有多条 assistant message)。
128
150
 
151
+ ## 内置技能 / Bundled skill
152
+
153
+ 插件随包注册了 `dsh-dcp` skill(`skills/dsh-dcp/SKILL.md`,经 `ctx.skills.registerProvider`):在会话里让 agent 调压缩、配置 dcp 或排查压缩行为时,指南自动加载——内含 ask_user_question 交互式调参向导(先问期望再映射到具体键)与 cordis.patch.yml 挂载块 `config:` 段的持久化写法,无需翻文档。
154
+
129
155
  ## 设计参考
130
156
 
131
157
  - [Opencode-DCP/opencode-dynamic-context-pruning](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)
package/cordis.patch.yml CHANGED
@@ -7,6 +7,7 @@
7
7
  # cordis.patch.yml editing.
8
8
 
9
9
  - id: compaction-basic
10
+ name: '@deepseek-ai/dsh-compaction-basic'
10
11
  disabled: true
11
12
  - insert:
12
13
  - id: dsh-dcp
package/lib/index.js CHANGED
@@ -35,6 +35,7 @@ import { ManualCompactionError } from '@deepseek-ai/dsh-compaction'
35
35
  import { splitConfig, resolveDcpConfig } from './config.js'
36
36
  import { summarizeDeterministically, noticeText } from './summarizer.js'
37
37
  import { registerDcpCommand } from './command.js'
38
+ import { skillProvider } from './skill.js'
38
39
 
39
40
  const require = createRequire(import.meta.url)
40
41
  const { version: VERSION } = require('../package.json')
@@ -68,7 +69,8 @@ const kAppendNotice = Symbol('dsh-dcp.appendNotice')
68
69
 
69
70
  /**
70
71
  * Deterministic compaction engine: `summarize()` overridden, everything else
71
- * inherited. Registers the `/dcp` command beside the inherited `/compact`.
72
+ * inherited. Registers the `/dcp` command beside the inherited `/compact`,
73
+ * and serves the bundled usage/config guide through the host skill registry.
72
74
  */
73
75
  /** Element schema mirroring compaction-basic's model-policy override shape. */
74
76
  const modelPolicy = z.object({
@@ -85,7 +87,7 @@ const modelPolicy = z.object({
85
87
  })
86
88
 
87
89
  export class DcpEngine extends BasicCompactionEngine {
88
- static inject = ['llm', 'tokenMeter', 'sessions', 'commands']
90
+ static inject = ['llm', 'tokenMeter', 'sessions', 'commands', 'skills']
89
91
 
90
92
  static Config = z.object({
91
93
  // compaction-basic policy keys (forwarded verbatim)
@@ -140,6 +142,15 @@ export class DcpEngine extends BasicCompactionEngine {
140
142
  this.dcpStats = { compactions: 0, shadowedTokens: 0, lastAt: null }
141
143
  this.pluginPath = fileURLToPath(import.meta.url)
142
144
  const engine = this
145
+ // Bundled usage/config guide. Soft-guarded: engines constructed in exotic
146
+ // hosts without the skill registry must still boot; losing the bundled
147
+ // guide is non-fatal. On the real host, `skills` in `static inject`
148
+ // guarantees the service is present (verified by scripts/smoke-boot.mjs).
149
+ try {
150
+ ctx?.skills?.registerProvider?.(() => skillProvider)
151
+ } catch {
152
+ // non-fatal — see comment above
153
+ }
143
154
  ctx.effect(function* () {
144
155
  yield registerDcpCommand(ctx, engine, VERSION)
145
156
  }, 'dsh-dcp /dcp command lifecycle')
package/lib/setup.js CHANGED
@@ -11,8 +11,12 @@
11
11
  */
12
12
  import { readdirSync, readFileSync } from 'node:fs'
13
13
 
14
- /** The patch entry that disables the default LLM summarizer. */
14
+ /** The patch entry that disables the default LLM summarizer. The `name` is
15
+ * a guard, not an override: if a future host renames or drops the row, the
16
+ * entry is skipped with a loader warning instead of silently disabling an
17
+ * unrelated component (the same pattern dsh-tui-pi's patch uses). */
15
18
  const DISABLE_ENTRY = `- id: compaction-basic
19
+ name: '@deepseek-ai/dsh-compaction-basic'
16
20
  disabled: true`
17
21
 
18
22
  /** The dsh-dcp mount block, one YAML list item, appended to the file. */
@@ -93,3 +97,65 @@ export function backupStamp(date = new Date()) {
93
97
  const p = (n) => String(n).padStart(2, '0')
94
98
  return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}`
95
99
  }
100
+
101
+ /** The marker comment every setup-written mount block starts with. */
102
+ const MOUNT_MARKER = 'added by @aiwayds/dsh-dcp setup'
103
+
104
+ /**
105
+ * Reverse of planPatch/mountBlock: remove the setup-appended dsh-dcp mount
106
+ * block — the marker comment, the disable entry it added, and the insert
107
+ * item including any config the user tuned inside it — from one patch
108
+ * file's text. Hand-written mounts without the marker are NOT touched (the
109
+ * human wrote it; the human removes it); user entries after the block are
110
+ * preserved byte-for-byte.
111
+ *
112
+ * Uninstalling the package without this reverse step leaves the mount
113
+ * pointing at the vanished absolute entry path, which fails the profile's
114
+ * whole boot with a module-not-found error.
115
+ *
116
+ * @param {string|undefined} text - the patch file content.
117
+ * @returns {{ removed: boolean, text: string, note?: string }} `text` with
118
+ * the block gone ('' when nothing else remains — the caller may delete the
119
+ * file); `removed: false` leaves `text` untouched and sets `note`.
120
+ */
121
+ export function planRemoval(text) {
122
+ if (!text) return { removed: false, text: text ?? '' }
123
+ const lines = text.split('\n')
124
+ const marker = lines.findIndex((line) => line.includes(MOUNT_MARKER))
125
+ if (marker === -1) {
126
+ return { removed: false, text, note: 'no setup-written dsh-dcp mount block found (hand-written mounts are left alone)' }
127
+ }
128
+ let i = marker + 1
129
+ let end = lines.length
130
+ let sawInsert = false
131
+ while (i < lines.length) {
132
+ const line = lines[i]
133
+ if (line === '') { i++; continue }
134
+ if (line.startsWith('- ')) {
135
+ // One top-level list item: the entry line plus its indented body (and
136
+ // any blank lines inside it the user added while tuning config). The
137
+ // split artifact after a final newline is EOF, not a blank to consume.
138
+ let j = i + 1
139
+ while (j < lines.length && (lines[j] === '' ? j < lines.length - 1 : lines[j].startsWith(' ') || lines[j].startsWith('\t'))) j++
140
+ if (line.startsWith('- insert')) {
141
+ sawInsert = true
142
+ end = j
143
+ break
144
+ }
145
+ i = j
146
+ continue
147
+ }
148
+ // Anything else at column 0 (a user comment or entry) ends our block.
149
+ end = i
150
+ break
151
+ }
152
+ if (!sawInsert) {
153
+ return { removed: false, text, note: 'the setup marker was found but the dsh-dcp insert item is missing — leaving the file untouched' }
154
+ }
155
+ let start = marker
156
+ // The block was appended with one leading blank line; take it so repeated
157
+ // mount/unmount cycles do not accumulate blank runs.
158
+ if (start > 0 && lines[start - 1].trim() === '') start -= 1
159
+ const remaining = [...lines.slice(0, start), ...lines.slice(end)].join('\n')
160
+ return { removed: true, text: remaining.trim() === '' ? '' : remaining }
161
+ }
package/lib/skill.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Bundled usage/configuration skill for dsh-dcp.
3
+ *
4
+ * Ships `skills/dsh-dcp/SKILL.md` through `ctx.skills.registerProvider`
5
+ * (same mechanism as dsh-llm-proxy and dsh-vault): an agent asked to tune
6
+ * compaction, run `/dcp`, or persist a config change loads the guide
7
+ * automatically instead of guessing at key names and mount shapes.
8
+ *
9
+ * @module dsh-dcp/skill
10
+ */
11
+
12
+ import { readFile } from 'node:fs/promises'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ // The runtime import of '@deepseek-ai/dsh-skill' is deliberately avoided:
16
+ // the registry-published dsh-skill lib imports host-closure siblings
17
+ // (@deepseek-ai/dsh-scope, dsh-llm — peers of it, but absent from a plugin
18
+ // repo's own dependency graph), which dies under pnpm's isolated layout.
19
+ // The host injects the real service at runtime; this module only hands it a
20
+ // plain provider object.
21
+
22
+ /** Mirrors dsh-skill's bundled-skill rank (a non-load-bearing ordering hint;
23
+ * the constant is hardcoded there too). Local copy — see the note above
24
+ * for why dsh-skill is not loaded at runtime here. */
25
+ const BUNDLED_SKILL_RANK = 600
26
+
27
+ /** Provider name under `ctx.skills`; doubles as the skill name. */
28
+ export const SKILL_PROVIDER_NAME = 'dsh-dcp'
29
+
30
+ /** Packaged skill body; `../skills/` resolves to the package root from lib/. */
31
+ const SKILL_BODY_URL = new URL('../skills/dsh-dcp/SKILL.md', import.meta.url)
32
+
33
+ /** Resource base served with the skill so its relative links resolve. */
34
+ const SKILL_RESOURCE_BASE = {
35
+ kind: 'directory',
36
+ path: fileURLToPath(new URL('../skills/dsh-dcp/', import.meta.url)),
37
+ }
38
+
39
+ const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true }
40
+
41
+ /** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
42
+ export const SKILL_DESCRIPTION = 'dsh 压缩引擎插件(@aiwayds/dsh-dcp)使用与配置指南。凡涉及上下文压缩、/dcp 命令、压缩调参(阈值/密度/语言/轮数触发),或要配置 dcp 时先读本指南:/dcp 状态与 /dcp set 十个可调键、持久化到 cordis.patch.yml 挂载块 config: 段(dsh-dcp-setup 管理)、ask_user_question 调参向导、四类触发(压力/溢出/轮数/手动)、subagent 会话独立计数生效。触发词:dcp、压缩、compaction、上下文超限、摘要、thresholdRatio、roundInterval。'
43
+
44
+ const SKILL_CANDIDATE = {
45
+ name: SKILL_PROVIDER_NAME,
46
+ description: SKILL_DESCRIPTION,
47
+ invocation: SKILL_INVOCATION,
48
+ provider: SKILL_PROVIDER_NAME,
49
+ source: 'bundled',
50
+ resourceBase: SKILL_RESOURCE_BASE,
51
+ rank: BUNDLED_SKILL_RANK,
52
+ locator: SKILL_BODY_URL,
53
+ }
54
+
55
+ /** The bundled-skill catalog entry, served through the host skill registry. */
56
+ export const skillProvider = {
57
+ name: SKILL_PROVIDER_NAME,
58
+ list(_options) {
59
+ return Promise.resolve([SKILL_CANDIDATE])
60
+ },
61
+ async get(_candidate, _options) {
62
+ return {
63
+ name: SKILL_CANDIDATE.name,
64
+ description: SKILL_CANDIDATE.description,
65
+ invocation: SKILL_CANDIDATE.invocation,
66
+ provider: SKILL_CANDIDATE.provider,
67
+ source: SKILL_CANDIDATE.source,
68
+ resourceBase: SKILL_RESOURCE_BASE,
69
+ content: stripFrontmatter(await readFile(SKILL_BODY_URL, 'utf8')),
70
+ }
71
+ },
72
+ }
73
+
74
+ /**
75
+ * Strip a leading YAML frontmatter block (`---` / body / `---`) from a skill
76
+ * markdown file. `SkillDefinition.content` must be the instruction body after
77
+ * metadata removal — the same shape the filesystem provider serves — so the
78
+ * bundled SKILL.md, which keeps its frontmatter for the GitHub/manual install
79
+ * paths, has the block removed when served through `skillProvider.get`.
80
+ * Tolerant by design: input that does not open with a `---` line, or whose
81
+ * frontmatter block is never closed, is returned unchanged. Mirrors the
82
+ * delimiter semantics of the upstream skill-filesystem provider.
83
+ */
84
+ export function stripFrontmatter(raw) {
85
+ const firstLineEnd = raw.indexOf('\n')
86
+ if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') return raw
87
+ let lineStart = firstLineEnd + 1
88
+ while (lineStart <= raw.length) {
89
+ const nextNewline = raw.indexOf('\n', lineStart)
90
+ const lineEnd = nextNewline < 0 ? raw.length : nextNewline
91
+ if (raw.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
92
+ return raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1).trim()
93
+ }
94
+ if (nextNewline < 0) return raw
95
+ lineStart = nextNewline + 1
96
+ }
97
+ return raw
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-dcp",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Deterministic context-pruning compaction backend for dsh (DeepSeek Harness) — zero-LLM summaries, /dcp command, works out of the box. Design references Opencode-DCP/opencode-dynamic-context-pruning.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "lib",
19
19
  "scripts",
20
+ "skills",
20
21
  "README.md",
21
22
  "README.en.md",
22
23
  "LICENSE",
@@ -61,6 +62,7 @@
61
62
  "@deepseek-ai/dsh-invariants": ">=0.1.2-rc.1",
62
63
  "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
63
64
  "@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
65
+ "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1",
64
66
  "@deepseek-ai/dsh-token-meter": ">=0.1.2-rc.1",
65
67
  "@deepseek-ai/schemastery": "^3.18.2"
66
68
  },
@@ -95,6 +97,9 @@
95
97
  "@deepseek-ai/dsh-session": {
96
98
  "optional": true
97
99
  },
100
+ "@deepseek-ai/dsh-skill": {
101
+ "optional": true
102
+ },
98
103
  "@deepseek-ai/dsh-token-meter": {
99
104
  "optional": true
100
105
  },
package/scripts/setup.mjs CHANGED
@@ -20,11 +20,13 @@
20
20
  * node scripts/setup.mjs # home patch
21
21
  * node scripts/setup.mjs --profile tui # tui profile's patch
22
22
  * node scripts/setup.mjs /path/to/cordis.patch.yml
23
+ * node scripts/setup.mjs --remove [...] # reverse: remove the setup-written
24
+ * # mount block (same target forms)
23
25
  */
24
26
  import fs from 'node:fs'
25
27
  import path from 'node:path'
26
28
  import { fileURLToPath } from 'node:url'
27
- import { planPatch, backupStamp, findBundledProfiles } from '../lib/setup.js'
29
+ import { planPatch, planRemoval, hasEntry, isMounted, backupStamp, findBundledProfiles } from '../lib/setup.js'
28
30
 
29
31
  const PKG = '@aiwayds/dsh-dcp'
30
32
  const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
@@ -36,18 +38,53 @@ function home() {
36
38
 
37
39
  function parseArgs(argv) {
38
40
  const force = argv.includes('--force')
41
+ const remove = argv.includes('--remove')
39
42
  if (argv.includes('--profile')) {
40
43
  const index = argv.indexOf('--profile')
41
44
  const name = argv[index + 1]
42
45
  if (!name) throw new Error('setup: --profile requires a profile name')
43
- return { target: path.join(home(), 'profiles', name, 'cordis.patch.yml'), kind: 'profile', profileName: name, force }
46
+ return { target: path.join(home(), 'profiles', name, 'cordis.patch.yml'), kind: 'profile', profileName: name, force, remove }
44
47
  }
45
48
  const explicit = argv.find((a) => !a.startsWith('-'))
46
- if (explicit) return { target: path.resolve(explicit), kind: 'explicit', force }
47
- return { target: path.join(home(), 'cordis.patch.yml'), kind: 'home', force }
49
+ if (explicit) return { target: path.resolve(explicit), kind: 'explicit', force, remove }
50
+ return { target: path.join(home(), 'cordis.patch.yml'), kind: 'home', force, remove }
51
+ }
52
+
53
+ const { target, kind, profileName, force, remove } = parseArgs(process.argv.slice(2))
54
+
55
+ if (remove) {
56
+ const existed = fs.existsSync(target)
57
+ const text = existed ? fs.readFileSync(target, 'utf8') : undefined
58
+ if (!existed) {
59
+ console.log(`no ${target} — nothing to remove.`)
60
+ process.exit(0)
61
+ }
62
+ const plan = planRemoval(text)
63
+ if (!plan.removed) {
64
+ console.log(`dsh-dcp mount not removed from ${target}.`)
65
+ if (plan.note) console.log(`note: ${plan.note}`)
66
+ process.exit(0)
67
+ }
68
+ const backup = `${target}.bak.${backupStamp()}`
69
+ fs.copyFileSync(target, backup)
70
+ console.log(`backup: ${backup}`)
71
+ if (plan.text === '') {
72
+ fs.rmSync(target)
73
+ console.log(`removed ${target} (the file held nothing else)`)
74
+ } else {
75
+ fs.writeFileSync(target, plan.text)
76
+ console.log(`removed the dsh-dcp mount block from ${target}`)
77
+ }
78
+ if (plan.text !== '' && hasEntry(plan.text, 'compaction-basic')) {
79
+ console.warn('WARN: a compaction-basic entry remains in this patch file — the stock LLM summarizer stays disabled; remove that entry by hand if it was only for dsh-dcp.')
80
+ }
81
+ if (plan.text !== '' && isMounted(plan.text)) {
82
+ console.warn('WARN: another dsh-dcp mount remains in this patch file — leaving it alone.')
83
+ }
84
+ console.log('restart dsh to apply.')
85
+ process.exit(0)
48
86
  }
49
87
 
50
- const { target, kind, profileName, force } = parseArgs(process.argv.slice(2))
51
88
  const existed = fs.existsSync(target)
52
89
  const text = existed ? fs.readFileSync(target, 'utf8') : undefined
53
90
  if (!existed) console.log(`no ${target} — will generate a fresh patch file`)
@@ -12,11 +12,16 @@
12
12
  // as a file: dependency, and a cordis.patch.yml carrying the
13
13
  // disable-compaction-basic + insert-dsh-dcp mount block
14
14
  // 3. pnpm install in the profile
15
- // 4. `dsh --profile smoke --dump-config` must compose dsh-dcp into the tree
16
- // (mount/patch-layer proof)
17
- // 5. a real boot under a timeout must load the plugin tree without a loader
18
- // error (a healthy boot is silent and survives to the kill signal; a
19
- // broken plugin dies within ~1s with the loader error)
15
+ // 4. Phase A: setup.mjs mounts the profile patch (marker block), the
16
+ // composed tree carries dsh-dcp with compaction-basic disabled, then
17
+ // `setup.mjs --remove` strips it and the recomposed tree is back to
18
+ // stock (this is what breaks a profile when the package is removed
19
+ // without the reverse step)
20
+ // 5. Phase B: a hand-written mount pointing into the installed tarball —
21
+ // the true user shape — composes, and a real boot under a timeout must
22
+ // load the plugin tree without a loader error (a healthy boot is silent
23
+ // and survives to the kill signal; a broken plugin dies within ~1s with
24
+ // the loader error)
20
25
  //
21
26
  // The dsh CLI comes from $DSH_BIN if set (e.g. a scratch alpha closure:
22
27
  // DSH_BIN=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai/dsh/lib/bin.js),
@@ -57,18 +62,9 @@ if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${
57
62
  const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1) ?? '')
58
63
 
59
64
  writeFileSync(path.join(profile, 'cordis.yml'), '# dsh profile root — empty; the tree is composed from the bundle patches\n[]\n')
60
- // The mount layer under test — isomorphic with scripts/setup.mjs's output:
61
- // compaction-basic disabled, dsh-dcp inserted by absolute entry path.
62
- writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
63
- - id: compaction-basic
64
- disabled: true
65
- - insert:
66
- - id: ${entryId}
67
- name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
68
- config:
69
- thresholdRatio: 0.7
70
- language: zh
71
- `)
65
+ // The patch file starts EMPTY: Phase A below drives the full setup.mjs
66
+ // mount → remove lifecycle against it, then Phase B hand-writes the mount
67
+ // (isomorphic with setup.mjs's output) for the real-boot proof.
72
68
  writeFileSync(path.join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
73
69
  writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
74
70
  name: 'dsh-profile-smoke',
@@ -90,8 +86,47 @@ writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
90
86
  const install = spawnSync('pnpm', ['install'], { cwd: profile, encoding: 'utf8' })
91
87
  if (install.status !== 0 || install.error) fail('pnpm install in the scratch profile failed', `${install.stdout}\n${install.stderr}`)
92
88
 
93
- // Phase 1 — mount proof: the composed tree must carry the dsh-dcp entry, with
94
- // compaction-basic disabled rather than removed-by-default.
89
+ // Phase A — the setup.mjs lifecycle: mount → compose → remove → stock tree.
90
+ // This is the exact pair of commands a standalone user runs (`npx dsh-dcp-setup`
91
+ // and its `--remove`), against the profile patch file they target.
92
+ const patchFile = path.join(profile, 'cordis.patch.yml')
93
+ const setup = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'setup.mjs'), patchFile], { encoding: 'utf8' })
94
+ if (setup.status !== 0 || setup.error) fail('setup.mjs mount failed', `${setup.stdout}\n${setup.stderr}`)
95
+ const dumpMounted = dsh(['--profile', 'smoke', '--dump-config'])
96
+ if (dumpMounted.status !== 0 || dumpMounted.error) fail('dsh --dump-config failed on the setup-mounted profile', `${dumpMounted.stdout}\n${dumpMounted.stderr}`)
97
+ if (!dumpMounted.stdout.includes(entryId) || !/compaction-basic\n( name: .*\n)? disabled: true/.test(dumpMounted.stdout)) {
98
+ fail('the setup-mounted tree does not carry dsh-dcp with compaction-basic disabled', dumpMounted.stdout)
99
+ }
100
+ const remove = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'setup.mjs'), '--remove', patchFile], { encoding: 'utf8' })
101
+ if (remove.status !== 0 || remove.error) fail('setup.mjs --remove failed', `${remove.stdout}\n${remove.stderr}`)
102
+ const dumpAfter = dsh(['--profile', 'smoke', '--dump-config'])
103
+ if (dumpAfter.status !== 0 || dumpAfter.error) fail('dsh --dump-config failed after removal', `${dumpAfter.stdout}\n${dumpAfter.stderr}`)
104
+ if (dumpAfter.stdout.includes(entryId)) {
105
+ fail(`the composed tree still contains the "${entryId}" entry after removal`, dumpAfter.stdout)
106
+ }
107
+ if (/compaction-basic\n( name: .*\n)? disabled: true/.test(dumpAfter.stdout)) {
108
+ fail('compaction-basic is still disabled after removal — the stock backend did not come back', dumpAfter.stdout)
109
+ }
110
+ if (!dumpAfter.stdout.includes('compaction-basic')) {
111
+ fail('compaction-basic vanished from the composed tree after removal', dumpAfter.stdout)
112
+ }
113
+ console.log('smoke-boot: setup.mjs mount → remove lifecycle restored the stock tree')
114
+
115
+ // Phase B — mount proof: the composed tree must carry the dsh-dcp entry, with
116
+ // compaction-basic disabled rather than removed-by-default. The hand-written
117
+ // mount points INTO the installed tarball (the true user shape), so the boot
118
+ // below exercises the packed artifact.
119
+ writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
120
+ - id: compaction-basic
121
+ name: '@deepseek-ai/dsh-compaction-basic'
122
+ disabled: true
123
+ - insert:
124
+ - id: ${entryId}
125
+ name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
126
+ config:
127
+ thresholdRatio: 0.7
128
+ language: zh
129
+ `)
95
130
  const dump = dsh(['--profile', 'smoke', '--dump-config'])
96
131
  if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
97
132
  if (!dump.stdout.includes(entryId)) {
@@ -118,5 +153,5 @@ if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
118
153
  fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
119
154
  }
120
155
 
121
- console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, and booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`})`)
156
+ console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`}); setup mount → remove lifecycle restored the stock tree`)
122
157
  rmSync(work, { recursive: true, force: true })
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: dsh-dcp
3
+ description: "dsh 压缩引擎插件(@aiwayds/dsh-dcp)使用与配置指南。凡涉及上下文压缩、/dcp 命令、压缩调参(阈值/密度/语言/轮数触发),或要配置 dcp 时先读本指南:/dcp 状态与 /dcp set 十个可调键、持久化到 cordis.patch.yml 挂载块 config: 段(dsh-dcp-setup 管理)、ask_user_question 调参向导、四类触发(压力/溢出/轮数/手动)、subagent 会话独立计数生效。触发词:dcp、压缩、compaction、上下文超限、摘要、thresholdRatio、roundInterval。"
4
+ ---
5
+
6
+ # dsh-dcp 使用指南(确定性上下文压缩)
7
+
8
+ > dsh 插件:替换官方 `compaction-basic` 的确定性压缩引擎——**零 LLM 调用**
9
+ > (去重 / 折错 / 密度控制),中文场景优先(CJK 计价、中文报错与"待办:"识别),
10
+ > 附轮数触发。压力触发、保留尾巴、溢出恢复、tool-pairing 安全机制全部继承官方,只替换"摘要"这一环。
11
+
12
+ ## 配置入口(两条路)
13
+
14
+ 1. **会话内临时调参**:`/dcp set <键> <值>`,只影响当前会话,重启失效。十个可调键:
15
+ `dedup` `purgeErrors` `maxItems` `maxItemChars` `maxSummaryTokens` `language`
16
+ `tokenEstimate` `thresholdRatio` `roundInterval` `notice`。
17
+ 2. **持久化**:cordis.patch.yml 里 dsh-dcp 挂载块的 `config:` 段。用
18
+ `npx dsh-dcp-setup` 写入并维护(带 marker 注释、改动前日期备份、幂等);
19
+ `--remove` 只删 setup 写的块,手工写的块不受影响。bundle 方式
20
+ (`dsh plugin add @aiwayds/dsh-dcp` 或列在 profile `bundles`)自动挂载,无需手写 patch。
21
+
22
+ 挂载块形状(`~/.dsh/cordis.patch.yml` 或某个 profile 的 cordis.patch.yml):
23
+
24
+ ```yaml
25
+ - id: compaction-basic
26
+ name: '@deepseek-ai/dsh-compaction-basic'
27
+ disabled: true
28
+ - insert:
29
+ - id: dsh-dcp
30
+ name: '@aiwayds/dsh-dcp'
31
+ config:
32
+ thresholdRatio: 0.7 # 每个键都可选
33
+ language: zh
34
+ ```
35
+
36
+ ## 配置键
37
+
38
+ dsh-dcp 自有键(除 `thresholdRatio` 外全部可用 `/dcp set` 调):
39
+
40
+ | 键 | 默认 | 说明 |
41
+ |---|---|---|
42
+ | `dedup` | `true` | 重复工具调用折叠成一条标注 |
43
+ | `purgeErrors` | `true` | 旧报错折叠成一条提示 |
44
+ | `maxItems` / `maxItemChars` | 10 / 200 | 摘要密度(条数 / 单条字符上限) |
45
+ | `maxSummaryTokens` | 2048 | 摘要 token 预算 |
46
+ | `language` | `en`(代码默认;bundle 挂载默认 `zh`) | 摘要语言;`zh` 额外识别中文报错和"待办:" |
47
+ | `tokenEstimate` | `cjk` | CJK(中/日/韩/全角)按 ~2 字符/token 计价;`ascii` 与宿主一致 |
48
+ | `protectedTools` | `['write', 'edit', 'apply_patch']` | 写侧工具(子串匹配)的重复调用不折叠进 dedup 标注 |
49
+ | `roundInterval` | 50 | 每 N 条 assistant message(一次 LLM 往返)触发一次压缩;`0` 关闭 |
50
+ | `notice` | `true` | 压缩后在会话追加一行通知 |
51
+
52
+ 转发上游 compaction-basic 的策略键:`thresholdRatio`(上游默认 0.8;**本插件 bundle 挂载默认 0.7**,中文场景建议 0.7)、`retainRatio`、`retainTokens`、`maxTokens`、`summarizationProvider`、`summarizationModel`、`compactionRetries`、`maxOverflowRetries`、`modelPolicies`、`auto`。
53
+
54
+ ## 交互式调参向导(ask_user_question)
55
+
56
+ 用户抱怨压缩行为时,不要甩配置表让对方自己读——先用 `ask_user_question`
57
+ 问清期望,再映射到键:
58
+
59
+ 1. **触发时机**:更晚触发 → `thresholdRatio` 调高,或 `roundInterval` 调大
60
+ (完全不想要轮数触发 → `roundInterval: 0`);更早/更频繁 → 反向。
61
+ 2. **摘要密度**:更细 → `maxItems` / `maxItemChars` 调大(预算不够再加
62
+ `maxSummaryTokens`);更省 → 调小。
63
+ 3. **摘要语言** → `language: en|zh`。
64
+ 4. **token 计价** → `tokenEstimate: cjk|ascii`。
65
+ 5. **通知行** → `notice: on|off`。
66
+
67
+ 流程:先用 `/dcp set <键> <值>` 在会话内试效果,满意后再代写持久 config——
68
+ 直接改 cordis.patch.yml 里 dsh-dcp 挂载块的 `config:` 段(setup 写的块可原位改,marker 保留)。
69
+
70
+ ## 触发条件(四类)
71
+
72
+ | 触发 | 时机 | 说明 |
73
+ |---|---|---|
74
+ | 压力 | 每步请求前 | token ≥ `thresholdRatio` × 上下文窗口 |
75
+ | 溢出 | 模型报 context 超限 | 继承官方恢复流程 |
76
+ | 轮数 | 每累计 `roundInterval` 条 assistant message | 任何一次压缩(含压力/手动)都重置时钟;`0` 关闭;需保持 `auto: true`(默认开) |
77
+ | 手动 | `/dcp compact`、`/compact` | 随时可用 |
78
+
79
+ - **subagent 同样生效**:进程内子代理(含 continuable 与 one-shot)走同一套事件分发,
80
+ 压力/溢出/轮数对每个会话独立计数、独立触发。
81
+ - `notice` 通知行本身也是上下文(每次压缩约 15–25 tokens);`notice: false` 可关。
82
+
83
+ ## 排障
84
+
85
+ 1. `/dcp`(无参数)看状态:当前配置、压缩次数、省下的 LLM 调用,以及
86
+ per-session 概览(含子代理;已销毁的会话自动消失,列表最多前 10 个,超出显示 `+N more`)。
87
+ 2. 会话压不动 → 先确认 `auto` 是否为 `true`(自动触发总开关,默认开),再看
88
+ `thresholdRatio` 是否设得过高、`roundInterval` 是否为 `0`。
89
+ 3. `npx dsh-dcp-setup --remove` 后出现 `WARN: a compaction-basic entry remains...` →
90
+ patch 文件里还留着 disable 行,官方 LLM 压缩后端会保持关闭;不是给 dsh-dcp 用的
91
+ 就手动删掉那一行。