@aiwayds/dsh-dcp 0.3.1 → 0.4.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
@@ -79,7 +79,7 @@ A checkpoint produced on a real session (Chinese content kept verbatim):
79
79
  `compaction-basic`
80
80
  - **Things dsh already does, deliberately not re-implemented**:
81
81
  - tool-result pruning (`compaction-tool-result-pruner`, deterministic by size)
82
- - trigger policy, retained tail, overflow recovery (inherited from official)
82
+ - trigger policy, retained tail, overflow recovery (inherited from official; this plugin only adds the round-interval trigger, see below)
83
83
  - `/compact` command, UI checkpoint cards (shipped with dsh)
84
84
 
85
85
  ## Install
@@ -112,7 +112,20 @@ npx dsh-dcp-setup # safe: date-stamped backup → append-only
112
112
  | `/dcp set <k> <v>` | adjust a knob for this session, with a persist hint |
113
113
 
114
114
  Settable: `dedup`, `purgeErrors`, `maxItems`, `maxItemChars`,
115
- `maxSummaryTokens`, `language`, `tokenEstimate`, `thresholdRatio`.
115
+ `maxSummaryTokens`, `language`, `tokenEstimate`, `thresholdRatio`,
116
+ `roundInterval`, `notice`.
117
+
118
+ ## Triggers
119
+
120
+ | Trigger | When | Notes |
121
+ |---|---|---|
122
+ | Pressure | before every step | tokens ≥ `thresholdRatio` (default 0.7) × context window, inherited |
123
+ | Overflow recovery | on a provider context-window error | inherited |
124
+ | **Round interval** | every `roundInterval` completed turns | added by this plugin; one round = one successfully completed turn. **Default 50**: first compaction at turn 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` |
125
+ | Manual | `/dcp compact`, `/compact` | anytime |
126
+
127
+ - **Subagents are covered**: in-process subagents (including continuable children) dispatch through the same events, so pressure/overflow/round triggers count and fire per child session independently.
128
+ - **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).
116
129
 
117
130
  ## Configuration
118
131
 
@@ -120,7 +133,9 @@ All optional, defaults work out of the box:
120
133
 
121
134
  | Key | Default | Meaning |
122
135
  |---|---|---|
123
- | `thresholdRatio` | 0.7 | compaction trigger; 0.7 recommended for CJK-heavy sessions |
136
+ | `thresholdRatio` | 0.7 | pressure trigger; 0.7 recommended for CJK-heavy sessions |
137
+ | `roundInterval` | 50 | compact every N completed turns (0 disables). Default 50: 50, 100, 150… — the clock restarts after every compaction |
138
+ | `notice` | `true` | append the one-line compaction notice to the session |
124
139
  | `language` | `zh` | summary language; `zh` also enables Chinese error/"待办:" detection |
125
140
  | `tokenEstimate` | `cjk` | CJK (zh/ja/ko/full-width) at ~2 chars/token; `ascii` matches the host |
126
141
  | `dedup` | `true` | annotate repeated tool calls |
@@ -136,7 +151,7 @@ All optional, defaults work out of the box:
136
151
  ## Development
137
152
 
138
153
  ```bash
