@aiwayds/dsh-dcp 0.5.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.
Files changed (2) hide show
  1. package/lib/index.js +60 -55
  2. package/package.json +37 -13
package/lib/index.js CHANGED
@@ -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,38 +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-assistant-message counters since the last dsh-dcp compaction,
99
- * per session — one "round" is one `assistant/message` (one LLM roundtrip).
100
- * Weak keys: disposed sessions (including one-shot subagents) drop out with
101
- * the object.
102
- */
103
- #rounds = new WeakMap()
104
-
105
- /**
106
- * Sessions whose next `compactNow` is a round-interval trigger rather than
107
- * a manual command. Only {@link DcpEngine.#maybeRoundCompact} writes;
108
- * `compactNow` reads and clears its own entry, so a stale marker can only
109
- * turn a manual compaction into a labeled `'round'` one — never the reverse.
110
- */
111
- #triggerLabels = new WeakMap()
112
-
113
- /** Sessions with a round-triggered compaction still in flight. */
114
- #roundInFlight = new WeakSet()
115
-
116
- /**
117
- * Per-session compaction records: one entry per session that committed at
118
- * least one compaction, counting the compactions and the shadowed tokens.
119
- * Weak keys (mirroring `#rounds`): disposed sessions — including one-shot
120
- * subagents — drop out with the object. Not enumerated directly, because a
121
- * WeakMap leaks nothing and lists nothing; `/dcp` walks the sessions
122
- * service and looks each live session up here.
123
- */
124
- #sessionStats = new WeakMap()
125
-
126
119
  constructor(ctx, config = {}) {
127
120
  const { basic, dcp } = splitConfig(config)
128
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()
129
133
  this.dcp = { ...resolveDcpConfig(dcp) }
130
134
  this.dcpStats = { compactions: 0, shadowedTokens: 0, lastAt: null }
131
135
  this.pluginPath = fileURLToPath(import.meta.url)
@@ -133,7 +137,7 @@ export class DcpEngine extends BasicCompactionEngine {
133
137
  ctx.effect(function* () {
134
138
  yield registerDcpCommand(ctx, engine, VERSION)
135
139
  }, 'dsh-dcp /dcp command lifecycle')
136
- if (this.config.auto) this.#registerRoundTrigger()
140
+ if (this.config.auto) this[kRegisterRoundTrigger]()
137
141
  }
138
142
 
139
143
  /**
@@ -145,7 +149,7 @@ export class DcpEngine extends BasicCompactionEngine {
145
149
  * like the top-level session — and because one-shot subagents emit many
146
150
  * assistant messages inside a single turn, they now trigger too.
147
151
  */
148
- #registerRoundTrigger() {
152
+ [kRegisterRoundTrigger]() {
149
153
  const { ctx } = this
150
154
  ctx.on('session/event', (session, event) => {
151
155
  // Counting is skipped while disabled, but the listener stays registered
@@ -155,10 +159,10 @@ export class DcpEngine extends BasicCompactionEngine {
155
159
  // counting it (instead of completed turns) also covers one-shot
156
160
  // subagents, whose whole run is a single turn with many model calls.
157
161
  if (event.type !== 'assistant/message') return
158
- this.#rounds.set(session, (this.#rounds.get(session) ?? 0) + 1)
162
+ this[kRounds].set(session, (this[kRounds].get(session) ?? 0) + 1)
159
163
  })
160
164
  ctx.on('agent/status', ({ agent, status }) => {
161
- if (status === 'idle') this.#maybeRoundCompact(agent)
165
+ if (status === 'idle') this[kMaybeRoundCompact](agent)
162
166
  })
163
167
  }
164
168
 
@@ -170,17 +174,17 @@ export class DcpEngine extends BasicCompactionEngine {
170
174
  * must not cancel the N assistant messages behind it. Any other failure warns
171
175
  * and releases the boundary: the pressure trigger remains the safety net.
172
176
  */
173
- #maybeRoundCompact(agent) {
177
+ [kMaybeRoundCompact](agent) {
174
178
  const interval = this.dcp.roundInterval
175
179
  if (!interval) return
176
180
  const session = agent?.session
177
- if (session === undefined || this.#roundInFlight.has(session)) return
178
- if ((this.#rounds.get(session) ?? 0) < interval) return
179
- this.#triggerLabels.set(session, 'round')
180
- this.#roundInFlight.add(session)
181
- const settle = () => this.#roundInFlight.delete(session)
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)
182
186
  const consume = () => {
183
- this.#rounds.delete(session)
187
+ this[kRounds].delete(session)
184
188
  settle()
185
189
  }
186
190
  void this.compactNow(agent, new AbortController().signal).then(consume, (error) => {
@@ -217,7 +221,7 @@ export class DcpEngine extends BasicCompactionEngine {
217
221
  async compactIfNeeded(agent, trigger, signal) {
218
222
  const label = trigger === 'context-overflow' ? 'overflow' : 'auto'
219
223
  const result = await super.compactIfNeeded(agent, trigger, signal)
220
- if (result !== null) this.#appendNotice(agent.session, result, label)
224
+ if (result !== null) this[kAppendNotice](agent.session, result, label)
221
225
  return result
222
226
  }
223
227
 
@@ -229,25 +233,25 @@ export class DcpEngine extends BasicCompactionEngine {
229
233
  */
230
234
  async compactNow(agent, signal, sourceCommandId) {
231
235
  const session = agent.session
232
- const trigger = this.#triggerLabels.get(session) === 'round' ? 'round' : 'manual'
236
+ const trigger = this[kTriggerLabels].get(session) === 'round' ? 'round' : 'manual'
233
237
  try {
234
238
  const result = await super.compactNow(agent, signal, sourceCommandId)
235
239
  // `null` means no useful range existed: release the round counter so an
236
240
  // early-session interval boundary cannot retry every idle boundary.
237
241
  // Manual compactions share the release: a user-driven compact restarts
238
242
  // interval counting whether or not it found anything to compact.
239
- if (result === null) this.#rounds.delete(session)
243
+ if (result === null) this[kRounds].delete(session)
240
244
  else this.recordCompaction(session, result, trigger)
241
245
  return result
242
246
  } finally {
243
- this.#triggerLabels.delete(session)
247
+ this[kTriggerLabels].delete(session)
244
248
  }
245
249
  }
246
250
 
247
251
  /** Stats and round-counter restart for every committed region. */
248
252
  async compactRegion(start, end, agent, signal) {
249
253
  const result = await super.compactRegion(start, end, agent, signal)
250
- this.#recordStats(agent.session, result)
254
+ this[kRecordStats](agent.session, result)
251
255
  return result
252
256
  }
253
257
 
@@ -255,12 +259,12 @@ export class DcpEngine extends BasicCompactionEngine {
255
259
  * Record one committed compaction: bump the `/dcp` counters and restart the
256
260
  * round-interval counting.
257
261
  */
258
- #recordStats(session, result) {
262
+ [kRecordStats](session, result) {
259
263
  this.dcpStats.compactions += 1
260
264
  this.dcpStats.shadowedTokens += result.shadowedTokenCount
261
265
  this.dcpStats.lastAt = Date.now()
262
- this.#rounds.delete(session)
263
- this.#recordSessionStats(session, result.shadowedTokenCount)
266
+ this[kRounds].delete(session)
267
+ this[kRecordSessionStats](session, result.shadowedTokenCount)
264
268
  }
265
269
 
266
270
  /**
@@ -268,12 +272,12 @@ export class DcpEngine extends BasicCompactionEngine {
268
272
  * must be objects, so a session-less recording (should not happen) is
269
273
  * skipped rather than thrown on.
270
274
  */
271
- #recordSessionStats(session, shadowedTokenCount) {
275
+ [kRecordSessionStats](session, shadowedTokenCount) {
272
276
  if (session === undefined || session === null || typeof session !== 'object') return
273
- const entry = this.#sessionStats.get(session) ?? { compactions: 0, shadowedTokens: 0 }
277
+ const entry = this[kSessionStats].get(session) ?? { compactions: 0, shadowedTokens: 0 }
274
278
  entry.compactions += 1
275
279
  entry.shadowedTokens += shadowedTokenCount
276
- this.#sessionStats.set(session, entry)
280
+ this[kSessionStats].set(session, entry)
277
281
  }
278
282
 
279
283
  /**
@@ -281,7 +285,7 @@ export class DcpEngine extends BasicCompactionEngine {
281
285
  * is a `notice`-form plugin message, so every dsh frontend renders it as a
282
286
  * collapsed transcript row, live and on replay.
283
287
  */
284
- #appendNotice(session, result, trigger) {
288
+ [kAppendNotice](session, result, trigger) {
285
289
  if (!this.dcp.notice) return
286
290
  const summary = boundContextSummary(noticeText(this.dcp.language, result.shadowedSeqs.length, result.shadowedTokenCount, trigger))
287
291
  try {
@@ -303,10 +307,11 @@ export class DcpEngine extends BasicCompactionEngine {
303
307
  *
304
308
  * Internal seam: `compactNow` calls it after its durable commit; tests
305
309
  * drive it directly instead of mocking the upstream region machinery.
310
+ * Public-named but not part of the dsh compaction contract.
306
311
  */
307
312
  recordCompaction(session, result, trigger) {
308
- this.#recordStats(session, result)
309
- this.#appendNotice(session, result, trigger)
313
+ this[kRecordStats](session, result)
314
+ this[kAppendNotice](session, result, trigger)
310
315
  }
311
316
 
312
317
  /**
@@ -323,7 +328,7 @@ export class DcpEngine extends BasicCompactionEngine {
323
328
  // Services live on ctx for a cordis plugin instance (`inject` only
324
329
  // declares them); this.sessions is undefined in production.
325
330
  for (const session of this.ctx.sessions?.list?.() ?? []) {
326
- const entry = this.#sessionStats.get(session)
331
+ const entry = this[kSessionStats].get(session)
327
332
  if (entry === undefined) continue
328
333
  overview.push({
329
334
  id: session.header?.id ?? '<unknown>',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-dcp",
3
- "version": "0.5.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": { "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 }
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",