@aiwayds/dsh-dcp 0.5.0 → 0.6.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/lib/config.js +3 -3
- package/lib/index.js +70 -55
- package/lib/setup.js +1 -1
- package/lib/summarizer.js +4 -4
- package/package.json +76 -47
- package/scripts/compare.mjs +1 -1
- package/scripts/link-dsh-closure.mjs +100 -0
- package/scripts/smoke-boot.mjs +122 -0
package/lib/config.js
CHANGED
|
@@ -60,11 +60,11 @@ export function splitConfig(config = {}) {
|
|
|
60
60
|
for (const key of Object.keys(config)) {
|
|
61
61
|
if (!known.has(key)) throw new Error(`DcpConfig: unknown key "${key}"`)
|
|
62
62
|
}
|
|
63
|
-
const basic = {}
|
|
63
|
+
const basic = /** @type {Record<string, unknown>} */ ({})
|
|
64
64
|
for (const key of BASIC_CONFIG_KEYS) {
|
|
65
65
|
if (config[key] !== undefined) basic[key] = config[key]
|
|
66
66
|
}
|
|
67
|
-
const dcp = {}
|
|
67
|
+
const dcp = /** @type {Record<string, unknown>} */ ({})
|
|
68
68
|
for (const key of DCP_CONFIG_KEYS) {
|
|
69
69
|
if (config[key] !== undefined) dcp[key] = config[key]
|
|
70
70
|
}
|
|
@@ -75,7 +75,7 @@ export function splitConfig(config = {}) {
|
|
|
75
75
|
* Validate and resolve dcp defaults.
|
|
76
76
|
*
|
|
77
77
|
* @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[]}>}
|
|
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}>}
|
|
79
79
|
*/
|
|
80
80
|
export function resolveDcpConfig(raw = {}) {
|
|
81
81
|
if (raw.dedup !== undefined && typeof raw.dedup !== 'boolean') {
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,33 @@ 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
|
+
* Verified against dsh 0.1.2-alpha.3 (cordis 4.0.2): `withProps` /
|
|
55
|
+
* `createTraceable` / `applyTraceable` receivers and the `ctx.mixin` service
|
|
56
|
+
* forwarding are unchanged, and the base engine still brands no `#private`
|
|
57
|
+
* state — the symbol-keyed approach remains both necessary and valid.
|
|
58
|
+
*/
|
|
59
|
+
const kRounds = Symbol('dsh-dcp.rounds')
|
|
60
|
+
const kTriggerLabels = Symbol('dsh-dcp.triggerLabels')
|
|
61
|
+
const kRoundInFlight = Symbol('dsh-dcp.roundInFlight')
|
|
62
|
+
const kSessionStats = Symbol('dsh-dcp.sessionStats')
|
|
63
|
+
const kRegisterRoundTrigger = Symbol('dsh-dcp.registerRoundTrigger')
|
|
64
|
+
const kMaybeRoundCompact = Symbol('dsh-dcp.maybeRoundCompact')
|
|
65
|
+
const kRecordStats = Symbol('dsh-dcp.recordStats')
|
|
66
|
+
const kRecordSessionStats = Symbol('dsh-dcp.recordSessionStats')
|
|
67
|
+
const kAppendNotice = Symbol('dsh-dcp.appendNotice')
|
|
68
|
+
|
|
42
69
|
/**
|
|
43
70
|
* Deterministic compaction engine: `summarize()` overridden, everything else
|
|
44
71
|
* inherited. Registers the `/dcp` command beside the inherited `/compact`.
|
|
@@ -89,43 +116,26 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
89
116
|
dcp
|
|
90
117
|
|
|
91
118
|
/** Compaction counters surfaced by `/dcp`. */
|
|
119
|
+
/** @type {{compactions: number, shadowedTokens: number, lastAt: number|null}} */
|
|
92
120
|
dcpStats
|
|
93
121
|
|
|
94
122
|
/** Absolute module path, echoed by `/dcp set` for persistence snippets. */
|
|
95
123
|
pluginPath
|
|
96
124
|
|
|
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
125
|
constructor(ctx, config = {}) {
|
|
127
126
|
const { basic, dcp } = splitConfig(config)
|
|
128
127
|
super(ctx, basic)
|
|
128
|
+
// Constructor assignments, not class fields: Node 26's V8 silently drops
|
|
129
|
+
// every symbol-keyed class field after the first one in a derived class
|
|
130
|
+
// (plain string-keyed and base-class fields are unaffected).
|
|
131
|
+
/** Per-session assistant-message counters since the last dsh-dcp compaction. */
|
|
132
|
+
this[kRounds] = new WeakMap()
|
|
133
|
+
/** Sessions whose next `compactNow` is a round-interval trigger, not manual. */
|
|
134
|
+
this[kTriggerLabels] = new WeakMap()
|
|
135
|
+
/** Sessions with a round-triggered compaction still in flight. */
|
|
136
|
+
this[kRoundInFlight] = new WeakSet()
|
|
137
|
+
/** Per-session compaction records for `/dcp` (weak so disposed sessions drop out). */
|
|
138
|
+
this[kSessionStats] = new WeakMap()
|
|
129
139
|
this.dcp = { ...resolveDcpConfig(dcp) }
|
|
130
140
|
this.dcpStats = { compactions: 0, shadowedTokens: 0, lastAt: null }
|
|
131
141
|
this.pluginPath = fileURLToPath(import.meta.url)
|
|
@@ -133,7 +143,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
133
143
|
ctx.effect(function* () {
|
|
134
144
|
yield registerDcpCommand(ctx, engine, VERSION)
|
|
135
145
|
}, 'dsh-dcp /dcp command lifecycle')
|
|
136
|
-
if (this.config.auto) this
|
|
146
|
+
if (this.config.auto) this[kRegisterRoundTrigger]()
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -145,7 +155,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
145
155
|
* like the top-level session — and because one-shot subagents emit many
|
|
146
156
|
* assistant messages inside a single turn, they now trigger too.
|
|
147
157
|
*/
|
|
148
|
-
|
|
158
|
+
[kRegisterRoundTrigger]() {
|
|
149
159
|
const { ctx } = this
|
|
150
160
|
ctx.on('session/event', (session, event) => {
|
|
151
161
|
// Counting is skipped while disabled, but the listener stays registered
|
|
@@ -155,10 +165,10 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
155
165
|
// counting it (instead of completed turns) also covers one-shot
|
|
156
166
|
// subagents, whose whole run is a single turn with many model calls.
|
|
157
167
|
if (event.type !== 'assistant/message') return
|
|
158
|
-
this
|
|
168
|
+
this[kRounds].set(session, (this[kRounds].get(session) ?? 0) + 1)
|
|
159
169
|
})
|
|
160
170
|
ctx.on('agent/status', ({ agent, status }) => {
|
|
161
|
-
if (status === 'idle') this
|
|
171
|
+
if (status === 'idle') this[kMaybeRoundCompact](agent)
|
|
162
172
|
})
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -170,17 +180,17 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
170
180
|
* must not cancel the N assistant messages behind it. Any other failure warns
|
|
171
181
|
* and releases the boundary: the pressure trigger remains the safety net.
|
|
172
182
|
*/
|
|
173
|
-
|
|
183
|
+
[kMaybeRoundCompact](agent) {
|
|
174
184
|
const interval = this.dcp.roundInterval
|
|
175
185
|
if (!interval) return
|
|
176
186
|
const session = agent?.session
|
|
177
|
-
if (session === undefined || this
|
|
178
|
-
if ((this
|
|
179
|
-
this
|
|
180
|
-
this
|
|
181
|
-
const settle = () => this
|
|
187
|
+
if (session === undefined || this[kRoundInFlight].has(session)) return
|
|
188
|
+
if ((this[kRounds].get(session) ?? 0) < interval) return
|
|
189
|
+
this[kTriggerLabels].set(session, 'round')
|
|
190
|
+
this[kRoundInFlight].add(session)
|
|
191
|
+
const settle = () => this[kRoundInFlight].delete(session)
|
|
182
192
|
const consume = () => {
|
|
183
|
-
this
|
|
193
|
+
this[kRounds].delete(session)
|
|
184
194
|
settle()
|
|
185
195
|
}
|
|
186
196
|
void this.compactNow(agent, new AbortController().signal).then(consume, (error) => {
|
|
@@ -217,7 +227,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
217
227
|
async compactIfNeeded(agent, trigger, signal) {
|
|
218
228
|
const label = trigger === 'context-overflow' ? 'overflow' : 'auto'
|
|
219
229
|
const result = await super.compactIfNeeded(agent, trigger, signal)
|
|
220
|
-
if (result !== null) this
|
|
230
|
+
if (result !== null) this[kAppendNotice](agent.session, result, label)
|
|
221
231
|
return result
|
|
222
232
|
}
|
|
223
233
|
|
|
@@ -226,28 +236,32 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
226
236
|
* round-interval trigger — the parent's `compactNow` bypasses
|
|
227
237
|
* `compactRegion` (it drives `compactSurfaceRegion` directly), so without
|
|
228
238
|
* this override the manual path would miss stats and the transcript notice.
|
|
239
|
+
*
|
|
240
|
+
* alpha.3 note: the base dereferences `signal` unguarded
|
|
241
|
+
* (`signal.throwIfAborted()`), so the signal is required — both this
|
|
242
|
+
* plugin's round trigger and the command invocation always pass one.
|
|
229
243
|
*/
|
|
230
244
|
async compactNow(agent, signal, sourceCommandId) {
|
|
231
245
|
const session = agent.session
|
|
232
|
-
const trigger = this
|
|
246
|
+
const trigger = this[kTriggerLabels].get(session) === 'round' ? 'round' : 'manual'
|
|
233
247
|
try {
|
|
234
248
|
const result = await super.compactNow(agent, signal, sourceCommandId)
|
|
235
249
|
// `null` means no useful range existed: release the round counter so an
|
|
236
250
|
// early-session interval boundary cannot retry every idle boundary.
|
|
237
251
|
// Manual compactions share the release: a user-driven compact restarts
|
|
238
252
|
// interval counting whether or not it found anything to compact.
|
|
239
|
-
if (result === null) this
|
|
253
|
+
if (result === null) this[kRounds].delete(session)
|
|
240
254
|
else this.recordCompaction(session, result, trigger)
|
|
241
255
|
return result
|
|
242
256
|
} finally {
|
|
243
|
-
this
|
|
257
|
+
this[kTriggerLabels].delete(session)
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
/** Stats and round-counter restart for every committed region. */
|
|
248
262
|
async compactRegion(start, end, agent, signal) {
|
|
249
263
|
const result = await super.compactRegion(start, end, agent, signal)
|
|
250
|
-
this
|
|
264
|
+
this[kRecordStats](agent.session, result)
|
|
251
265
|
return result
|
|
252
266
|
}
|
|
253
267
|
|
|
@@ -255,12 +269,12 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
255
269
|
* Record one committed compaction: bump the `/dcp` counters and restart the
|
|
256
270
|
* round-interval counting.
|
|
257
271
|
*/
|
|
258
|
-
|
|
272
|
+
[kRecordStats](session, result) {
|
|
259
273
|
this.dcpStats.compactions += 1
|
|
260
274
|
this.dcpStats.shadowedTokens += result.shadowedTokenCount
|
|
261
275
|
this.dcpStats.lastAt = Date.now()
|
|
262
|
-
this
|
|
263
|
-
this
|
|
276
|
+
this[kRounds].delete(session)
|
|
277
|
+
this[kRecordSessionStats](session, result.shadowedTokenCount)
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
/**
|
|
@@ -268,12 +282,12 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
268
282
|
* must be objects, so a session-less recording (should not happen) is
|
|
269
283
|
* skipped rather than thrown on.
|
|
270
284
|
*/
|
|
271
|
-
|
|
285
|
+
[kRecordSessionStats](session, shadowedTokenCount) {
|
|
272
286
|
if (session === undefined || session === null || typeof session !== 'object') return
|
|
273
|
-
const entry = this
|
|
287
|
+
const entry = this[kSessionStats].get(session) ?? { compactions: 0, shadowedTokens: 0 }
|
|
274
288
|
entry.compactions += 1
|
|
275
289
|
entry.shadowedTokens += shadowedTokenCount
|
|
276
|
-
this
|
|
290
|
+
this[kSessionStats].set(session, entry)
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
/**
|
|
@@ -281,7 +295,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
281
295
|
* is a `notice`-form plugin message, so every dsh frontend renders it as a
|
|
282
296
|
* collapsed transcript row, live and on replay.
|
|
283
297
|
*/
|
|
284
|
-
|
|
298
|
+
[kAppendNotice](session, result, trigger) {
|
|
285
299
|
if (!this.dcp.notice) return
|
|
286
300
|
const summary = boundContextSummary(noticeText(this.dcp.language, result.shadowedSeqs.length, result.shadowedTokenCount, trigger))
|
|
287
301
|
try {
|
|
@@ -303,10 +317,11 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
303
317
|
*
|
|
304
318
|
* Internal seam: `compactNow` calls it after its durable commit; tests
|
|
305
319
|
* drive it directly instead of mocking the upstream region machinery.
|
|
320
|
+
* Public-named but not part of the dsh compaction contract.
|
|
306
321
|
*/
|
|
307
322
|
recordCompaction(session, result, trigger) {
|
|
308
|
-
this
|
|
309
|
-
this
|
|
323
|
+
this[kRecordStats](session, result)
|
|
324
|
+
this[kAppendNotice](session, result, trigger)
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
/**
|
|
@@ -323,7 +338,7 @@ export class DcpEngine extends BasicCompactionEngine {
|
|
|
323
338
|
// Services live on ctx for a cordis plugin instance (`inject` only
|
|
324
339
|
// declares them); this.sessions is undefined in production.
|
|
325
340
|
for (const session of this.ctx.sessions?.list?.() ?? []) {
|
|
326
|
-
const entry = this
|
|
341
|
+
const entry = this[kSessionStats].get(session)
|
|
327
342
|
if (entry === undefined) continue
|
|
328
343
|
overview.push({
|
|
329
344
|
id: session.header?.id ?? '<unknown>',
|
package/lib/setup.js
CHANGED
|
@@ -68,7 +68,7 @@ export function findBundledProfiles(profilesDir, pkg) {
|
|
|
68
68
|
*
|
|
69
69
|
* @param {string|undefined} text - existing file content, or undefined when the file is absent.
|
|
70
70
|
* @param {{ name: string }} options - the resolvable dsh-dcp entry specifier to mount.
|
|
71
|
-
* @returns {{ action: 'create'|'patch'
|
|
71
|
+
* @returns {{ action: 'create'|'patch', block: string, note?: string } | { action: 'skip' }}
|
|
72
72
|
*/
|
|
73
73
|
export function planPatch(text, { name }) {
|
|
74
74
|
if (text === undefined || text === null) {
|
package/lib/summarizer.js
CHANGED
|
@@ -105,7 +105,7 @@ export function estimateTextTokens(text, mode = 'cjk') {
|
|
|
105
105
|
* tool-result payloads (the same content the host meter prices, minus its
|
|
106
106
|
* per-block overhead).
|
|
107
107
|
*
|
|
108
|
-
* @param {{ content?: Array<
|
|
108
|
+
* @param {{ content?: Array<Record<string, any>> }} message - any message-shaped object.
|
|
109
109
|
* @param {'cjk'|'ascii'} [mode] - pricing mode, see {@link estimateTextTokens}.
|
|
110
110
|
* @returns {number} estimated tokens.
|
|
111
111
|
*/
|
|
@@ -275,7 +275,7 @@ function argDisplay(name, parsed) {
|
|
|
275
275
|
* @returns {object} the extracted fact bundle.
|
|
276
276
|
*/
|
|
277
277
|
export function extractFacts(messages, language = 'en') {
|
|
278
|
-
const facts = {
|
|
278
|
+
const facts = /** @type {{messageCount: number, intents: string[], files: Map<string, {reads: number, writes: number}>, commands: string[], errors: string[], pendingTodos: string[], concepts: Set<string>, dupCounts: Map<string, {name: string, parsed: any, count: number}>, toolCallCount: number, lastUserText: string, lastAssistantText: string, carried: Map<string, string[]>}} */ ({
|
|
279
279
|
messageCount: messages.length,
|
|
280
280
|
intents: [],
|
|
281
281
|
files: new Map(),
|
|
@@ -288,7 +288,7 @@ export function extractFacts(messages, language = 'en') {
|
|
|
288
288
|
lastUserText: '',
|
|
289
289
|
lastAssistantText: '',
|
|
290
290
|
carried: new Map(),
|
|
291
|
-
}
|
|
291
|
+
})
|
|
292
292
|
const callsById = new Map()
|
|
293
293
|
const errorRegexes = errorPatterns(language)
|
|
294
294
|
const { checkbox, markers } = todoPatterns(language)
|
|
@@ -521,7 +521,7 @@ function composeTerse(facts, t, itemChars) {
|
|
|
521
521
|
*
|
|
522
522
|
* @param {{ messages: import('@deepseek-ai/dsh-llm').Message[] }} input - replayed region.
|
|
523
523
|
* @param {object} dcp - resolved dcp config.
|
|
524
|
-
* @param {(message:
|
|
524
|
+
* @param {(message: any) => number} [estimateMessageLike] - token estimator; defaults to {@link estimateMessageTokens}.
|
|
525
525
|
* @returns {{ summary: { type: 'text', text: string }[], provider: string, model: string }}
|
|
526
526
|
*/
|
|
527
527
|
export function summarizeDeterministically(input, dcp, estimateMessageLike = (message) => estimateMessageTokens(message, dcp.tokenEstimate)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-dcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"test": "node --test",
|
|
27
|
-
"setup": "node scripts/setup.mjs"
|
|
27
|
+
"setup": "node scripts/setup.mjs",
|
|
28
|
+
"link:closure": "node scripts/link-dsh-closure.mjs",
|
|
29
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
30
|
+
"smoke": "node scripts/smoke-boot.mjs"
|
|
28
31
|
},
|
|
29
32
|
"keywords": [
|
|
30
33
|
"dsh",
|
|
@@ -48,57 +51,83 @@
|
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
52
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
53
|
-
"@deepseek-ai/dsh-brand": "0.1.
|
|
54
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
55
|
-
"@deepseek-ai/dsh-compaction": "0.1.
|
|
56
|
-
"@deepseek-ai/dsh-compaction-basic": "0.1.
|
|
57
|
-
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.
|
|
58
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
59
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
60
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
61
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
62
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
55
|
+
"@deepseek-ai/dsh-agent": ">=0.1.2-alpha.3",
|
|
56
|
+
"@deepseek-ai/dsh-brand": ">=0.1.2-alpha.3",
|
|
57
|
+
"@deepseek-ai/dsh-commands": ">=0.1.2-alpha.3",
|
|
58
|
+
"@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.3",
|
|
59
|
+
"@deepseek-ai/dsh-compaction-basic": ">=0.1.2-alpha.3",
|
|
60
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": ">=0.1.2-alpha.3",
|
|
61
|
+
"@deepseek-ai/dsh-invariants": ">=0.1.2-alpha.3",
|
|
62
|
+
"@deepseek-ai/dsh-llm": ">=0.1.2-alpha.3",
|
|
63
|
+
"@deepseek-ai/dsh-session": ">=0.1.2-alpha.3",
|
|
64
|
+
"@deepseek-ai/dsh-token-meter": ">=0.1.2-alpha.3",
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
63
66
|
},
|
|
64
67
|
"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
|
-
|
|
68
|
+
"@deepseek-ai/cordis": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@deepseek-ai/dsh-agent": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@deepseek-ai/dsh-brand": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@deepseek-ai/dsh-commands": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"@deepseek-ai/dsh-compaction": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@deepseek-ai/dsh-compaction-basic": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@deepseek-ai/dsh-invariants": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@deepseek-ai/dsh-llm": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@deepseek-ai/dsh-session": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@deepseek-ai/dsh-token-meter": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"@deepseek-ai/schemastery": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
77
104
|
},
|
|
78
105
|
"devDependencies": {
|
|
79
|
-
"@
|
|
80
|
-
"@deepseek-ai/
|
|
81
|
-
"@deepseek-ai/dsh-
|
|
82
|
-
"@deepseek-ai/dsh-
|
|
83
|
-
"@deepseek-ai/dsh-
|
|
84
|
-
"@deepseek-ai/dsh-compaction
|
|
85
|
-
"@deepseek-ai/dsh-compaction-
|
|
86
|
-
"@deepseek-ai/dsh-
|
|
87
|
-
"@deepseek-ai/dsh-
|
|
88
|
-
"@deepseek-ai/dsh-
|
|
89
|
-
"@deepseek-ai/dsh-
|
|
90
|
-
"@deepseek-ai/
|
|
106
|
+
"@types/node": "^24.0.0",
|
|
107
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
108
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
109
|
+
"@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
|
|
110
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
111
|
+
"@deepseek-ai/dsh-compaction": "0.1.2-alpha.3",
|
|
112
|
+
"@deepseek-ai/dsh-compaction-basic": "0.1.2-alpha.3",
|
|
113
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-alpha.3",
|
|
114
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
115
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
116
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
117
|
+
"@deepseek-ai/dsh-token-meter": "0.1.2-alpha.3",
|
|
118
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
119
|
+
"typescript": "~5.9.0"
|
|
91
120
|
},
|
|
92
121
|
"overrides": {
|
|
93
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
94
|
-
"@deepseek-ai/dsh-brand": "0.1.
|
|
95
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
96
|
-
"@deepseek-ai/dsh-compaction": "0.1.
|
|
97
|
-
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.
|
|
98
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
99
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
100
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
101
|
-
"@deepseek-ai/dsh-token-meter": "0.1.
|
|
122
|
+
"@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
|
|
123
|
+
"@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
|
|
124
|
+
"@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
|
|
125
|
+
"@deepseek-ai/dsh-compaction": "0.1.2-alpha.3",
|
|
126
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-alpha.3",
|
|
127
|
+
"@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
|
|
128
|
+
"@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
|
|
129
|
+
"@deepseek-ai/dsh-session": "0.1.2-alpha.3",
|
|
130
|
+
"@deepseek-ai/dsh-token-meter": "0.1.2-alpha.3"
|
|
102
131
|
},
|
|
103
132
|
"dsh": {
|
|
104
133
|
"bundle": {
|
package/scripts/compare.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function fmt(n) {
|
|
|
175
175
|
*/
|
|
176
176
|
function simulate(events, mode, useDcp) {
|
|
177
177
|
const surface = []
|
|
178
|
-
const stats = { requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] }
|
|
178
|
+
const stats = /** @type {{ requests: number, inputTokens: number, hitTokens: number, compactions: Array<{regionNodes: number, regionHost: number, regionCjk: number, summaryHost: number, summaryCjk: number, compression: string, totalAfter: number, preview: string}> }} */ ({ requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] })
|
|
179
179
|
let prevSurface = []
|
|
180
180
|
let compactionId = 0
|
|
181
181
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closure linker: point every `node_modules/@deepseek-ai/*` entry at one dsh
|
|
3
|
+
* closure (the installed dsh CLI's own `node_modules/@deepseek-ai` tree).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: dsh-dcp is a plugin that runs *inside* the installed dsh
|
|
6
|
+
* CLI, and its source imports the `@deepseek-ai/*` host packages (cordis,
|
|
7
|
+
* dsh-compaction-basic, dsh-llm, …). This repo is lib-as-source — there is no
|
|
8
|
+
* `src/` and no build step — so typecheck (`pnpm check`, tsc checkJs) and the
|
|
9
|
+
* node:test suite must resolve those imports against exactly ONE copy of the
|
|
10
|
+
* host graph: the closure the real runtime uses. Linking also guarantees a
|
|
11
|
+
* single `@deepseek-ai/cordis` instance, which the `declare module`
|
|
12
|
+
* augmentations (e.g. `ctx.commands`) require.
|
|
13
|
+
*
|
|
14
|
+
* NOTE on the guard below: unlike a TS repo this repo has no `src/` directory
|
|
15
|
+
* — `lib/` IS the source — so the "am I inside the repo?" check looks for
|
|
16
|
+
* `lib/` (plus `.git/`, which a published tarball never carries). The script
|
|
17
|
+
* is never wired as a lifecycle hook (no postinstall); it is run explicitly
|
|
18
|
+
* (CI step, `pnpm link:closure`), and the guard only protects stray manual
|
|
19
|
+
* runs from outside the repo.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order for the closure:
|
|
22
|
+
* 0) `$DSH_CLOSURE_DIR` — explicit override, e.g. a scratch closure from
|
|
23
|
+
* `npm i --prefix ~/tmp/dsh-alpha-closure @deepseek-ai/dsh@alpha`:
|
|
24
|
+
* DSH_CLOSURE_DIR=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai node scripts/link-dsh-closure.mjs
|
|
25
|
+
* 1) the `dsh` bin on PATH → its package's node_modules/@deepseek-ai
|
|
26
|
+
* 2) `npm root -g` nested closure, then npm's flat global @deepseek-ai scope
|
|
27
|
+
*
|
|
28
|
+
* It is a no-op (exit 0) when no dsh closure is found.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { execFileSync } from 'node:child_process'
|
|
32
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
33
|
+
import { dirname, join } from 'node:path'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
|
|
36
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
37
|
+
// Guard: dev convenience for THIS repo only. lib/ is this repo's source (there
|
|
38
|
+
// is no src/), and .git/ only exists in a working clone — never in a packed
|
|
39
|
+
// tarball — so both together prove "inside the repo, not a consumer install".
|
|
40
|
+
if (!existsSync(join(repoRoot, 'lib')) || !existsSync(join(repoRoot, '.git'))) {
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
const scopeDir = join(repoRoot, 'node_modules', '@deepseek-ai')
|
|
44
|
+
|
|
45
|
+
/** The dsh closure: a node_modules dir whose @deepseek-ai scope is complete. */
|
|
46
|
+
function findDshClosure() {
|
|
47
|
+
// 0) Explicit override for dev/typecheck against an unreleased dsh line.
|
|
48
|
+
const override = process.env.DSH_CLOSURE_DIR
|
|
49
|
+
if (override !== undefined && override !== '') {
|
|
50
|
+
const dir = realpathSync(override)
|
|
51
|
+
if (existsSync(join(dir, 'cordis'))) return dir
|
|
52
|
+
console.warn(`[link-dsh-closure] DSH_CLOSURE_DIR=${override} lacks @deepseek-ai/cordis — ignoring override`)
|
|
53
|
+
}
|
|
54
|
+
// 1) Follow the `dsh` bin — the most faithful pointer to the installed CLI
|
|
55
|
+
// (`/opt/homebrew/bin/dsh` → …/lib/bin.js → pkg dir → its node_modules).
|
|
56
|
+
try {
|
|
57
|
+
const bin = execFileSync('which', ['dsh'], { encoding: 'utf8' }).trim()
|
|
58
|
+
if (bin !== '') {
|
|
59
|
+
const real = realpathSync(bin)
|
|
60
|
+
const closure = join(dirname(dirname(real)), 'node_modules', '@deepseek-ai')
|
|
61
|
+
if (existsSync(join(closure, 'cordis'))) return closure
|
|
62
|
+
}
|
|
63
|
+
} catch { /* dsh not on PATH */ }
|
|
64
|
+
// 2) Fall back to the global node_modules root — the dsh package's own
|
|
65
|
+
// nested closure (what current npm produces for a global install).
|
|
66
|
+
try {
|
|
67
|
+
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8' }).trim()
|
|
68
|
+
const nested = join(root, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai')
|
|
69
|
+
if (existsSync(join(nested, 'cordis'))) return nested
|
|
70
|
+
// 3) Last resort: npm's flat global layout, where dsh's @deepseek-ai/*
|
|
71
|
+
// deps are hoisted straight into <npm root -g>/@deepseek-ai next to
|
|
72
|
+
// the dsh package itself.
|
|
73
|
+
const flat = join(root, '@deepseek-ai')
|
|
74
|
+
if (existsSync(join(flat, 'cordis'))) return flat
|
|
75
|
+
} catch { /* npm unavailable */ }
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const closure = findDshClosure()
|
|
80
|
+
if (closure === undefined) {
|
|
81
|
+
console.warn('[link-dsh-closure] no dsh closure found — skipping @deepseek-ai links (dev without dsh)')
|
|
82
|
+
process.exit(0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mkdirSync(scopeDir, { recursive: true })
|
|
86
|
+
let linked = 0
|
|
87
|
+
for (const name of readdirSync(closure)) {
|
|
88
|
+
const target = join(scopeDir, name)
|
|
89
|
+
const source = join(closure, name)
|
|
90
|
+
try {
|
|
91
|
+
// Replace any existing entry (stale symlink, or a local .pnpm copy a
|
|
92
|
+
// previous install created) with the closure link.
|
|
93
|
+
rmSync(target, { recursive: true, force: true })
|
|
94
|
+
symlinkSync(source, target, 'junction')
|
|
95
|
+
linked++
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn(`[link-dsh-closure] failed to link ${name}: ${(error instanceof Error ? error.message : String(error))}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`[link-dsh-closure] linked ${linked} @deepseek-ai/* packages from ${closure}`)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Boot-smoke: mount the freshly packed dsh-dcp into a scratch dsh profile and
|
|
3
|
+
// boot it with the real dsh CLI.
|
|
4
|
+
//
|
|
5
|
+
// Unlike dsh-cron (which mounts through the bundle patch), this exercises the
|
|
6
|
+
// cordis.patch.yml mechanism — the same layer scripts/setup.mjs writes for
|
|
7
|
+
// users: the profile patch disables the default `compaction-basic` backend and
|
|
8
|
+
// inserts dsh-dcp by absolute path.
|
|
9
|
+
//
|
|
10
|
+
// 1. npm pack the repo → tarball
|
|
11
|
+
// 2. scratch $DSH_HOME/profiles/smoke: the dsh-base bundle plus the tarball
|
|
12
|
+
// as a file: dependency, and a cordis.patch.yml carrying the
|
|
13
|
+
// disable-compaction-basic + insert-dsh-dcp mount block
|
|
14
|
+
// 3. pnpm install in the profile
|
|
15
|
+
// 4. `dsh --profile smoke --dump-config` must compose dsh-dcp into the tree
|
|
16
|
+
// (mount/patch-layer proof)
|
|
17
|
+
// 5. a real boot under a timeout must load the plugin tree without a loader
|
|
18
|
+
// error (a healthy boot is silent and survives to the kill signal; a
|
|
19
|
+
// broken plugin dies within ~1s with the loader error)
|
|
20
|
+
//
|
|
21
|
+
// The dsh CLI comes from $DSH_BIN if set (e.g. a scratch alpha closure:
|
|
22
|
+
// DSH_BIN=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai/dsh/lib/bin.js),
|
|
23
|
+
// otherwise `dsh` on PATH.
|
|
24
|
+
//
|
|
25
|
+
// Exit 0 = mounted and boots clean. Temp dir is kept and printed on failure,
|
|
26
|
+
// removed on success.
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'node:child_process'
|
|
29
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { readFile } from 'node:fs/promises'
|
|
31
|
+
import { tmpdir } from 'node:os'
|
|
32
|
+
import path from 'node:path'
|
|
33
|
+
import { fileURLToPath } from 'node:url'
|
|
34
|
+
|
|
35
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
36
|
+
const pkg = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'))
|
|
37
|
+
const ownName = pkg.name // @aiwayds/dsh-dcp
|
|
38
|
+
const entryId = 'dsh-dcp'
|
|
39
|
+
|
|
40
|
+
const work = mkdtempSync(path.join(tmpdir(), 'dsh-dcp-smoke-'))
|
|
41
|
+
const home = path.join(work, 'dsh-home')
|
|
42
|
+
const profile = path.join(home, 'profiles', 'smoke')
|
|
43
|
+
mkdirSync(profile, { recursive: true })
|
|
44
|
+
|
|
45
|
+
function fail(message, output = '') {
|
|
46
|
+
console.error(`smoke-boot: FAIL — ${message}`)
|
|
47
|
+
if (output) console.error(output.split('\n').slice(0, 30).join('\n'))
|
|
48
|
+
console.error(`smoke-boot: scratch kept at ${work}`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const dshBin = process.env.DSH_BIN || 'dsh'
|
|
53
|
+
const dsh = (args, opts = {}) => spawnSync(dshBin, args, { cwd: profile, encoding: 'utf8', env: { ...process.env, DSH_HOME: home }, ...opts })
|
|
54
|
+
|
|
55
|
+
const pack = spawnSync('npm', ['pack', '--pack-destination', work], { cwd: repoRoot, encoding: 'utf8' })
|
|
56
|
+
if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${pack.stderr}`)
|
|
57
|
+
const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1) ?? '')
|
|
58
|
+
|
|
59
|
+
writeFileSync(path.join(profile, 'cordis.yml'), '# dsh profile root — empty; the tree is composed from the bundle patches\n[]\n')
|
|
60
|
+
// The mount layer under test — isomorphic with scripts/setup.mjs's output:
|
|
61
|
+
// compaction-basic disabled, dsh-dcp inserted by absolute entry path.
|
|
62
|
+
writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
|
|
63
|
+
- id: compaction-basic
|
|
64
|
+
disabled: true
|
|
65
|
+
- insert:
|
|
66
|
+
- id: ${entryId}
|
|
67
|
+
name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
|
|
68
|
+
config:
|
|
69
|
+
thresholdRatio: 0.7
|
|
70
|
+
language: zh
|
|
71
|
+
`)
|
|
72
|
+
writeFileSync(path.join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
|
|
73
|
+
writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
|
|
74
|
+
name: 'dsh-profile-smoke',
|
|
75
|
+
private: true,
|
|
76
|
+
dependencies: {
|
|
77
|
+
[ownName]: `file:${tarball}`,
|
|
78
|
+
},
|
|
79
|
+
dsh: {
|
|
80
|
+
profile: {
|
|
81
|
+
// dsh-base brings the standard tree (including compaction-basic) that
|
|
82
|
+
// the patch layer then re-wires; dsh-dcp itself mounts via the patch.
|
|
83
|
+
bundles: [
|
|
84
|
+
'@deepseek-ai/dsh-base',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}, null, 2) + '\n')
|
|
89
|
+
|
|
90
|
+
const install = spawnSync('pnpm', ['install'], { cwd: profile, encoding: 'utf8' })
|
|
91
|
+
if (install.status !== 0 || install.error) fail('pnpm install in the scratch profile failed', `${install.stdout}\n${install.stderr}`)
|
|
92
|
+
|
|
93
|
+
// Phase 1 — mount proof: the composed tree must carry the dsh-dcp entry, with
|
|
94
|
+
// compaction-basic disabled rather than removed-by-default.
|
|
95
|
+
const dump = dsh(['--profile', 'smoke', '--dump-config'])
|
|
96
|
+
if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
|
|
97
|
+
if (!dump.stdout.includes(entryId)) {
|
|
98
|
+
fail(`the composed profile tree does not contain the "${entryId}" entry — the patch mount is broken`, dump.stdout)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase 2 — boot proof: the plugin tree must LOAD without a loader error.
|
|
102
|
+
const bootSeconds = 25
|
|
103
|
+
const boot = dsh(['--profile', 'smoke'], { timeout: bootSeconds * 1000, killSignal: 'SIGKILL' })
|
|
104
|
+
const output = `${boot.stdout ?? ''}\n${boot.stderr ?? ''}`
|
|
105
|
+
const loaderErrors = [
|
|
106
|
+
/plugin tree failed to load/,
|
|
107
|
+
/failed to apply loader entry/,
|
|
108
|
+
/cannot get property ".*" without inject/,
|
|
109
|
+
/cannot get required service/,
|
|
110
|
+
/Cannot find (package|module)/,
|
|
111
|
+
/unknown key/,
|
|
112
|
+
]
|
|
113
|
+
const hit = loaderErrors.filter((re) => re.test(output))
|
|
114
|
+
if (hit.length > 0) {
|
|
115
|
+
fail('the real host failed to load the plugin tree:', output.split('\n').filter((line) => hit.some((re) => re.test(line)) || /Error/.test(line)).slice(0, 15).join('\n'))
|
|
116
|
+
}
|
|
117
|
+
if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
|
|
118
|
+
fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, and booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`})`)
|
|
122
|
+
rmSync(work, { recursive: true, force: true })
|