139
- npm install && npm test # 45 tests: extractor/compaction/command/config/setup
154
+ npm install && npm test # 61 tests: extractor/compaction/command/config/triggers/setup
140
155
  ```
141
156
 
142
157
  ## License
package/README.md CHANGED
@@ -61,7 +61,7 @@ dsh 默认的压缩(`compaction-basic`)每次压缩都要让模型把旧对
61
61
  - **不做语义归纳**:不"理解"代码,只保留"出现过的事实"。需要深度语义摘要的场景,请继续用官方 `compaction-basic`
62
62
  - **dsh 已经有的我们不重复做**:
63
63
  - 工具结果剪枝(`compaction-tool-result-pruner`,确定性按大小剪)
64
- - 触发策略、保留尾巴、溢出恢复(直接继承官方)
64
+ - 触发策略、保留尾巴、溢出恢复(直接继承官方;本插件仅新增轮数触发,见上)
65
65
  - `/compact` 命令、UI 检查点卡片(dsh 自带)
66
66
 
67
67
  ## 安装
@@ -90,7 +90,19 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
90
90
  | `/dcp compact` | 立即压缩(零 LLM) |
91
91
  | `/dcp set <k> <v>` | 会话内调参,并提示如何持久化 |
92
92
 
93
- 可调键:`dedup`、`purgeErrors`、`maxItems`、`maxItemChars`、`maxSummaryTokens`、`language`、`tokenEstimate`、`thresholdRatio`。
93
+ 可调键:`dedup`、`purgeErrors`、`maxItems`、`maxItemChars`、`maxSummaryTokens`、`language`、`tokenEstimate`、`thresholdRatio`、`roundInterval`、`notice`。
94
+
95
+ ## 触发条件
96
+
97
+ | 触发 | 时机 | 说明 |
98
+ |---|---|---|
99
+ | 压力触发 | 每步请求前 | token ≥ `thresholdRatio`(默认 0.7)× 上下文窗口,继承官方 |
100
+ | 溢出恢复 | 模型报 context 超限时 | 继承官方 |
101
+ | **轮数触发** | 会话每完成 `roundInterval` 轮 | 本插件新增;一轮 = 一次正常完成的 turn。**默认 50**:第 50 轮触发第一次,之后每 50 轮一次(100、150……);任何一次压缩(含压力触发)都会重置轮数时钟。到达轮数后的第一个空闲点触发(阈值之下也压)。`0` 关闭;需保持 `auto: true`(默认开) |
102
+ | 手动 | `/dcp compact`、`/compact` | 随时可用 |
103
+
104
+ - **subagent 同样生效**:进程内 subagent(含 continuable 子代理)走同一套事件分发,压力/溢出/轮数触发对子会话独立计数、独立触发。
105
+ - **压缩可见性**:每次压缩成功后,会话里追加一行 `dcp: 已压缩 N 条历史(约 X tokens,触发方式)` 通知行(前端渲染为折叠行)。注意该行也会作为上下文随请求发给模型(每次压缩约 15–25 tokens),且 **0.4.0 起默认开启**;`notice: false` 可关闭。`/dcp` 的 stats 持续累计(压力触发的多次 region 提交各计一次)。
94
106
 
95
107
  ## 配置
96
108
 
@@ -98,7 +110,9 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
98
110
 
99
111
  | 键 | 默认 | 说明 |
100
112
  |---|---|---|
101
- | `thresholdRatio` | 0.7 | 触发阈值;中文场景建议 0.7 |
113
+ | `thresholdRatio` | 0.7 | 压力触发阈值;中文场景建议 0.7 |
114
+ | `roundInterval` | 50 | 每 N 轮触发一次压缩(0 关闭)。默认 50:50、100、150……每次压缩后重数 |
115
+ | `notice` | `true` | 压缩后在会话中追加一行通知 |
102
116
  | `language` | `zh` | 摘要语言;`zh` 额外识别中文报错和"待办:" |
103
117
  | `tokenEstimate` | `cjk` | CJK(中/日/韩/全角)按 ~2 字符/token 计价;`ascii` 与宿主一致 |
104
118
  | `dedup` | `true` | 标注重复工具调用 |
@@ -114,7 +128,7 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
114
128
  ## 开发
115
129
 
116
130
  ```bash
117
- npm install && npm test # 45 个用例:抽取/压缩/命令/配置/安装脚本
131
+ npm install && npm test # 61 个用例:抽取/压缩/命令/配置/触发/安装脚本
118
132
  ```
119
133
 
120
134
  ## License
package/lib/command.js CHANGED
@@ -14,7 +14,7 @@ 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)`
17
+ tokenEstimate, thresholdRatio, roundInterval, notice)`
18
18
 
19
19
  const FAILURE_TEXT = Object.freeze({
20
20
  busy: 'Compaction is unavailable because this process has an active compaction, or the agent is not idle.',
@@ -63,6 +63,11 @@ function applySet(engine, key, rawValue) {
63
63
  engine.config = Object.freeze({ ...engine.config, thresholdRatio: numeric })
64
64
  return `thresholdRatio = ${numeric} (this session)`
65
65
  }
66
+ if (kind === 'nonnegative-integer') {
67
+ if (!Number.isInteger(numeric) || numeric < 0) return `${key} expects 0 or a positive integer`
68
+ engine.dcp[key] = numeric
69
+ return `${key} = ${numeric}${numeric === 0 ? ' (disabled)' : ''} (this session)`
70
+ }
66
71
  if (!Number.isInteger(numeric) || numeric < 1) return `${key} expects a positive integer`
67
72
  engine.dcp[key] = numeric
68
73
  return `${key} = ${numeric} (this session)`
@@ -73,7 +78,7 @@ function statusText(engine, version) {
73
78
  const stats = engine.dcpStats
74
79
  const lines = [
75
80
  `dsh-dcp ${version} — deterministic compaction backend (zero LLM summarization calls)`,
76
- `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}`,
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}`,
77
82
  `stats: ${stats.compactions} compaction${stats.compactions === 1 ? '' : 's'}, ~${stats.shadowedTokens} tokens shadowed, ${stats.compactions} LLM summary call${stats.compactions === 1 ? '' : 's'} avoided`,
