@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 +31 -0
- package/README.md +26 -0
- package/cordis.patch.yml +1 -0
- package/lib/index.js +13 -2
- package/lib/setup.js +67 -1
- package/lib/skill.js +98 -0
- package/package.json +6 -1
- package/scripts/setup.mjs +42 -5
- package/scripts/smoke-boot.mjs +55 -20
- package/skills/dsh-dcp/SKILL.md +91 -0
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
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.
|
|
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`)
|
package/scripts/smoke-boot.mjs
CHANGED
|
@@ -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.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
|
94
|
-
//
|
|
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,
|
|
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
|
+
就手动删掉那一行。
|