@aiwayds/dsh-dcp 0.4.0 → 0.5.1
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 +15 -6
- package/README.md +10 -6
- package/lib/command.js +16 -0
- package/lib/index.js +108 -48
- package/package.json +37 -13
package/README.en.md
CHANGED
|
@@ -115,16 +115,23 @@ Settable: `dedup`, `purgeErrors`, `maxItems`, `maxItemChars`,
|
|
|
115
115
|
`maxSummaryTokens`, `language`, `tokenEstimate`, `thresholdRatio`,
|
|
116
116
|
`roundInterval`, `notice`.
|
|
117
117
|
|
|
118
|
+
The `/dcp` status also lists every session that has compacted (subagents
|
|
119
|
+
included): `per-session: session-1 (2 compactions, ~444 tokens), child
|
|
120
|
+
(1 compaction, ~22 tokens)`. Compactions count per session; disposed
|
|
121
|
+
sessions (one-shot subagents included) fall out of the overview
|
|
122
|
+
automatically, and the list is capped at the first 10 sessions (`+N more`
|
|
123
|
+
for the rest) so the status stays one line.
|
|
124
|
+
|
|
118
125
|
## Triggers
|
|
119
126
|
|
|
120
127
|
| Trigger | When | Notes |
|
|
121
128
|
|---|---|---|
|
|
122
|
-
| Pressure | before every step | tokens ≥ `thresholdRatio` (default 0.7) × context window
|
|
129
|
+
| Pressure | before every step | tokens ≥ `thresholdRatio` (inherited upstream default 0.8; this plugin's bundle mounts 0.7 — see config table) × context window |
|
|
123
130
|
| Overflow recovery | on a provider context-window error | inherited |
|
|
124
|
-
| **Round interval** | every `roundInterval`
|
|
131
|
+
| **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` |
|
|
125
132
|
| Manual | `/dcp compact`, `/compact` | anytime |
|
|
126
133
|
|
|
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.
|
|
134
|
+
- **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.
|
|
128
135
|
- **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).
|
|
129
136
|
|
|
130
137
|
## Configuration
|
|
@@ -133,8 +140,8 @@ All optional, defaults work out of the box:
|
|
|
133
140
|
|
|
134
141
|
| Key | Default | Meaning |
|
|
135
142
|
|---|---|---|
|
|
136
|
-
| `thresholdRatio` | 0.
|
|
137
|
-
| `roundInterval` | 50 | compact every N
|
|
143
|
+
| `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) |
|
|
144
|
+
| `roundInterval` | 50 | compact every N assistant messages (one LLM roundtrip) (0 disables). Default 50: 50, 100, 150… — the clock restarts after every compaction |
|
|
138
145
|
| `notice` | `true` | append the one-line compaction notice to the session |
|
|
139
146
|
| `language` | `zh` | summary language; `zh` also enables Chinese error/"待办:" detection |
|
|
140
147
|
| `tokenEstimate` | `cjk` | CJK (zh/ja/ko/full-width) at ~2 chars/token; `ascii` matches the host |
|
|
@@ -143,6 +150,8 @@ All optional, defaults work out of the box:
|
|
|
143
150
|
| `maxItems` / `maxItemChars` | 10 / 200 | summary density |
|
|
144
151
|
| `maxSummaryTokens` | 2048 | summary token budget |
|
|
145
152
|
|
|
153
|
+
> **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).
|
|
154
|
+
|
|
146
155
|
## Design reference
|
|
147
156
|
|
|
148
157
|
- [Opencode-DCP/opencode-dynamic-context-pruning](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)
|
|
@@ -151,7 +160,7 @@ All optional, defaults work out of the box:
|
|
|
151
160
|
## Development
|
|
152
161
|
|
|
153
162
|
```bash
|
|
154
|
-
npm install && npm test #
|
|
163
|
+
npm install && npm test # 65 tests: extractor/compaction/command/config/triggers/setup
|
|
155
164
|
```
|
|
156
165
|
|
|
157
166
|
## License
|
package/README.md
CHANGED
|
@@ -92,16 +92,18 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
|
|
|
92
92
|
|
|
93
93
|
可调键:`dedup`、`purgeErrors`、`maxItems`、`maxItemChars`、`maxSummaryTokens`、`language`、`tokenEstimate`、`thresholdRatio`、`roundInterval`、`notice`。
|
|
94
94
|
|
|
95
|
+
`/dcp` 状态还会列出每个发生过压缩的会话(per-session 概览,含子代理),例如 `per-session: session-1 (2 compactions, ~444 tokens), child (1 compaction, ~22 tokens)`。压缩按会话独立计数;已销毁的会话(含 one-shot 子代理)自动从概览消失;列表封顶一行(最多前 10 个会话,超出显示 `+N more`)。
|
|
96
|
+
|
|
95
97
|
## 触发条件
|
|
96
98
|
|
|
97
99
|
| 触发 | 时机 | 说明 |
|
|
98
100
|
|---|---|---|
|
|
99
|
-
| 压力触发 | 每步请求前 | token ≥ `thresholdRatio
|
|
101
|
+
| 压力触发 | 每步请求前 | token ≥ `thresholdRatio`(继承上游默认 0.8;本插件 bundle 挂载默认 0.7,见配置表)× 上下文窗口 |
|
|
100
102
|
| 溢出恢复 | 模型报 context 超限时 | 继承官方 |
|
|
101
|
-
| **轮数触发** |
|
|
103
|
+
| **轮数触发** | 会话每收到 `roundInterval` 条 assistant message | 本插件新增;一条 = 一次 LLM 往返(每轮工具迭代各算一条,one-shot 子代理也能触发)。**默认 50**:第 50 条后触发第一次,之后每 50 条一次(100、150……);任何一次压缩(含压力触发)都会重置轮数时钟。到达条数后的第一个空闲点触发(阈值之下也压)。`0` 关闭;需保持 `auto: true`(默认开) |
|
|
102
104
|
| 手动 | `/dcp compact`、`/compact` | 随时可用 |
|
|
103
105
|
|
|
104
|
-
- **subagent 同样生效**:进程内 subagent(含 continuable
|
|
106
|
+
- **subagent 同样生效**:进程内 subagent(含 continuable 与 one-shot 子代理)走同一套事件分发,压力/溢出/轮数触发对子会话独立计数、独立触发。轮数触发按 assistant message 计数,所以全程只有 1 个 turn 的 one-shot 子代理(多次工具迭代)也能触发。
|
|
105
107
|
- **压缩可见性**:每次压缩成功后,会话里追加一行 `dcp: 已压缩 N 条历史(约 X tokens,触发方式)` 通知行(前端渲染为折叠行)。注意该行也会作为上下文随请求发给模型(每次压缩约 15–25 tokens),且 **0.4.0 起默认开启**;`notice: false` 可关闭。`/dcp` 的 stats 持续累计(压力触发的多次 region 提交各计一次)。
|
|
106
108
|
|
|
107
109
|
## 配置
|
|
@@ -110,8 +112,8 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
|
|
|
110
112
|
|
|
111
113
|
| 键 | 默认 | 说明 |
|
|
112
114
|
|---|---|---|
|
|
113
|
-
| `thresholdRatio` | 0.
|
|
114
|
-
| `roundInterval` | 50 | 每 N
|
|
115
|
+
| `thresholdRatio` | 0.8 | 压力触发阈值(继承上游 compaction-basic 默认 0.8;本插件 bundle patch 挂载时默认 0.7,中文场景建议 0.7) |
|
|
116
|
+
| `roundInterval` | 50 | 每 N 条 assistant message(一次 LLM 往返)触发一次压缩(0 关闭)。默认 50:50、100、150……每次压缩后重数 |
|
|
115
117
|
| `notice` | `true` | 压缩后在会话中追加一行通知 |
|
|
116
118
|
| `language` | `zh` | 摘要语言;`zh` 额外识别中文报错和"待办:" |
|
|
117
119
|
| `tokenEstimate` | `cjk` | CJK(中/日/韩/全角)按 ~2 字符/token 计价;`ascii` 与宿主一致 |
|
|
@@ -120,6 +122,8 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
|
|
|
120
122
|
| `maxItems` / `maxItemChars` | 10 / 200 | 摘要密度 |
|
|
121
123
|
| `maxSummaryTokens` | 2048 | 摘要 token 预算 |
|
|
122
124
|
|
|
125
|
+
> **升级提示(0.5.0)**:`roundInterval` 的计数单位由 completed turn 改为 assistant message——同值下触发会更频繁(一个 turn 内往往有多条 assistant message)。
|
|
126
|
+
|
|
123
127
|
## 设计参考
|
|
124
128
|
|
|
125
129
|
- [Opencode-DCP/opencode-dynamic-context-pruning](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning)
|
|
@@ -128,7 +132,7 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
|
|
|
128
132
|
## 开发
|
|
129
133
|
|
|
130
134
|
```bash
|
|
131
|
-
npm install && npm test #
|
|
135
|
+
npm install && npm test # 65 个用例:抽取/压缩/命令/配置/触发/安装脚本
|
|
132
136
|
```
|
|
133
137
|
|
|
134
138
|
## License
|
package/lib/command.js
CHANGED
|
@@ -81,6 +81,22 @@ function statusText(engine, version) {
|
|
|
81
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}`,
|
|
82
82
|
`stats: ${stats.compactions} compaction${stats.compactions === 1 ? '' : 's'}, ~${stats.shadowedTokens} tokens shadowed, ${stats.compactions} LLM summary call${stats.compactions === 1 ? '' : 's'} avoided`,
|
|
83
83
|
]
|
|
84
|
+
// Per-session dimension: one compacted session per entry, in store
|
|
85
|
+
// (creation) order. Bounded to one line so a long-lived host with many
|
|
86
|
+
// compacted children never floods the status.
|
|
87
|
+
const perSession = engine.sessionStatsOverview()
|
|
88
|
+
if (perSession.length > 0) {
|
|
89
|
+
const MAX_SESSIONS = 10
|
|
90
|
+
const shown = perSession.slice(0, MAX_SESSIONS)
|
|
91
|
+
const rest = perSession.length - shown.length
|
|
92
|
+
const items = shown.map(({ id, compactions, shadowedTokens }) => {
|
|
93
|
+
const unit = compactions === 1 ? 'compaction' : 'compactions'
|
|
94
|
+
const tokens = shadowedTokens > 0 ? `, ~${shadowedTokens} tokens` : ''
|
|
95
|
+
return `${id} (${compactions} ${unit}${tokens})`
|
|
96
|
+
})
|
|
97
|
+
if (rest > 0) items.push(`+${rest} more`)
|
|
98
|
+
lines.push(`per-session: ${items.join(', ')}`)
|
|
99
|
+
}
|
|
84
100
|
if (stats.lastAt !== null) lines.push(`last compaction: ${new Date(stats.lastAt).toLocaleString()}`)
|
|
85
101
|
return lines.join('\n')
|
|
86
102
|
}
|
package/lib/index.js
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* - id: compaction-basic
|
|
20
20
|
* name: /absolute/path/to/dsh-dcp/lib/index.js
|
|
21
21
|
* config:
|
|
22
|
-
* thresholdRatio: 0.7 # optional; every key is optional
|
|
23
|
-
* roundInterval: 100 # optional; also compact every N
|
|
22
|
+
* thresholdRatio: 0.7 # optional; 0.7 = this bundle patch's mount value, package default is 0.8; every key is optional
|
|
23
|
+
* roundInterval: 100 # optional; also compact every N assistant messages, one per LLM roundtrip (default 50)
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
26
|
* @module dsh-dcp
|
|
@@ -39,6 +39,28 @@ import { registerDcpCommand } from './command.js'
|
|
|
39
39
|
const require = createRequire(import.meta.url)
|
|
40
40
|
const { version: VERSION } = require('../package.json')
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Per-engine state uses module-scoped symbol keys, never `#private` members.
|
|
44
|
+
* Cordis hands services to consuming fibers through derived receivers —
|
|
45
|
+
* `ctx.mixin` binds methods to a withProps proxy and `createTraceable` calls
|
|
46
|
+
* them on Object.create-derived shadows — so `/compact` and `/dcp compact`
|
|
47
|
+
* (both `ctx.compaction.compactNow(...)`) routinely execute engine methods
|
|
48
|
+
* with a `this` this class's constructor never initialized. Private members
|
|
49
|
+
* brand-check against exactly that and throw
|
|
50
|
+
* "Cannot read private member #triggerLabels from an object whose class did
|
|
51
|
+
* not declare it", while symbol lookups traverse those receivers to this
|
|
52
|
+
* instance's own state.
|
|
53
|
+
*/
|
|
54
|
+
const kRounds = Symbol('dsh-dcp.rounds')
|
|
55
|
+
const kTriggerLabels = Symbol('dsh-dcp.triggerLabels')
|
|
56
|
+
const kRoundInFlight = Symbol('dsh-dcp.roundInFlight')
|
|
57
|
+
const kSessionStats = Symbol('dsh-dcp.sessionStats')
|
|
58
|
+
const kRegisterRoundTrigger = Symbol('dsh-dcp.registerRoundTrigger')
|
|
59
|
+
const kMaybeRoundCompact = Symbol('dsh-dcp.maybeRoundCompact')
|
|
60
|
+
const kRecordStats = Symbol('dsh-dcp.recordStats')
|
|
61
|
+
const kRecordSessionStats = Symbol('dsh-dcp.recordSessionStats')
|
|
62
|
+
const kAppendNotice = Symbol('dsh-dcp.appendNotice')
|
|
63
|
+
|
|
42
64
|
/**
|
|
43
65
|
* Deterministic compaction engine: `summarize()` overridden, everything else
|
|
44
66
|
* inherited. Registers the `/dcp` command beside the inherited `/compact`.
|
|
@@ -94,27 +116,20 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
94
116
|
/** Absolute module path, echoed by `/dcp set` for persistence snippets. */
|
|
95
117
|
pluginPath
|
|
96
118
|
|
|
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
|
-
|
|
115
119
|
constructor(ctx, config = {}) {
|
|
116
120
|
const { basic, dcp } = splitConfig(config)
|
|
117
121
|
super(ctx, basic)
|
|
122
|
+
// Constructor assignments, not class fields: Node 26's V8 silently drops
|
|
123
|
+
// every symbol-keyed class field after the first one in a derived class
|
|
124
|
+
// (plain string-keyed and base-class fields are unaffected).
|
|
125
|
+
/** Per-session assistant-message counters since the last dsh-dcp compaction. */
|
|
126
|
+
this[kRounds] = new WeakMap()
|
|
127
|
+
/** Sessions whose next `compactNow` is a round-interval trigger, not manual. */
|
|
128
|
+
this[kTriggerLabels] = new WeakMap()
|
|
129
|
+
/** Sessions with a round-triggered compaction still in flight. */
|
|
130
|
+
this[kRoundInFlight] = new WeakSet()
|
|
131
|
+
/** Per-session compaction records for `/dcp` (weak so disposed sessions drop out). */
|
|
132
|
+
this[kSessionStats] = new WeakMap()
|
|
118
133
|
this.dcp = { ...resolveDcpConfig(dcp) }
|
|
119
134
|
this.dcpStats = { compactions: 0, shadowedTokens: 0, lastAt: null }
|
|
120
135
|
this.pluginPath = fileURLToPath(import.meta.url)
|
|
@@ -122,27 +137,32 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
122
137
|
ctx.effect(function* () {
|
|
123
138
|
yield registerDcpCommand(ctx, engine, VERSION)
|
|
124
139
|
}, 'dsh-dcp /dcp command lifecycle')
|
|
125
|
-
if (this.config.auto) this
|
|
140
|
+
if (this.config.auto) this[kRegisterRoundTrigger]()
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
/**
|
|
129
|
-
* Round-interval trigger: count
|
|
130
|
-
* configured `roundInterval` is reached, compact
|
|
131
|
-
* boundary through the manual-compaction seam
|
|
132
|
-
* subagents run through the same session/event
|
|
133
|
-
* so continuable children are covered exactly
|
|
144
|
+
* Round-interval trigger: count assistant messages (one per LLM roundtrip)
|
|
145
|
+
* per session and, once the configured `roundInterval` is reached, compact
|
|
146
|
+
* at the agent's next idle boundary through the manual-compaction seam
|
|
147
|
+
* (`compactNow`). In-process subagents run through the same session/event
|
|
148
|
+
* and agent/status dispatch, so continuable children are covered exactly
|
|
149
|
+
* like the top-level session — and because one-shot subagents emit many
|
|
150
|
+
* assistant messages inside a single turn, they now trigger too.
|
|
134
151
|
*/
|
|
135
|
-
|
|
152
|
+
[kRegisterRoundTrigger]() {
|
|
136
153
|
const { ctx } = this
|
|
137
154
|
ctx.on('session/event', (session, event) => {
|
|
138
155
|
// Counting is skipped while disabled, but the listener stays registered
|
|
139
156
|
// so `/dcp set roundInterval N` can arm it again at runtime.
|
|
140
157
|
if (!this.dcp.roundInterval) return
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
// Every assembled assistant message is one completed LLM roundtrip;
|
|
159
|
+
// counting it (instead of completed turns) also covers one-shot
|
|
160
|
+
// subagents, whose whole run is a single turn with many model calls.
|
|
161
|
+
if (event.type !== 'assistant/message') return
|
|
162
|
+
this[kRounds].set(session, (this[kRounds].get(session) ?? 0) + 1)
|
|
143
163
|
})
|
|
144
164
|
ctx.on('agent/status', ({ agent, status }) => {
|
|
145
|
-
if (status === 'idle') this
|
|
165
|
+
if (status === 'idle') this[kMaybeRoundCompact](agent)
|
|
146
166
|
})
|
|
147
167
|
}
|
|
148
168
|
|
|
@@ -151,20 +171,20 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
151
171
|
* single-flight per session. `busy` (queued waking work won the race) and
|
|
152
172
|
* `cancelled` (the agent was interrupted mid-compaction) both keep the
|
|
153
173
|
* accumulated rounds for the next idle boundary — cancelling one attempt
|
|
154
|
-
* must not cancel the N
|
|
174
|
+
* must not cancel the N assistant messages behind it. Any other failure warns
|
|
155
175
|
* and releases the boundary: the pressure trigger remains the safety net.
|
|
156
176
|
*/
|
|
157
|
-
|
|
177
|
+
[kMaybeRoundCompact](agent) {
|
|
158
178
|
const interval = this.dcp.roundInterval
|
|
159
179
|
if (!interval) return
|
|
160
180
|
const session = agent?.session
|
|
161
|
-
if (session === undefined || this
|
|
162
|
-
if ((this
|
|
163
|
-
this
|
|
164
|
-
this
|
|
165
|
-
const settle = () => this
|
|
181
|
+
if (session === undefined || this[kRoundInFlight].has(session)) return
|
|
182
|
+
if ((this[kRounds].get(session) ?? 0) < interval) return
|
|
183
|
+
this[kTriggerLabels].set(session, 'round')
|
|
184
|
+
this[kRoundInFlight].add(session)
|
|
185
|
+
const settle = () => this[kRoundInFlight].delete(session)
|
|
166
186
|
const consume = () => {
|
|
167
|
-
this
|
|
187
|
+
this[kRounds].delete(session)
|
|
168
188
|
settle()
|
|
169
189
|
}
|
|
170
190
|
void this.compactNow(agent, new AbortController().signal).then(consume, (error) => {
|
|
@@ -201,7 +221,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
201
221
|
async compactIfNeeded(agent, trigger, signal) {
|
|
202
222
|
const label = trigger === 'context-overflow' ? 'overflow' : 'auto'
|
|
203
223
|
const result = await super.compactIfNeeded(agent, trigger, signal)
|
|
204
|
-
if (result !== null) this
|
|
224
|
+
if (result !== null) this[kAppendNotice](agent.session, result, label)
|
|
205
225
|
return result
|
|
206
226
|
}
|
|
207
227
|
|
|
@@ -213,25 +233,25 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
213
233
|
*/
|
|
214
234
|
async compactNow(agent, signal, sourceCommandId) {
|
|
215
235
|
const session = agent.session
|
|
216
|
-
const trigger = this
|
|
236
|
+
const trigger = this[kTriggerLabels].get(session) === 'round' ? 'round' : 'manual'
|
|
217
237
|
try {
|
|
218
238
|
const result = await super.compactNow(agent, signal, sourceCommandId)
|
|
219
239
|
// `null` means no useful range existed: release the round counter so an
|
|
220
240
|
// early-session interval boundary cannot retry every idle boundary.
|
|
221
241
|
// Manual compactions share the release: a user-driven compact restarts
|
|
222
242
|
// interval counting whether or not it found anything to compact.
|
|
223
|
-
if (result === null) this
|
|
243
|
+
if (result === null) this[kRounds].delete(session)
|
|
224
244
|
else this.recordCompaction(session, result, trigger)
|
|
225
245
|
return result
|
|
226
246
|
} finally {
|
|
227
|
-
this
|
|
247
|
+
this[kTriggerLabels].delete(session)
|
|
228
248
|
}
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
/** Stats and round-counter restart for every committed region. */
|
|
232
252
|
async compactRegion(start, end, agent, signal) {
|
|
233
253
|
const result = await super.compactRegion(start, end, agent, signal)
|
|
234
|
-
this
|
|
254
|
+
this[kRecordStats](agent.session, result)
|
|
235
255
|
return result
|
|
236
256
|
}
|
|
237
257
|
|
|
@@ -239,11 +259,25 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
239
259
|
* Record one committed compaction: bump the `/dcp` counters and restart the
|
|
240
260
|
* round-interval counting.
|
|
241
261
|
*/
|
|
242
|
-
|
|
262
|
+
[kRecordStats](session, result) {
|
|
243
263
|
this.dcpStats.compactions += 1
|
|
244
264
|
this.dcpStats.shadowedTokens += result.shadowedTokenCount
|
|
245
265
|
this.dcpStats.lastAt = Date.now()
|
|
246
|
-
this
|
|
266
|
+
this[kRounds].delete(session)
|
|
267
|
+
this[kRecordSessionStats](session, result.shadowedTokenCount)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Accumulate one committed compaction against its session. WeakMap keys
|
|
272
|
+
* must be objects, so a session-less recording (should not happen) is
|
|
273
|
+
* skipped rather than thrown on.
|
|
274
|
+
*/
|
|
275
|
+
[kRecordSessionStats](session, shadowedTokenCount) {
|
|
276
|
+
if (session === undefined || session === null || typeof session !== 'object') return
|
|
277
|
+
const entry = this[kSessionStats].get(session) ?? { compactions: 0, shadowedTokens: 0 }
|
|
278
|
+
entry.compactions += 1
|
|
279
|
+
entry.shadowedTokens += shadowedTokenCount
|
|
280
|
+
this[kSessionStats].set(session, entry)
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
/**
|
|
@@ -251,7 +285,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
251
285
|
* is a `notice`-form plugin message, so every dsh frontend renders it as a
|
|
252
286
|
* collapsed transcript row, live and on replay.
|
|
253
287
|
*/
|
|
254
|
-
|
|
288
|
+
[kAppendNotice](session, result, trigger) {
|
|
255
289
|
if (!this.dcp.notice) return
|
|
256
290
|
const summary = boundContextSummary(noticeText(this.dcp.language, result.shadowedSeqs.length, result.shadowedTokenCount, trigger))
|
|
257
291
|
try {
|
|
@@ -273,10 +307,36 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
273
307
|
*
|
|
274
308
|
* Internal seam: `compactNow` calls it after its durable commit; tests
|
|
275
309
|
* drive it directly instead of mocking the upstream region machinery.
|
|
310
|
+
* Public-named but not part of the dsh compaction contract.
|
|
276
311
|
*/
|
|
277
312
|
recordCompaction(session, result, trigger) {
|
|
278
|
-
this
|
|
279
|
-
this
|
|
313
|
+
this[kRecordStats](session, result)
|
|
314
|
+
this[kAppendNotice](session, result, trigger)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Per-session compaction overview for `/dcp`: one entry per live session
|
|
319
|
+
* that has recorded at least one compaction. The WeakMap is not enumerable
|
|
320
|
+
* and must never retain sessions, so this walks the sessions service and
|
|
321
|
+
* looks each live session up. Disposed sessions (including one-shot
|
|
322
|
+
* subagents) fall out of the store, and their records with them.
|
|
323
|
+
*
|
|
324
|
+
* @returns {Array<{id: string, compactions: number, shadowedTokens: number}>}
|
|
325
|
+
*/
|
|
326
|
+
sessionStatsOverview() {
|
|
327
|
+
const overview = []
|
|
328
|
+
// Services live on ctx for a cordis plugin instance (`inject` only
|
|
329
|
+
// declares them); this.sessions is undefined in production.
|
|
330
|
+
for (const session of this.ctx.sessions?.list?.() ?? []) {
|
|
331
|
+
const entry = this[kSessionStats].get(session)
|
|
332
|
+
if (entry === undefined) continue
|
|
333
|
+
overview.push({
|
|
334
|
+
id: session.header?.id ?? '<unknown>',
|
|
335
|
+
compactions: entry.compactions,
|
|
336
|
+
shadowedTokens: entry.shadowedTokens,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
return overview
|
|
280
340
|
}
|
|
281
341
|
}
|
|
282
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-dcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -62,18 +62,42 @@
|
|
|
62
62
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
|
-
"@deepseek-ai/cordis": {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"@deepseek-ai/dsh-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
"@deepseek-ai/cordis": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@deepseek-ai/dsh-agent": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@deepseek-ai/dsh-brand": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@deepseek-ai/dsh-commands": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@deepseek-ai/dsh-compaction": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"@deepseek-ai/dsh-compaction-basic": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@deepseek-ai/dsh-invariants": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@deepseek-ai/dsh-llm": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@deepseek-ai/dsh-session": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@deepseek-ai/dsh-token-meter": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@deepseek-ai/schemastery": {
|
|
99
|
+
"optional": true
|
|
100
|
+
}
|
|
77
101
|
},
|
|
78
102
|
"devDependencies": {
|
|
79
103
|
"@deepseek-ai/cordis": "^4.0.1",
|