78
83
  ]
79
84
  if (stats.lastAt !== null) lines.push(`last compaction: ${new Date(stats.lastAt).toLocaleString()}`)
package/lib/config.js CHANGED
@@ -16,6 +16,8 @@ export const DCP_CONFIG_KEYS = [
16
16
  'language',
17
17
  'tokenEstimate',
18
18
  'protectedTools',
19
+ 'roundInterval',
20
+ 'notice',
19
21
  ]
20
22
 
21
23
  /** compaction-basic policy keys forwarded to the parent engine verbatim. */
@@ -41,6 +43,8 @@ const DEFAULTS = Object.freeze({
41
43
  language: 'en',
42
44
  tokenEstimate: 'cjk',
43
45
  protectedTools: Object.freeze(['write', 'edit', 'apply_patch']),
46
+ roundInterval: 50,
47
+ notice: true,
44
48
  })
45
49
 
46
50
  /**
@@ -97,6 +101,15 @@ export function resolveDcpConfig(raw = {}) {
97
101
  throw new Error('DcpConfig: protectedTools must be an array of non-empty strings')
98
102
  }
99
103
  }
104
+ if (raw.roundInterval !== undefined) {
105
+ const value = raw.roundInterval
106
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
107
+ throw new Error('DcpConfig: roundInterval must be a non-negative integer (0 disables the round trigger)')
108
+ }
109
+ }
110
+ if (raw.notice !== undefined && typeof raw.notice !== 'boolean') {
111
+ throw new Error('DcpConfig: notice must be a boolean')
112
+ }
100
113
  return Object.freeze({ ...DEFAULTS, ...raw })
101
114
  }
102
115
 
@@ -110,4 +123,6 @@ export const RUNTIME_SETTABLE = Object.freeze({
110
123
  language: 'language',
111
124
  tokenEstimate: 'token-estimate',
112
125
  thresholdRatio: 'ratio',
126
+ roundInterval: 'nonnegative-integer',
127
+ notice: 'boolean',
113
128
  })
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
  * name: /absolute/path/to/dsh-dcp/lib/index.js
21
21
  * config:
22
22
  * thresholdRatio: 0.7 # optional; every key is optional
23
+ * roundInterval: 100 # optional; also compact every N completed turns (default 50)
23
24
  * ```
24
25
  *
25
26
  * @module dsh-dcp
@@ -29,8 +30,10 @@ import { createRequire } from 'node:module'
29
30
  import { fileURLToPath } from 'node:url'
30
31
  import z from '@deepseek-ai/schemastery'
31
32
  import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
33
+ import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm'
34
+ import { ManualCompactionError } from '@deepseek-ai/dsh-compaction'
32
35
  import { splitConfig, resolveDcpConfig } from './config.js'
33
- import { summarizeDeterministically } from './summarizer.js'
36
+ import { summarizeDeterministically, noticeText } from './summarizer.js'
34
37
  import { registerDcpCommand } from './command.js'
35
38
 
36
39
  const require = createRequire(import.meta.url)
@@ -78,6 +81,8 @@ export class DcpEngine extends BasicCompactionEngine {
78
81
  language: z.string(),
79
82
  tokenEstimate: z.string(),
80
83
  protectedTools: z.array(z.string()),
84
+ roundInterval: z.number().step(1).min(0),
85
+ notice: z.boolean(),
81
86
  })
82
87
 
83
88
  /** Resolved dcp knobs; mutable at runtime through `/dcp set`. */
