@aiwayds/dsh-dcp 0.9.0 → 0.10.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 +7 -3
- package/README.md +7 -3
- package/lib/command.js +8 -2
- package/lib/config.js +16 -1
- package/lib/index.js +194 -23
- package/lib/skill.js +5 -5
- package/lib/summarizer.js +21 -0
- package/package.json +1 -1
- package/skills/{dsh-dcp → dsh-dcp-config}/SKILL.md +13 -6
package/README.en.md
CHANGED
|
@@ -137,7 +137,7 @@ Removing the package without the reverse step leaves the mount pointing at the v
|
|
|
137
137
|
|
|
138
138
|
Settable: `dedup`, `purgeErrors`, `maxItems`, `maxItemChars`,
|
|
139
139
|
`maxSummaryTokens`, `language`, `tokenEstimate`, `thresholdRatio`,
|
|
140
|
-
`roundInterval`, `notice`.
|
|
140
|
+
`roundInterval`, `notice`, `onModelSwitch`, `modelSwitchMinTokens`.
|
|
141
141
|
|
|
142
142
|
The `/dcp` status also lists every session that has compacted (subagents
|
|
143
143
|
included): `per-session: session-1 (2 compactions, ~444 tokens), child
|
|
@@ -153,9 +153,11 @@ for the rest) so the status stays one line.
|
|
|
153
153
|
| Pressure | before every step | tokens ≥ `thresholdRatio` (inherited upstream default 0.8; this plugin's bundle mounts 0.7 — see config table) × context window |
|
|
154
154
|
| Overflow recovery | on a provider context-window error | inherited |
|
|
155
155
|
| **Round interval** | every `roundInterval` assistant messages | added by this plugin; one round = one LLM roundtrip (each tool-iteration response counts, so one-shot subagents trigger too). **Default 50**: first compaction after message 50, then every 50 more (100, 150, …); any compaction (pressure included) restarts the clock. Fires at the first idle boundary after the count is reached (below the pressure threshold too). `0` disables; requires the default `auto: true` |
|
|
156
|
+
| **Model switch** | the session's effective provider/model route changes | added by this plugin (`onModelSwitch`). Default `notice`: appends one row suggesting `/dcp compact` first to shadow the old model's history and save tokens; `auto` compacts at the session's next idle boundary; `off` disables. Two gates: switches within 10 assistant messages of the last compaction are ignored (nothing stale to shadow), and switches while the context is below `modelSwitchMinTokens` (default 32768, `0` disables this gate) are ignored (not enough to be worth shadowing); `auto` requires the default `auto: true`, otherwise it degrades to `notice` |
|
|
156
157
|
| Manual | `/dcp compact`, `/compact` | anytime |
|
|
157
158
|
|
|
158
|
-
- **Subagents are covered**: in-process subagents (including continuable and one-shot children) dispatch through the same events, so pressure/overflow/round triggers count and fire per child session independently. The round trigger counts assistant messages, so a one-shot subagent whose whole run is a single turn (many tool iterations) triggers too.
|
|
159
|
+
- **Subagents are covered**: in-process subagents (including continuable and one-shot children) dispatch through the same events, so pressure/overflow/round/model-switch triggers count and fire per child session independently. The round trigger counts assistant messages, so a one-shot subagent whose whole run is a single turn (many tool iterations) triggers too.
|
|
160
|
+
- **How model-switch detection works**: the per-request `request/context` routing snapshot is folded per session (a provider or model change alone counts as a switch), covering every switch entry point — TUI `/model`, web clients, changed default-model settings. A session's first observed request only seeds the baseline. `notice: false` silences the compaction rows only; the switch row is controlled by `onModelSwitch` independently. The size floor is measured by the host token meter (`measure().surfaceTokens`) and fails open when unavailable.
|
|
159
161
|
- **Visibility**: after every trigger event a one-line notice row (`dcp: compacted N history items (~X tokens, trigger)`) is appended to the session; frontends render it as a collapsed row. Note the row also rides the model request context (~15–25 tokens per compaction), and it is **on by default since 0.4.0** — disable with `notice: false`. `/dcp` stats count every committed region (a pressure retry loop may commit several).
|
|
160
162
|
|
|
161
163
|
## Configuration
|
|
@@ -166,6 +168,8 @@ All optional, defaults work out of the box:
|
|
|
166
168
|
|---|---|---|
|
|
167
169
|
| `thresholdRatio` | 0.8 | pressure trigger (inherited upstream compaction-basic default 0.8; this plugin's bundle patch mounts 0.7, recommended for CJK-heavy sessions) |
|
|
168
170
|
| `roundInterval` | 50 | compact every N assistant messages (one LLM roundtrip) (0 disables). Default 50: 50, 100, 150… — the clock restarts after every compaction |
|
|
171
|
+
| `onModelSwitch` | `notice` | after a model switch: `notice` suggests `/dcp compact` (default); `auto` compacts at the next idle boundary; `off` disables |
|
|
172
|
+
| `modelSwitchMinTokens` | 32768 | minimum priced context size (measured by the host token meter) for a model switch to be announced; `0` disables this gate. 32k default ≈ the post-compaction baseline (~16% of the window) plus some real growth |
|
|
169
173
|
| `notice` | `true` | append the one-line compaction notice to the session |
|
|
170
174
|
| `language` | `zh` | summary language; `zh` also enables Chinese error/"待办:" detection |
|
|
171
175
|
| `tokenEstimate` | `cjk` | CJK (zh/ja/ko/full-width) at ~2 chars/token; `ascii` matches the host |
|
|
@@ -178,7 +182,7 @@ All optional, defaults work out of the box:
|
|
|
178
182
|
|
|
179
183
|
## Bundled skill
|
|
180
184
|
|
|
181
|
-
The package registers a `dsh-dcp` skill (`skills/dsh-dcp/SKILL.md`, served via
|
|
185
|
+
The package registers a `dsh-dcp-config` skill (`skills/dsh-dcp-config/SKILL.md`, served via
|
|
182
186
|
`ctx.skills.registerProvider`): ask the agent to tune compaction, configure dcp,
|
|
183
187
|
or debug compaction behavior and the guide loads automatically — it carries an
|
|
184
188
|
interactive `ask_user_question` tuning wizard (collect the expectation first,
|
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
|
|
|
114
114
|
| `/dcp compact` | 立即压缩(零 LLM) |
|
|
115
115
|
| `/dcp set <k> <v>` | 会话内调参,并提示如何持久化 |
|
|
116
116
|
|
|
117
|
-
可调键:`dedup`、`purgeErrors`、`maxItems`、`maxItemChars`、`maxSummaryTokens`、`language`、`tokenEstimate`、`thresholdRatio`、`roundInterval`、`notice`。
|
|
117
|
+
可调键:`dedup`、`purgeErrors`、`maxItems`、`maxItemChars`、`maxSummaryTokens`、`language`、`tokenEstimate`、`thresholdRatio`、`roundInterval`、`notice`、`onModelSwitch`、`modelSwitchMinTokens`。
|
|
118
118
|
|
|
119
119
|
`/dcp` 状态还会列出每个发生过压缩的会话(per-session 概览,含子代理),例如 `per-session: session-1 (2 compactions, ~444 tokens), child (1 compaction, ~22 tokens)`。压缩按会话独立计数;已销毁的会话(含 one-shot 子代理)自动从概览消失;列表封顶一行(最多前 10 个会话,超出显示 `+N more`)。
|
|
120
120
|
|
|
@@ -125,9 +125,11 @@ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
|
|
|
125
125
|
| 压力触发 | 每步请求前 | token ≥ `thresholdRatio`(继承上游默认 0.8;本插件 bundle 挂载默认 0.7,见配置表)× 上下文窗口 |
|
|
126
126
|
| 溢出恢复 | 模型报 context 超限时 | 继承官方 |
|
|
127
127
|
| **轮数触发** | 会话每收到 `roundInterval` 条 assistant message | 本插件新增;一条 = 一次 LLM 往返(每轮工具迭代各算一条,one-shot 子代理也能触发)。**默认 50**:第 50 条后触发第一次,之后每 50 条一次(100、150……);任何一次压缩(含压力触发)都会重置轮数时钟。到达条数后的第一个空闲点触发(阈值之下也压)。`0` 关闭;需保持 `auto: true`(默认开) |
|
|
128
|
+
| **模型切换** | 会话实际路由的 provider/model 变化时 | 本插件新增(`onModelSwitch`)。默认 `notice`:追加一行提醒"建议先执行 `/dcp compact` 压缩旧模型历史,节省 token";`auto` 在该会话下一个空闲点自动压缩;`off` 关闭。两道门控:距上次压缩不足 10 条 assistant message 忽略(没有旧账可甩),上下文不足 `modelSwitchMinTokens`(默认 32768,`0` 关闭该门)忽略(不够甩的量);`auto` 需保持 `auto: true`(默认开),否则降级为 `notice` |
|
|
128
129
|
| 手动 | `/dcp compact`、`/compact` | 随时可用 |
|
|
129
130
|
|
|
130
|
-
- **subagent 同样生效**:进程内 subagent(含 continuable 与 one-shot
|
|
131
|
+
- **subagent 同样生效**:进程内 subagent(含 continuable 与 one-shot 子代理)走同一套事件分发,压力/溢出/轮数/模型切换对子会话独立计数、独立触发。轮数触发按 assistant message 计数,所以全程只有 1 个 turn 的 one-shot 子代理(多次工具迭代)也能触发。
|
|
132
|
+
- **模型切换检测原理**:折叠每请求的 `request/context` 路由快照(provider 或 model 任一变化即判定切换),覆盖所有切换入口(TUI `/model`、Web 客户端、默认模型设置变更);会话首个请求只播种基线不告警。`notice: false` 只关压缩通知行,模型切换提醒行由 `onModelSwitch` 独立控制。上下文下限按宿主 tokenMeter 的实测计价(`measure().surfaceTokens`),测不到时 fail-open(只留轮数门控)。
|
|
131
133
|
- **压缩可见性**:每次压缩成功后,会话里追加一行 `dcp: 已压缩 N 条历史(约 X tokens,触发方式)` 通知行(前端渲染为折叠行)。注意该行也会作为上下文随请求发给模型(每次压缩约 15–25 tokens),且 **0.4.0 起默认开启**;`notice: false` 可关闭。`/dcp` 的 stats 持续累计(压力触发的多次 region 提交各计一次)。
|
|
132
134
|
|
|
133
135
|
## 配置
|
|
@@ -138,6 +140,8 @@ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
|
|
|
138
140
|
|---|---|---|
|
|
139
141
|
| `thresholdRatio` | 0.8 | 压力触发阈值(继承上游 compaction-basic 默认 0.8;本插件 bundle patch 挂载时默认 0.7,中文场景建议 0.7) |
|
|
140
142
|
| `roundInterval` | 50 | 每 N 条 assistant message(一次 LLM 往返)触发一次压缩(0 关闭)。默认 50:50、100、150……每次压缩后重数 |
|
|
143
|
+
| `onModelSwitch` | `notice` | 模型切换后:`notice` 提醒执行 `/dcp compact`(默认);`auto` 下一个空闲点自动压缩;`off` 关闭 |
|
|
144
|
+
| `modelSwitchMinTokens` | 32768 | 模型切换提醒/自动压缩的上下文下限(按宿主 tokenMeter 实测计价):不足则忽略该次切换;`0` 关闭此门。默认 32k ≈ 压缩后基线(~16% 窗口)之上再涨一截才有得甩 |
|
|
141
145
|
| `notice` | `true` | 压缩后在会话中追加一行通知 |
|
|
142
146
|
| `language` | `zh` | 摘要语言;`zh` 额外识别中文报错和"待办:" |
|
|
143
147
|
| `tokenEstimate` | `cjk` | CJK(中/日/韩/全角)按 ~2 字符/token 计价;`ascii` 与宿主一致 |
|
|
@@ -150,7 +154,7 @@ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
|
|
|
150
154
|
|
|
151
155
|
## 内置技能 / Bundled skill
|
|
152
156
|
|
|
153
|
-
插件随包注册了 `dsh-dcp` skill(`skills/dsh-dcp/SKILL.md`,经 `ctx.skills.registerProvider`):在会话里让 agent 调压缩、配置 dcp 或排查压缩行为时,指南自动加载——内含 ask_user_question 交互式调参向导(先问期望再映射到具体键)与 cordis.patch.yml 挂载块 `config:` 段的持久化写法,无需翻文档。
|
|
157
|
+
插件随包注册了 `dsh-dcp-config` skill(`skills/dsh-dcp-config/SKILL.md`,经 `ctx.skills.registerProvider`):在会话里让 agent 调压缩、配置 dcp 或排查压缩行为时,指南自动加载——内含 ask_user_question 交互式调参向导(先问期望再映射到具体键)与 cordis.patch.yml 挂载块 `config:` 段的持久化写法,无需翻文档。
|
|
154
158
|
|
|
155
159
|
## 设计参考
|
|
156
160
|
|
package/lib/command.js
CHANGED
|
@@ -14,7 +14,8 @@ const USAGE = `Usage:
|
|
|
14
14
|
/dcp compact compact now (deterministic, no LLM call)
|
|
15
15
|
/dcp set <k> <v> adjust a knob for this session (dedup, purgeErrors,
|
|
16
16
|
maxItems, maxItemChars, maxSummaryTokens, language,
|
|
17
|
-
tokenEstimate, thresholdRatio, roundInterval, notice
|
|
17
|
+
tokenEstimate, thresholdRatio, roundInterval, notice,
|
|
18
|
+
onModelSwitch, modelSwitchMinTokens)`
|
|
18
19
|
|
|
19
20
|
const FAILURE_TEXT = Object.freeze({
|
|
20
21
|
busy: 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
|
|
@@ -53,6 +54,11 @@ function applySet(engine, key, rawValue) {
|
|
|
53
54
|
engine.dcp.tokenEstimate = value
|
|
54
55
|
return `tokenEstimate = ${value} (this session)`
|
|
55
56
|
}
|
|
57
|
+
if (kind === 'model-switch-mode') {
|
|
58
|
+
if (value !== 'off' && value !== 'notice' && value !== 'auto') return 'onModelSwitch expects off/notice/auto'
|
|
59
|
+
engine.dcp.onModelSwitch = value
|
|
60
|
+
return `onModelSwitch = ${value} (this session)`
|
|
61
|
+
}
|
|
56
62
|
const numeric = Number(rawValue)
|
|
57
63
|
if (!Number.isFinite(numeric)) return `${key} expects a number`
|
|
58
64
|
if (kind === 'ratio') {
|
|
@@ -78,7 +84,7 @@ function statusText(engine, version) {
|
|
|
78
84
|
const stats = engine.dcpStats
|
|
79
85
|
const lines = [
|
|
80
86
|
`dsh-dcp ${version} — deterministic compaction backend (zero LLM summarization calls)`,
|
|
81
|
-
`config: dedup=${engine.dcp.dedup} purgeErrors=${engine.dcp.purgeErrors} maxItems=${engine.dcp.maxItems} maxItemChars=${engine.dcp.maxItemChars} maxSummaryTokens=${engine.dcp.maxSummaryTokens} language=${engine.dcp.language} tokenEstimate=${engine.dcp.tokenEstimate} protectedTools=[${engine.dcp.protectedTools.join(', ')}] thresholdRatio=${engine.config.thresholdRatio} roundInterval=${engine.dcp.roundInterval}${engine.dcp.roundInterval > 0 ? '' : ' (off)'} notice=${engine.dcp.notice}`,
|
|
87
|
+
`config: dedup=${engine.dcp.dedup} purgeErrors=${engine.dcp.purgeErrors} maxItems=${engine.dcp.maxItems} maxItemChars=${engine.dcp.maxItemChars} maxSummaryTokens=${engine.dcp.maxSummaryTokens} language=${engine.dcp.language} tokenEstimate=${engine.dcp.tokenEstimate} protectedTools=[${engine.dcp.protectedTools.join(', ')}] thresholdRatio=${engine.config.thresholdRatio} roundInterval=${engine.dcp.roundInterval}${engine.dcp.roundInterval > 0 ? '' : ' (off)'} notice=${engine.dcp.notice} onModelSwitch=${engine.dcp.onModelSwitch}${engine.dcp.onModelSwitch === 'off' ? '' : ` minTokens=${engine.dcp.modelSwitchMinTokens}${engine.dcp.modelSwitchMinTokens > 0 ? '' : ' (off)'}`}`,
|
|
82
88
|
`stats: ${stats.compactions} compaction${stats.compactions === 1 ? '' : 's'}, ~${stats.shadowedTokens} tokens shadowed, ${stats.compactions} LLM summary call${stats.compactions === 1 ? '' : 's'} avoided`,
|
|
83
89
|
]
|
|
84
90
|
// Per-session dimension: one compacted session per entry, in store
|
package/lib/config.js
CHANGED
|
@@ -18,6 +18,8 @@ export const DCP_CONFIG_KEYS = [
|
|
|
18
18
|
'protectedTools',
|
|
19
19
|
'roundInterval',
|
|
20
20
|
'notice',
|
|
21
|
+
'onModelSwitch',
|
|
22
|
+
'modelSwitchMinTokens',
|
|
21
23
|
]
|
|
22
24
|
|
|
23
25
|
/** compaction-basic policy keys forwarded to the parent engine verbatim. */
|
|
@@ -45,6 +47,8 @@ const DEFAULTS = Object.freeze({
|
|
|
45
47
|
protectedTools: Object.freeze(['write', 'edit', 'apply_patch']),
|
|
46
48
|
roundInterval: 50,
|
|
47
49
|
notice: true,
|
|
50
|
+
onModelSwitch: 'notice',
|
|
51
|
+
modelSwitchMinTokens: 32768,
|
|
48
52
|
})
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -75,7 +79,7 @@ export function splitConfig(config = {}) {
|
|
|
75
79
|
* Validate and resolve dcp defaults.
|
|
76
80
|
*
|
|
77
81
|
* @param {Record<string, unknown>} raw - the dcp half of {@link splitConfig}.
|
|
78
|
-
* @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[], roundInterval: number, notice: boolean}>}
|
|
82
|
+
* @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[], roundInterval: number, notice: boolean, onModelSwitch: 'off'|'notice'|'auto', modelSwitchMinTokens: number}>}
|
|
79
83
|
*/
|
|
80
84
|
export function resolveDcpConfig(raw = {}) {
|
|
81
85
|
if (raw.dedup !== undefined && typeof raw.dedup !== 'boolean') {
|
|
@@ -110,6 +114,15 @@ export function resolveDcpConfig(raw = {}) {
|
|
|
110
114
|
if (raw.notice !== undefined && typeof raw.notice !== 'boolean') {
|
|
111
115
|
throw new Error('DcpConfig: notice must be a boolean')
|
|
112
116
|
}
|
|
117
|
+
if (raw.onModelSwitch !== undefined && raw.onModelSwitch !== 'off' && raw.onModelSwitch !== 'notice' && raw.onModelSwitch !== 'auto') {
|
|
118
|
+
throw new Error('DcpConfig: onModelSwitch must be "off", "notice", or "auto"')
|
|
119
|
+
}
|
|
120
|
+
if (raw.modelSwitchMinTokens !== undefined) {
|
|
121
|
+
const value = raw.modelSwitchMinTokens
|
|
122
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
123
|
+
throw new Error('DcpConfig: modelSwitchMinTokens must be a non-negative integer (0 disables the size gate)')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
113
126
|
return Object.freeze({ ...DEFAULTS, ...raw })
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -125,4 +138,6 @@ export const RUNTIME_SETTABLE = Object.freeze({
|
|
|
125
138
|
thresholdRatio: 'ratio',
|
|
126
139
|
roundInterval: 'nonnegative-integer',
|
|
127
140
|
notice: 'boolean',
|
|
141
|
+
onModelSwitch: 'model-switch-mode',
|
|
142
|
+
modelSwitchMinTokens: 'nonnegative-integer',
|
|
128
143
|
})
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* config:
|
|
22
22
|
* thresholdRatio: 0.7 # optional; 0.7 = this bundle patch's mount value, package default is 0.8; every key is optional
|
|
23
23
|
* roundInterval: 100 # optional; also compact every N assistant messages, one per LLM roundtrip (default 50)
|
|
24
|
+
* onModelSwitch: auto # optional; after a model switch: notice (default) | auto-compact | off
|
|
24
25
|
* ```
|
|
25
26
|
*
|
|
26
27
|
* @module dsh-dcp
|
|
@@ -33,7 +34,7 @@ import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
|
|
|
33
34
|
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
34
35
|
import { ManualCompactionError } from '@deepseek-ai/dsh-compaction'
|
|
35
36
|
import { splitConfig, resolveDcpConfig } from './config.js'
|
|
36
|
-
import { summarizeDeterministically, noticeText } from './summarizer.js'
|
|
37
|
+
import { summarizeDeterministically, noticeText, modelSwitchNoticeText } from './summarizer.js'
|
|
37
38
|
import { registerDcpCommand } from './command.js'
|
|
38
39
|
import { skillProvider } from './skill.js'
|
|
39
40
|
|
|
@@ -61,11 +62,29 @@ const kRounds = Symbol('dsh-dcp.rounds')
|
|
|
61
62
|
const kTriggerLabels = Symbol('dsh-dcp.triggerLabels')
|
|
62
63
|
const kRoundInFlight = Symbol('dsh-dcp.roundInFlight')
|
|
63
64
|
const kSessionStats = Symbol('dsh-dcp.sessionStats')
|
|
65
|
+
const kRoutes = Symbol('dsh-dcp.routes')
|
|
66
|
+
const kSwitchPending = Symbol('dsh-dcp.switchPending')
|
|
67
|
+
const kRegisterAssistantMessageCounter = Symbol('dsh-dcp.registerAssistantMessageCounter')
|
|
64
68
|
const kRegisterRoundTrigger = Symbol('dsh-dcp.registerRoundTrigger')
|
|
65
69
|
const kMaybeRoundCompact = Symbol('dsh-dcp.maybeRoundCompact')
|
|
70
|
+
const kRegisterModelSwitchWatch = Symbol('dsh-dcp.registerModelSwitchWatch')
|
|
71
|
+
const kMaybeOnModelSwitch = Symbol('dsh-dcp.maybeOnModelSwitch')
|
|
72
|
+
const kAttemptSwitchCompaction = Symbol('dsh-dcp.attemptSwitchCompaction')
|
|
66
73
|
const kRecordStats = Symbol('dsh-dcp.recordStats')
|
|
67
74
|
const kRecordSessionStats = Symbol('dsh-dcp.recordSessionStats')
|
|
68
75
|
const kAppendNotice = Symbol('dsh-dcp.appendNotice')
|
|
76
|
+
const kAppendSwitchNotice = Symbol('dsh-dcp.appendSwitchNotice')
|
|
77
|
+
const kMeetsMinTokens = Symbol('dsh-dcp.meetsMinTokens')
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A model switch detected fewer than this many assistant messages after the
|
|
81
|
+
* session's last committed compaction is ignored (both notice and auto
|
|
82
|
+
* modes): a switch right after a compaction has no stale history to shadow,
|
|
83
|
+
* so prompting (or compacting again) would only add noise and rewrites.
|
|
84
|
+
* Deliberately not a config key — it is a guard rail, not a user-intent knob.
|
|
85
|
+
* The token-size counterpart is the user-tunable `modelSwitchMinTokens`.
|
|
86
|
+
*/
|
|
87
|
+
const MODEL_SWITCH_MIN_ROUNDS = 10
|
|
69
88
|
|
|
70
89
|
/**
|
|
71
90
|
* Deterministic compaction engine: `summarize()` overridden, everything else
|
|
@@ -112,6 +131,8 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
112
131
|
protectedTools: z.array(z.string()),
|
|
113
132
|
roundInterval: z.number().step(1).min(0),
|
|
114
133
|
notice: z.boolean(),
|
|
134
|
+
onModelSwitch: z.string(),
|
|
135
|
+
modelSwitchMinTokens: z.number().step(1).min(0),
|
|
115
136
|
})
|
|
116
137
|
|
|
117
138
|
/** Resolved dcp knobs; mutable at runtime through `/dcp set`. */
|
|
@@ -138,6 +159,10 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
138
159
|
this[kRoundInFlight] = new WeakSet()
|
|
139
160
|
/** Per-session compaction records for `/dcp` (weak so disposed sessions drop out). */
|
|
140
161
|
this[kSessionStats] = new WeakMap()
|
|
162
|
+
/** Per-session last routed `{provider, model}` for model-switch detection. */
|
|
163
|
+
this[kRoutes] = new WeakMap()
|
|
164
|
+
/** Sessions with a model-switch auto compaction waiting for an idle boundary. */
|
|
165
|
+
this[kSwitchPending] = new WeakSet()
|
|
141
166
|
this.dcp = { ...resolveDcpConfig(dcp) }
|
|
142
167
|
this.dcpStats = { compactions: 0, shadowedTokens: 0, lastAt: null }
|
|
143
168
|
this.pluginPath = fileURLToPath(import.meta.url)
|
|
@@ -154,30 +179,41 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
154
179
|
ctx.effect(function* () {
|
|
155
180
|
yield registerDcpCommand(ctx, engine, VERSION)
|
|
156
181
|
}, 'dsh-dcp /dcp command lifecycle')
|
|
182
|
+
// The counter is unconditional: the round trigger reads it, and so does
|
|
183
|
+
// the model-switch recency gate. Counting also continues while the round
|
|
184
|
+
// trigger is disabled (`roundInterval: 0`), so `/dcp set roundInterval N`
|
|
185
|
+
// can arm it mid-session and the switch gate stays meaningful.
|
|
186
|
+
this[kRegisterAssistantMessageCounter]()
|
|
157
187
|
if (this.config.auto) this[kRegisterRoundTrigger]()
|
|
188
|
+
this[kRegisterModelSwitchWatch]()
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* and agent/status dispatch, so continuable children are covered exactly
|
|
166
|
-
* like the top-level session — and because one-shot subagents emit many
|
|
167
|
-
* assistant messages inside a single turn, they now trigger too.
|
|
192
|
+
* Unconditional per-session assistant-message counting: every assembled
|
|
193
|
+
* assistant message is one completed LLM roundtrip, so this WeakMap is the
|
|
194
|
+
* "rounds since the session's last committed compaction" clock shared by
|
|
195
|
+
* the round trigger and the model-switch recency gate.
|
|
168
196
|
*/
|
|
169
|
-
[
|
|
197
|
+
[kRegisterAssistantMessageCounter]() {
|
|
170
198
|
const { ctx } = this
|
|
171
199
|
ctx.on('session/event', (session, event) => {
|
|
172
|
-
// Counting is skipped while disabled, but the listener stays registered
|
|
173
|
-
// so `/dcp set roundInterval N` can arm it again at runtime.
|
|
174
|
-
if (!this.dcp.roundInterval) return
|
|
175
|
-
// Every assembled assistant message is one completed LLM roundtrip;
|
|
176
|
-
// counting it (instead of completed turns) also covers one-shot
|
|
177
|
-
// subagents, whose whole run is a single turn with many model calls.
|
|
178
200
|
if (event.type !== 'assistant/message') return
|
|
179
201
|
this[kRounds].set(session, (this[kRounds].get(session) ?? 0) + 1)
|
|
180
202
|
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Round-interval trigger: once the session's assistant-message clock
|
|
207
|
+
* (kept by `[kRegisterAssistantMessageCounter]`) reaches the configured
|
|
208
|
+
* `roundInterval`, compact at the agent's next idle boundary through the
|
|
209
|
+
* manual-compaction seam (`compactNow`). In-process subagents run through
|
|
210
|
+
* the same session/event and agent/status dispatch, so continuable
|
|
211
|
+
* children are covered exactly like the top-level session — and because
|
|
212
|
+
* one-shot subagents emit many assistant messages inside a single turn,
|
|
213
|
+
* they now trigger too.
|
|
214
|
+
*/
|
|
215
|
+
[kRegisterRoundTrigger]() {
|
|
216
|
+
const { ctx } = this
|
|
181
217
|
ctx.on('agent/status', ({ agent, status }) => {
|
|
182
218
|
if (status === 'idle') this[kMaybeRoundCompact](agent)
|
|
183
219
|
})
|
|
@@ -213,6 +249,136 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
213
249
|
})
|
|
214
250
|
}
|
|
215
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Model-switch watch: fold `request/context` (the per-request routing
|
|
254
|
+
* snapshot, logged on change) per session; a provider or model change from
|
|
255
|
+
* the session's previous route is a switch. Covers every entry point — TUI
|
|
256
|
+
* `/model`, web clients, changed default settings — because it observes the
|
|
257
|
+
* effective route rather than any client's selection intent. Subagent
|
|
258
|
+
* sessions fold independently, mirroring the round trigger's stance.
|
|
259
|
+
*/
|
|
260
|
+
[kRegisterModelSwitchWatch]() {
|
|
261
|
+
const { ctx } = this
|
|
262
|
+
ctx.on('session/event', (session, event) => {
|
|
263
|
+
if (event.type !== 'request/context') return
|
|
264
|
+
const data = /** @type {{data?: {provider?: unknown, model?: unknown}}} */ (event).data
|
|
265
|
+
if (typeof data?.provider !== 'string' || typeof data?.model !== 'string') return
|
|
266
|
+
const route = { provider: data.provider, model: data.model }
|
|
267
|
+
const previous = this[kRoutes].get(session)
|
|
268
|
+
this[kRoutes].set(session, route)
|
|
269
|
+
// First observed request only seeds the baseline: without a previous
|
|
270
|
+
// route there is no "switched from" to report.
|
|
271
|
+
if (previous === undefined) return
|
|
272
|
+
if (previous.provider === route.provider && previous.model === route.model) return
|
|
273
|
+
this[kMaybeOnModelSwitch](session, previous, route)
|
|
274
|
+
})
|
|
275
|
+
// The auto mode's compaction needs a real agent (`runMaintenance`, routed
|
|
276
|
+
// options), which `session/event` never carries — so pending switches
|
|
277
|
+
// wait for the same idle boundary the round trigger uses.
|
|
278
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
279
|
+
if (status === 'idle') this[kAttemptSwitchCompaction](agent)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Handle one detected model switch. Two gates guard every mode: the
|
|
285
|
+
* recency gate (fewer than `MODEL_SWITCH_MIN_ROUNDS` assistant messages
|
|
286
|
+
* since the session's last committed compaction — nothing stale has
|
|
287
|
+
* accumulated, so prompting or compacting again only adds noise and
|
|
288
|
+
* rewrites) and the size gate (`modelSwitchMinTokens` — the context is
|
|
289
|
+
* too small for shadowing to be worth anything). `notice` appends the
|
|
290
|
+
* suggest-`/dcp compact` row; `auto` appends the in-progress row and
|
|
291
|
+
* marks the session for the next idle boundary. The `auto` mode is an
|
|
292
|
+
* automatic compaction trigger, so the `auto: false` master switch
|
|
293
|
+
* downgrades it to `notice`.
|
|
294
|
+
*/
|
|
295
|
+
[kMaybeOnModelSwitch](session, previous, route) {
|
|
296
|
+
let mode = this.dcp.onModelSwitch
|
|
297
|
+
if (mode === 'off') return
|
|
298
|
+
if (mode === 'auto' && !this.config.auto) mode = 'notice'
|
|
299
|
+
if ((this[kRounds].get(session) ?? 0) < MODEL_SWITCH_MIN_ROUNDS) return
|
|
300
|
+
if (!this[kMeetsMinTokens](session)) return
|
|
301
|
+
const from = `${previous.provider}/${previous.model}`
|
|
302
|
+
const to = `${route.provider}/${route.model}`
|
|
303
|
+
if (mode === 'notice') {
|
|
304
|
+
this[kAppendSwitchNotice](session, 'notice', from, to)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
this[kSwitchPending].add(session)
|
|
308
|
+
this[kAppendSwitchNotice](session, 'auto', from, to)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Size gate for the model-switch feature: the session's current priced
|
|
313
|
+
* surface must reach `modelSwitchMinTokens` before a switch is worth
|
|
314
|
+
* announcing — below it there is not enough history for shadowing to save
|
|
315
|
+
* anything meaningful. `0` disables the gate. Measured per switch (the
|
|
316
|
+
* `request/context` event only fires on route changes, so this is rare).
|
|
317
|
+
* Measurement failure or absence fails open: the recency gate still
|
|
318
|
+
* applies, and a meterless exotic host keeps the feature alive.
|
|
319
|
+
*/
|
|
320
|
+
[kMeetsMinTokens](session) {
|
|
321
|
+
const min = this.dcp.modelSwitchMinTokens
|
|
322
|
+
if (!min) return true
|
|
323
|
+
try {
|
|
324
|
+
// tokenMeter arrives through `static inject`, which checkJs cannot see
|
|
325
|
+
// on the Context type — the optional chain plus cast is the contract.
|
|
326
|
+
const meter = /** @type {{measure?: (s: unknown) => {surfaceTokens?: number} | null}} */ (/** @type {any} */ (this.ctx).tokenMeter)
|
|
327
|
+
const measurement = meter?.measure?.(session)
|
|
328
|
+
if (typeof measurement?.surfaceTokens !== 'number') return true
|
|
329
|
+
return measurement.surfaceTokens >= min
|
|
330
|
+
} catch {
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Serve one pending model-switch compaction at an idle boundary. The
|
|
337
|
+
* attempt shares the round trigger's single-flight WeakSet, so the two
|
|
338
|
+
* idle listeners can never drive concurrent `compactNow` calls on the
|
|
339
|
+
* same agent; the trigger label is claimed here (not at detection time)
|
|
340
|
+
* so a manual `/dcp compact` in between cannot steal it. `busy` and
|
|
341
|
+
* `cancelled` restore the pending mark for the next boundary, mirroring
|
|
342
|
+
* the round trigger; any other failure warns and drops it — the pressure
|
|
343
|
+
* trigger remains the safety net.
|
|
344
|
+
*/
|
|
345
|
+
[kAttemptSwitchCompaction](agent) {
|
|
346
|
+
const session = agent?.session
|
|
347
|
+
if (session === undefined || !this[kSwitchPending].has(session)) return
|
|
348
|
+
if (this[kRoundInFlight].has(session)) return
|
|
349
|
+
this[kSwitchPending].delete(session)
|
|
350
|
+
this[kTriggerLabels].set(session, 'model-switch')
|
|
351
|
+
this[kRoundInFlight].add(session)
|
|
352
|
+
const settle = () => this[kRoundInFlight].delete(session)
|
|
353
|
+
void this.compactNow(agent, new AbortController().signal).then(settle, (error) => {
|
|
354
|
+
settle()
|
|
355
|
+
if (error instanceof ManualCompactionError && (error.code === 'busy' || error.code === 'cancelled')) {
|
|
356
|
+
this[kSwitchPending].add(session)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
this.ctx.logger.warn(`model-switch compaction failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Append the model-switch notice row (same collapsed-row channel as the
|
|
365
|
+
* compaction notice). Failures never surface as switch-handling failures —
|
|
366
|
+
* in auto mode the compaction still proceeds, in notice mode the switch is
|
|
367
|
+
* simply unannounced.
|
|
368
|
+
*/
|
|
369
|
+
[kAppendSwitchNotice](session, mode, from, to) {
|
|
370
|
+
const summary = boundContextSummary(modelSwitchNoticeText(this.dcp.language, mode, from, to))
|
|
371
|
+
try {
|
|
372
|
+
session.append('user/message', createUserMessage({
|
|
373
|
+
content: [{ type: 'text', text: summary }],
|
|
374
|
+
source: { kind: 'plugin', plugin: 'dsh-dcp', form: 'notice', summary },
|
|
375
|
+
}))
|
|
376
|
+
} catch (error) {
|
|
377
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
378
|
+
this.ctx.logger.warn(`dsh-dcp model-switch notice append failed: ${message}`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
216
382
|
/**
|
|
217
383
|
* The sole overridden seam: condense the replayed region deterministically.
|
|
218
384
|
* No LLM call, no cancellation window beyond the fast synchronous walk.
|
|
@@ -243,18 +409,19 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
243
409
|
}
|
|
244
410
|
|
|
245
411
|
/**
|
|
246
|
-
* Manual seam (`/dcp compact`, `/compact`) and this plugin's own
|
|
247
|
-
* round
|
|
248
|
-
* `compactRegion` (it drives `compactSurfaceRegion`
|
|
249
|
-
* this override
|
|
412
|
+
* Manual seam (`/dcp compact`, `/compact`) and this plugin's own automatic
|
|
413
|
+
* idle-boundary triggers (round interval, model switch) — the parent's
|
|
414
|
+
* `compactNow` bypasses `compactRegion` (it drives `compactSurfaceRegion`
|
|
415
|
+
* directly), so without this override those paths would miss stats and the
|
|
416
|
+
* transcript notice.
|
|
250
417
|
*
|
|
251
418
|
* alpha.3 note: the base dereferences `signal` unguarded
|
|
252
419
|
* (`signal.throwIfAborted()`), so the signal is required — both this
|
|
253
|
-
* plugin's
|
|
420
|
+
* plugin's triggers and the command invocation always pass one.
|
|
254
421
|
*/
|
|
255
422
|
async compactNow(agent, signal, sourceCommandId) {
|
|
256
423
|
const session = agent.session
|
|
257
|
-
const trigger = this[kTriggerLabels].get(session)
|
|
424
|
+
const trigger = this[kTriggerLabels].get(session) ?? 'manual'
|
|
258
425
|
try {
|
|
259
426
|
const result = await super.compactNow(agent, signal, sourceCommandId)
|
|
260
427
|
// `null` means no useful range existed: release the round counter so an
|
|
@@ -277,14 +444,18 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
277
444
|
}
|
|
278
445
|
|
|
279
446
|
/**
|
|
280
|
-
* Record one committed compaction: bump the `/dcp` counters and restart
|
|
281
|
-
* round-interval counting.
|
|
447
|
+
* Record one committed compaction: bump the `/dcp` counters and restart
|
|
448
|
+
* the round-interval counting. A committed compaction also clears any
|
|
449
|
+
* pending model-switch compaction — the stale history the switch was
|
|
450
|
+
* going to shadow is now shadowed by this compaction, whatever triggered
|
|
451
|
+
* it.
|
|
282
452
|
*/
|
|
283
453
|
[kRecordStats](session, result) {
|
|
284
454
|
this.dcpStats.compactions += 1
|
|
285
455
|
this.dcpStats.shadowedTokens += result.shadowedTokenCount
|
|
286
456
|
this.dcpStats.lastAt = Date.now()
|
|
287
457
|
this[kRounds].delete(session)
|
|
458
|
+
this[kSwitchPending].delete(session)
|
|
288
459
|
this[kRecordSessionStats](session, result.shadowedTokenCount)
|
|
289
460
|
}
|
|
290
461
|
|
package/lib/skill.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundled usage/configuration skill for dsh-dcp.
|
|
3
3
|
*
|
|
4
|
-
* Ships `skills/dsh-dcp/SKILL.md` through `ctx.skills.registerProvider`
|
|
4
|
+
* Ships `skills/dsh-dcp-config/SKILL.md` through `ctx.skills.registerProvider`
|
|
5
5
|
* (same mechanism as dsh-llm-proxy and dsh-vault): an agent asked to tune
|
|
6
6
|
* compaction, run `/dcp`, or persist a config change loads the guide
|
|
7
7
|
* automatically instead of guessing at key names and mount shapes.
|
|
@@ -25,21 +25,21 @@ import { fileURLToPath } from 'node:url'
|
|
|
25
25
|
const BUNDLED_SKILL_RANK = 600
|
|
26
26
|
|
|
27
27
|
/** Provider name under `ctx.skills`; doubles as the skill name. */
|
|
28
|
-
export const SKILL_PROVIDER_NAME = 'dsh-dcp'
|
|
28
|
+
export const SKILL_PROVIDER_NAME = 'dsh-dcp-config'
|
|
29
29
|
|
|
30
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)
|
|
31
|
+
const SKILL_BODY_URL = new URL('../skills/dsh-dcp-config/SKILL.md', import.meta.url)
|
|
32
32
|
|
|
33
33
|
/** Resource base served with the skill so its relative links resolve. */
|
|
34
34
|
const SKILL_RESOURCE_BASE = {
|
|
35
35
|
kind: 'directory',
|
|
36
|
-
path: fileURLToPath(new URL('../skills/dsh-dcp/', import.meta.url)),
|
|
36
|
+
path: fileURLToPath(new URL('../skills/dsh-dcp-config/', import.meta.url)),
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true }
|
|
40
40
|
|
|
41
41
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
42
|
-
export const SKILL_DESCRIPTION = 'dsh 压缩引擎插件(@aiwayds/dsh-dcp)使用与配置指南。凡涉及上下文压缩、/dcp
|
|
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、onModelSwitch。'
|
|
43
43
|
|
|
44
44
|
const SKILL_CANDIDATE = {
|
|
45
45
|
name: SKILL_PROVIDER_NAME,
|
package/lib/summarizer.js
CHANGED
|
@@ -133,6 +133,8 @@ const i18n = Object.freeze({
|
|
|
133
133
|
carried: 'carried from prior checkpoint',
|
|
134
134
|
terseHeader: '{messages} messages ({calls} tool calls) compacted deterministically by dsh-dcp.',
|
|
135
135
|
notice: 'dcp: compacted {items} history items (~{tokens} tokens, {trigger})',
|
|
136
|
+
modelSwitchNotice: 'dcp: model switched {from} → {to} — run /dcp compact first to shadow the old model\'s history and save tokens',
|
|
137
|
+
modelSwitchAuto: 'dcp: model switched {from} → {to} — compacting the previous model\'s history automatically',
|
|
136
138
|
}),
|
|
137
139
|
zh: Object.freeze({
|
|
138
140
|
none: '(无)',
|
|
@@ -143,6 +145,8 @@ const i18n = Object.freeze({
|
|
|
143
145
|
carried: '继承自上一次压缩检查点',
|
|
144
146
|
terseHeader: 'dsh-dcp 确定性压缩了 {messages} 条消息({calls} 次工具调用)。',
|
|
145
147
|
notice: 'dcp: 已压缩 {items} 条历史(约 {tokens} tokens,{trigger})',
|
|
148
|
+
modelSwitchNotice: 'dcp: 模型已切换 {from} → {to} —— 建议先执行 /dcp compact 压缩旧模型的历史,节省 token',
|
|
149
|
+
modelSwitchAuto: 'dcp: 模型已切换 {from} → {to} —— 正在自动压缩旧模型的历史',
|
|
146
150
|
}),
|
|
147
151
|
})
|
|
148
152
|
|
|
@@ -154,6 +158,23 @@ export function noticeText(language, items, tokens, trigger = 'auto') {
|
|
|
154
158
|
.replace('{trigger}', trigger) ?? ''
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
/**
|
|
162
|
+
* One-line model-switch notice for the transcript row. `mode` picks the
|
|
163
|
+
* copy: `notice` suggests the command, `auto` announces the upcoming
|
|
164
|
+
* automatic compaction. Unknown languages fall back to English.
|
|
165
|
+
*
|
|
166
|
+
* @param {'en'|'zh'} language
|
|
167
|
+
* @param {'notice'|'auto'} mode
|
|
168
|
+
* @param {string} fromModel - model id routed before the switch.
|
|
169
|
+
* @param {string} toModel - model id routed now.
|
|
170
|
+
*/
|
|
171
|
+
export function modelSwitchNoticeText(language, mode, fromModel, toModel) {
|
|
172
|
+
const table = i18n[language] ?? i18n.en
|
|
173
|
+
return (mode === 'auto' ? table.modelSwitchAuto : table.modelSwitchNotice)
|
|
174
|
+
.replace('{from}', fromModel)
|
|
175
|
+
.replace('{to}', toModel)
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
/** Collapse whitespace and hard-cap one item's length with an ellipsis. */
|
|
158
179
|
export function clip(text, maxChars) {
|
|
159
180
|
const flat = String(text).replace(/\s+/g, ' ').trim()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-dcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: dsh-dcp
|
|
3
|
-
description: "dsh 压缩引擎插件(@aiwayds/dsh-dcp)使用与配置指南。凡涉及上下文压缩、/dcp
|
|
2
|
+
name: dsh-dcp-config
|
|
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、onModelSwitch。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dsh-dcp 使用指南(确定性上下文压缩)
|
|
@@ -11,9 +11,10 @@ description: "dsh 压缩引擎插件(@aiwayds/dsh-dcp)使用与配置指南
|
|
|
11
11
|
|
|
12
12
|
## 配置入口(两条路)
|
|
13
13
|
|
|
14
|
-
1. **会话内临时调参**:`/dcp set <键>
|
|
14
|
+
1. **会话内临时调参**:`/dcp set <键> <值>`,只影响当前会话,重启失效。十二个可调键:
|
|
15
15
|
`dedup` `purgeErrors` `maxItems` `maxItemChars` `maxSummaryTokens` `language`
|
|
16
|
-
`tokenEstimate` `thresholdRatio` `roundInterval` `notice
|
|
16
|
+
`tokenEstimate` `thresholdRatio` `roundInterval` `notice` `onModelSwitch`
|
|
17
|
+
`modelSwitchMinTokens`。
|
|
17
18
|
2. **持久化**:cordis.patch.yml 里 dsh-dcp 挂载块的 `config:` 段。用
|
|
18
19
|
`npx dsh-dcp-setup` 写入并维护(带 marker 注释、改动前日期备份、幂等);
|
|
19
20
|
`--remove` 只删 setup 写的块,手工写的块不受影响。bundle 方式
|
|
@@ -48,6 +49,8 @@ dsh-dcp 自有键(除 `thresholdRatio` 外全部可用 `/dcp set` 调):
|
|
|
48
49
|
| `protectedTools` | `['write', 'edit', 'apply_patch']` | 写侧工具(子串匹配)的重复调用不折叠进 dedup 标注 |
|
|
49
50
|
| `roundInterval` | 50 | 每 N 条 assistant message(一次 LLM 往返)触发一次压缩;`0` 关闭 |
|
|
50
51
|
| `notice` | `true` | 压缩后在会话追加一行通知 |
|
|
52
|
+
| `onModelSwitch` | `notice` | 模型切换后:`notice` 提醒执行 `/dcp compact`;`auto` 下一个空闲点自动压缩;`off` 关闭 |
|
|
53
|
+
| `modelSwitchMinTokens` | 32768 | 模型切换提醒/自动压缩的上下文下限(宿主 tokenMeter 实测):不足则忽略该次切换;`0` 关闭此门 |
|
|
51
54
|
|
|
52
55
|
转发上游 compaction-basic 的策略键:`thresholdRatio`(上游默认 0.8;**本插件 bundle 挂载默认 0.7**,中文场景建议 0.7)、`retainRatio`、`retainTokens`、`maxTokens`、`summarizationProvider`、`summarizationModel`、`compactionRetries`、`maxOverflowRetries`、`modelPolicies`、`auto`。
|
|
53
56
|
|
|
@@ -63,21 +66,25 @@ dsh-dcp 自有键(除 `thresholdRatio` 外全部可用 `/dcp set` 调):
|
|
|
63
66
|
3. **摘要语言** → `language: en|zh`。
|
|
64
67
|
4. **token 计价** → `tokenEstimate: cjk|ascii`。
|
|
65
68
|
5. **通知行** → `notice: on|off`。
|
|
69
|
+
6. **模型切换行为**:问用户切模型后想怎样——只提醒(默认 `notice`)/
|
|
70
|
+
自动压缩(`auto`,省心但切换频繁时会多次重写历史)/ 不管(`off`)。
|
|
66
71
|
|
|
67
72
|
流程:先用 `/dcp set <键> <值>` 在会话内试效果,满意后再代写持久 config——
|
|
68
73
|
直接改 cordis.patch.yml 里 dsh-dcp 挂载块的 `config:` 段(setup 写的块可原位改,marker 保留)。
|
|
69
74
|
|
|
70
|
-
##
|
|
75
|
+
## 触发条件(五类)
|
|
71
76
|
|
|
72
77
|
| 触发 | 时机 | 说明 |
|
|
73
78
|
|---|---|---|
|
|
74
79
|
| 压力 | 每步请求前 | token ≥ `thresholdRatio` × 上下文窗口 |
|
|
75
80
|
| 溢出 | 模型报 context 超限 | 继承官方恢复流程 |
|
|
76
81
|
| 轮数 | 每累计 `roundInterval` 条 assistant message | 任何一次压缩(含压力/手动)都重置时钟;`0` 关闭;需保持 `auto: true`(默认开) |
|
|
82
|
+
| 模型切换 | 会话实际路由的 provider/model 变化 | `onModelSwitch` 控制(默认 `notice` 提醒);`auto` 在下一个空闲点自动压缩;两道门:距上次压缩 <10 条消息、上下文 <`modelSwitchMinTokens` |
|
|
77
83
|
| 手动 | `/dcp compact`、`/compact` | 随时可用 |
|
|
78
84
|
|
|
79
85
|
- **subagent 同样生效**:进程内子代理(含 continuable 与 one-shot)走同一套事件分发,
|
|
80
|
-
|
|
86
|
+
压力/溢出/轮数/模型切换对每个会话独立计数、独立触发。
|
|
87
|
+
- **两类通知行相互独立**:压缩通知行受 `notice` 管,模型切换提醒行受 `onModelSwitch` 管。
|
|
81
88
|
- `notice` 通知行本身也是上下文(每次压缩约 15–25 tokens);`notice: false` 可关。
|
|
82
89
|
|
|
83
90
|
## 排障
|