@@ -89,6 +94,24 @@ export class DcpEngine extends BasicCompactionEngine {
89
94
  /** Absolute module path, echoed by `/dcp set` for persistence snippets. */
90
95
  pluginPath
91
96
 
97
+ /**
98
+ * Completed-turn counters since the last dsh-dcp compaction, per session —
99
+ * one "round" is one `turn/end` whose reason is `completed`. Weak keys:
100
+ * disposed sessions (including one-shot subagents) drop out with the object.
101
+ */
102
+ #rounds = new WeakMap()
103
+
104
+ /**
105
+ * Sessions whose next `compactNow` is a round-interval trigger rather than
106
+ * a manual command. Only {@link DcpEngine.#maybeRoundCompact} writes;
107
+ * `compactNow` reads and clears its own entry, so a stale marker can only
108
+ * turn a manual compaction into a labeled `'round'` one — never the reverse.
109
+ */
110
+ #triggerLabels = new WeakMap()
111
+
112
+ /** Sessions with a round-triggered compaction still in flight. */
113
+ #roundInFlight = new WeakSet()
114
+
92
115
  constructor(ctx, config = {}) {
93
116
  const { basic, dcp } = splitConfig(config)
94
117
  super(ctx, basic)
@@ -99,6 +122,58 @@ export class DcpEngine extends BasicCompactionEngine {
99
122
  ctx.effect(function* () {
100
123
  yield registerDcpCommand(ctx, engine, VERSION)
101
124
  }, 'dsh-dcp /dcp command lifecycle')
125
+ if (this.config.auto) this.#registerRoundTrigger()
126
+ }
127
+
128
+ /**
129
+ * Round-interval trigger: count completed turns per session and, once the
130
+ * configured `roundInterval` is reached, compact at the agent's next idle
131
+ * boundary through the manual-compaction seam (`compactNow`). In-process
132
+ * subagents run through the same session/event and agent/status dispatch,
133
+ * so continuable children are covered exactly like the top-level session.
134
+ */
135
+ #registerRoundTrigger() {
136
+ const { ctx } = this
137
+ ctx.on('session/event', (session, event) => {
138
+ // Counting is skipped while disabled, but the listener stays registered
139
+ // so `/dcp set roundInterval N` can arm it again at runtime.
140
+ if (!this.dcp.roundInterval) return
141
+ if (event.type !== 'turn/end' || event.data.reason?.kind !== 'completed') return
142
+ this.#rounds.set(session, (this.#rounds.get(session) ?? 0) + 1)
143
+ })
144
+ ctx.on('agent/status', ({ agent, status }) => {
145
+ if (status === 'idle') this.#maybeRoundCompact(agent)
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Attempt one round-triggered compaction for an idle agent. The attempt is
151
+ * single-flight per session. `busy` (queued waking work won the race) and
152
+ * `cancelled` (the agent was interrupted mid-compaction) both keep the
153
+ * accumulated rounds for the next idle boundary — cancelling one attempt
154
+ * must not cancel the N completed turns behind it. Any other failure warns
155
+ * and releases the boundary: the pressure trigger remains the safety net.
156
+ */
157
+ #maybeRoundCompact(agent) {
158
+ const interval = this.dcp.roundInterval
159
+ if (!interval) return
160
+ const session = agent?.session
161
+ if (session === undefined || this.#roundInFlight.has(session)) return
162
+ if ((this.#rounds.get(session) ?? 0) < interval) return
163
+ this.#triggerLabels.set(session, 'round')
164
+ this.#roundInFlight.add(session)
165
+ const settle = () => this.#roundInFlight.delete(session)
166
+ const consume = () => {
167
+ this.#rounds.delete(session)
168
+ settle()
169
+ }
170
+ void this.compactNow(agent, new AbortController().signal).then(consume, (error) => {
171
+ if (error instanceof ManualCompactionError && (error.code === 'busy' || error.code === 'cancelled')) {
172
+ return settle()
173
+ }
174
+ this.ctx.logger.warn(`round-interval compaction failed: ${error instanceof Error ? error.message : String(error)}`)
175
+ consume()
176
+ })
102
177
  }
103
178
 
104
179
  /**
@@ -116,13 +191,92 @@ export class DcpEngine extends BasicCompactionEngine {
116
191
  }
117
192
  }
118
193
 
119
- /** Count every committed compaction (automatic and manual) for `/dcp`. */
194
+ /**
195
+ * Automatic triggers (pressure, overflow): every committed region is
196
+ * recorded through `compactRegion`, and ONE notice row is emitted per
197
+ * trigger event — the parent's retry loop may commit several regions
198
+ * before landing below the threshold, and stacking a near-duplicate row
199
+ * per region is noise, while the stats below count each real compaction.
200
+ */
201
+ async compactIfNeeded(agent, trigger, signal) {
202
+ const label = trigger === 'context-overflow' ? 'overflow' : 'auto'
203
+ const result = await super.compactIfNeeded(agent, trigger, signal)
204
+ if (result !== null) this.#appendNotice(agent.session, result, label)
205
+ return result
206
+ }
207
+
208
+ /**
209
+ * Manual seam (`/dcp compact`, `/compact`) and this plugin's own
210
+ * round-interval trigger — the parent's `compactNow` bypasses
211
+ * `compactRegion` (it drives `compactSurfaceRegion` directly), so without
212
+ * this override the manual path would miss stats and the transcript notice.
213
+ */
214
+ async compactNow(agent, signal, sourceCommandId) {
215
+ const session = agent.session
216
+ const trigger = this.#triggerLabels.get(session) === 'round' ? 'round' : 'manual'
217
+ try {
218
+ const result = await super.compactNow(agent, signal, sourceCommandId)
219
+ // `null` means no useful range existed: release the round counter so an
220
+ // early-session interval boundary cannot retry every idle boundary.
221
+ // Manual compactions share the release: a user-driven compact restarts
222
+ // interval counting whether or not it found anything to compact.
223
+ if (result === null) this.#rounds.delete(session)
224
+ else this.recordCompaction(session, result, trigger)
225
+ return result
226
+ } finally {
227
+ this.#triggerLabels.delete(session)
228
+ }
229
+ }
230
+
231
+ /** Stats and round-counter restart for every committed region. */
120
232
  async compactRegion(start, end, agent, signal) {
121
233
  const result = await super.compactRegion(start, end, agent, signal)
234
+ this.#recordStats(agent.session, result)
235
+ return result
236
+ }
237
+
238
+ /**
239
+ * Record one committed compaction: bump the `/dcp` counters and restart the
240
+ * round-interval counting.
241
+ */
242
+ #recordStats(session, result) {
122
243
  this.dcpStats.compactions += 1
123
244
  this.dcpStats.shadowedTokens += result.shadowedTokenCount
124
245
  this.dcpStats.lastAt = Date.now()
125
- return result
246
+ this.#rounds.delete(session)
247
+ }
248
+
249
+ /**
250
+ * Append the one-line notice row for one committed compaction. The notice
251
+ * is a `notice`-form plugin message, so every dsh frontend renders it as a
252
+ * collapsed transcript row, live and on replay.
253
+ */
254
+ #appendNotice(session, result, trigger) {
255
+ if (!this.dcp.notice) return
256
+ const summary = boundContextSummary(noticeText(this.dcp.language, result.shadowedSeqs.length, result.shadowedTokenCount, trigger))
257
+ try {
258
+ session.append('user/message', createUserMessage({
259
+ content: [{ type: 'text', text: summary }],
260
+ source: { kind: 'plugin', plugin: 'dsh-dcp', form: 'notice', summary },
261
+ }))
262
+ } catch (error) {
263
+ // The compaction already committed; a display-row failure must never
264
+ // surface as a compaction failure.
265
+ const message = error instanceof Error ? error.message : String(error)
266
+ this.ctx.logger.warn(`dsh-dcp notice append failed: ${message}`)
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Record stats and append the notice row for one `compactNow`-path
272
+ * compaction.
273
+ *
274
+ * Internal seam: `compactNow` calls it after its durable commit; tests
275
+ * drive it directly instead of mocking the upstream region machinery.
276
+ */
277
+ recordCompaction(session, result, trigger) {
278
+ this.#recordStats(session, result)
279
+ this.#appendNotice(session, result, trigger)
126
280
  }
127
281
  }
128
282
 
package/lib/summarizer.js CHANGED
@@ -132,6 +132,7 @@ const i18n = Object.freeze({
132
132
  contextNote: '{messages} messages / {calls} tool calls compacted deterministically by dsh-dcp (no LLM summarization call)',
133
133
  carried: 'carried from prior checkpoint',
134
134
  terseHeader: '{messages} messages ({calls} tool calls) compacted deterministically by dsh-dcp.',
135
+ notice: 'dcp: compacted {items} history items (~{tokens} tokens, {trigger})',
135
136
  }),
136
137
  zh: Object.freeze({
137
138
  none: '(无)',
@@ -141,9 +142,18 @@ const i18n = Object.freeze({
141
142
  contextNote: 'dsh-dcp 确定性压缩了 {messages} 条消息 / {calls} 次工具调用(未调用 LLM 摘要)',
142
143
  carried: '继承自上一次压缩检查点',
143
144
  terseHeader: 'dsh-dcp 确定性压缩了 {messages} 条消息({calls} 次工具调用)。',
145
+ notice: 'dcp: 已压缩 {items} 条历史(约 {tokens} tokens,{trigger})',
144
146
  }),
145
147
  })
146
148
 
149
+ /** One-line compaction account for the transcript notice row. */
150
+ export function noticeText(language, items, tokens, trigger = 'auto') {
151
+ return i18n[language]?.notice
152
+ .replace('{items}', String(items))
153
+ .replace('{tokens}', String(tokens))
154
+ .replace('{trigger}', trigger) ?? ''
155
+ }
156
+
147
157
  /** Collapse whitespace and hard-cap one item's length with an ellipsis. */
148
158
  export function clip(text, maxChars) {
149
159
  const flat = String(text).replace(/\s+/g, ' ').trim()
@@ -363,6 +373,9 @@ export function extractFacts(messages, language = 'en') {
363
373
  facts.carried = parseCheckpointSections(text)
364
374
  continue
365
375
  }
376
+ // dsh-dcp's own compaction notices are transcript display rows: the model
377
+ // learns nothing from them the checkpoint below does not already carry.
378
+ if (message.source?.kind === 'plugin' && message.source.plugin === 'dsh-dcp') continue
366
379
  if (isInjectedContext(message)) continue
367
380
  const intentText = noticeSummary(message) ?? text
368
381
  facts.lastUserText = intentText
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-dcp",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -20,7 +20,6 @@
20
20
  "README.md",
21
21
  "README.en.md",
22
22
  "LICENSE",
23
- "cordis.patch.example.yml",
24
23
  "cordis.patch.yml"
25
24
  ],
26
25
  "scripts": {
@@ -62,6 +61,20 @@
62
61
  "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
63
62
  "@deepseek-ai/schemastery": "^3.18.1"
64
63
  },
64
+ "peerDependenciesMeta": {
65
+ "@deepseek-ai/cordis": { "optional": true },
66
+ "@deepseek-ai/dsh-agent": { "optional": true },
67
+ "@deepseek-ai/dsh-brand": { "optional": true },
68
+ "@deepseek-ai/dsh-commands": { "optional": true },
69
+ "@deepseek-ai/dsh-compaction": { "optional": true },
70
+ "@deepseek-ai/dsh-compaction-basic": { "optional": true },
71
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": { "optional": true },
72
+ "@deepseek-ai/dsh-invariants": { "optional": true },
73
+ "@deepseek-ai/dsh-llm": { "optional": true },
74
+ "@deepseek-ai/dsh-session": { "optional": true },
75
+ "@deepseek-ai/dsh-token-meter": { "optional": true },
76
+ "@deepseek-ai/schemastery": { "optional": true }
77
+ },
65
78
  "devDependencies": {
66
79
  "@deepseek-ai/cordis": "^4.0.1",
67
80
  "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
@@ -1,19 +0,0 @@
1
- # Example: mount dsh-dcp as the compaction backend.
2
- #
3
- # Append to ~/.dsh/cordis.patch.yml (home level, applies to every profile).
4
- # The row id stays `compaction-basic` (patch overrides by id), so the line
5
- # remains inside the standard preset's compaction isolate group and every
6
- # consumer (/compact, UI checkpoint card, pressure triggers) keeps working.
7
- #
8
- # `name` MUST be an absolute path to this checkout's entry file; relative
9
- # paths resolve against the loader's baseUrl and are easy to get wrong.
10
- - id: compaction-basic
11
- name: /Users/CHANGE/ME/dsh-dcp/lib/index.js
12
- config:
13
- thresholdRatio: 0.7 # optional: trigger earlier for CJK-heavy sessions
14
- language: zh # optional: filler text language (content is verbatim)
15
- # dedup: true # optional: every dsh-dcp key may be omitted
16
- # purgeErrors: true
17
- # maxItems: 10
18
- # maxItemChars: 200
19
- # maxSummaryTokens: 2048