dsh-auto-compaction 0.1.1 → 0.1.2

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/index.js CHANGED
@@ -26,7 +26,9 @@ import { decideTrigger, shouldAttempt } from "./plan.js";
26
26
  const LOG = "[handoff]";
27
27
 
28
28
  const SETTINGS_DIR = join(homedir(), ".dsh-handoff-compaction");
29
- const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
29
+ // Test harnesses point this at a missing file so the live sidecar cannot leak
30
+ // into a case that expects the configured ratio.
31
+ const SETTINGS_FILE = process.env.DSH_HANDOFF_SETTINGS_FILE ?? join(SETTINGS_DIR, "settings.json");
30
32
  const DEFAULT_TEMPLATE_FILE = join(SETTINGS_DIR, "template.md");
31
33
 
32
34
  /**
@@ -328,6 +330,13 @@ export default class HandoffCompaction extends BasicCompactionEngine {
328
330
  : undefined;
329
331
  const target = configured ?? latest ?? agentTarget;
330
332
  if (target === undefined) throw new Error("no provider/model available for the handoff summary");
333
+ // Replaying the prefix only reuses the provider cache when the auxiliary
334
+ // request carries the same routing knobs as the routed one. Strata writes
335
+ // the reasoning directive into the prefix, so a missing effort breaks the
336
+ // cache a few thousand tokens in. Only inherit it when the summary target
337
+ // is the same model that produced the latest request.
338
+ const sameTarget = latest !== undefined && target.provider === latest.provider && target.model === latest.model;
339
+ const reasoningEffort = sameTarget ? latest.reasoningEffort : undefined;
331
340
 
332
341
  const resolved = this.handoff.instruction === undefined
333
342
  ? readTemplate(this.handoff.templateFile, this.handoff.autoCreateTemplate)
@@ -349,6 +358,7 @@ export default class HandoffCompaction extends BasicCompactionEngine {
349
358
  toolHistory: agent.session.toolHistory(),
350
359
  ...(input.tools === undefined ? {} : { tools: [...input.tools] }),
351
360
  maxTokens: this.config.maxTokens,
361
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
352
362
  sessionId: agent.session.id,
353
363
  purpose: "compaction",
354
364
  ...(signal === undefined ? {} : { signal }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-compaction",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -40,5 +40,8 @@
40
40
  ],
41
41
  "publishConfig": {
42
42
  "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "js-yaml": "^4.1.0"
43
46
  }
44
47
  }
@@ -2,42 +2,141 @@
2
2
  * preset-scope.mjs — switch an editable DSH preset between the handoff plugin and
3
3
  * DSH's native compaction, reversibly.
4
4
  *
5
- * A preset context can only have ONE provider of `ctx.compaction`, so "native off"
6
- * means "which row is active". The script replaces a marker-delimited slot in the
7
- * preset's cordis.patch.yml with either row file, validates the resulting YAML, and
8
- * keeps a .bak-scope copy. Disabling restores the stock row (`auto: true`), so a
9
- * preset goes back to DSH native compaction the moment the plugin is switched off.
5
+ * Two layers must agree for "native off" to be real:
6
+ * 1. the preset's marker-delimited compaction slot (the row that supplies
7
+ * `ctx.compaction` inside the preset group), and
8
+ * 2. the profile-level root `compaction-basic` node (DSH's default engine,
9
+ * which also listens on `agent/pre-step` and can compact before the
10
+ * preset's isolated plugin is consulted).
11
+ *
12
+ * Layer 1 is replaced wholesale between row-plugin.yml and row-native.yml.
13
+ * Layer 2 is guarded by a marker comment that sets `auto: false` while the
14
+ * plugin is enabled and is removed on disable, restoring the stock default.
15
+ *
16
+ * Without --preset/--all-local, `enable` reads the plugin settings sidecar:
17
+ * `scope: "preset"` switches exactly the listed preset ids to the plugin and
18
+ * resets every other editable preset to native; `scope: "global"` switches all.
19
+ * `disable` restores native everywhere. The root guard is on only while at
20
+ * least one preset uses the plugin.
10
21
  *
11
22
  * Usage:
12
- * node scripts/preset-scope.mjs status [--preset <cordis.patch.yml>] [--all-local]
13
- * node scripts/preset-scope.mjs enable [--preset <cordis.patch.yml>] [--all-local]
14
- * node scripts/preset-scope.mjs disable [--preset <cordis.patch.yml>] [--all-local]
23
+ * node scripts/preset-scope.mjs status [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
24
+ * node scripts/preset-scope.mjs enable [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
25
+ * node scripts/preset-scope.mjs disable [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
15
26
  * Restart DSH afterwards.
16
27
  */
17
28
  import fs from "node:fs";
29
+ import { homedir } from "node:os";
18
30
  import path from "node:path";
19
31
  import { fileURLToPath } from "node:url";
20
32
  import { createRequire } from "node:module";
21
33
 
22
34
  const HERE = path.dirname(fileURLToPath(import.meta.url));
23
- const DEFAULT_PRESET = "C:/Users/Me/.dsh-local-pkgs/dsh-preset-lightweight-local/cordis.patch.yml";
24
- const LOCAL_PKGS = "C:/Users/Me/.dsh-local-pkgs";
35
+ const APP_DATA = process.env.APPDATA ?? path.join(homedir(), "AppData", "Roaming");
36
+ const DEFAULT_PROFILE_PATCH = process.env.DSH_PROFILE_PATCH
37
+ ?? path.join(APP_DATA, "dsh-desktop", "harness", "profiles", "web", "cordis.patch.yml");
38
+ const DEFAULT_SETTINGS = path.join(homedir(), ".dsh-handoff-compaction", "settings.json");
39
+ const LOCAL_PKGS = process.env.DSH_LOCAL_PKGS ?? path.join(homedir(), ".dsh-local-pkgs");
40
+ const SETTINGS_FILE = process.env.DSH_HANDOFF_SETTINGS_FILE ?? DEFAULT_SETTINGS;
25
41
  const SLOT_BEGIN = "# >>> handoff:slot:begin";
26
42
  const SLOT_END = "# <<< handoff:slot:end";
43
+ const GUARD_BEGIN = "# >>> handoff:native-guard:begin";
44
+ const GUARD_END = "# <<< handoff:native-guard:end";
27
45
  const ROW_INDENT = 14;
28
46
 
29
47
  const argv = process.argv.slice(2);
30
48
  const mode = (argv.find((a) => !a.startsWith("--")) ?? "status").toLowerCase();
31
49
  const presetArg = argv.indexOf("--preset");
50
+ const profileArg = argv.indexOf("--profile-patch");
32
51
  const allLocal = argv.includes("--all-local");
52
+ const skipProfile = argv.includes("--skip-profile");
53
+ const profilePatch = profileArg >= 0
54
+ ? argv[profileArg + 1]
55
+ : (process.env.DSH_PROFILE_PATCH ?? DEFAULT_PROFILE_PATCH);
56
+
57
+ function readSettings() {
58
+ try {
59
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
60
+ return parsed && typeof parsed === "object" ? parsed : undefined;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ function presetIdOf(text) {
67
+ const yaml = loadYaml();
68
+ if (yaml === undefined) return undefined;
69
+ try {
70
+ const doc = yaml.load(text, { schema: yamlSchema(yaml) });
71
+ return doc?.[0]?.insert?.[0]?.config?.id;
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ /** Every editable local preset that carries the handoff slot markers. */
78
+ function discoverPresetFiles() {
79
+ if (!fs.existsSync(LOCAL_PKGS)) return [];
80
+ return fs.readdirSync(LOCAL_PKGS)
81
+ .map((name) => path.join(LOCAL_PKGS, name, "cordis.patch.yml"))
82
+ .filter((file) => fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(SLOT_BEGIN));
83
+ }
33
84
 
34
- function targets() {
35
- if (allLocal) {
36
- return fs.readdirSync(LOCAL_PKGS)
37
- .map((name) => path.join(LOCAL_PKGS, name, "cordis.patch.yml"))
38
- .filter((file) => fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(SLOT_BEGIN));
85
+ function defaultPresetFile() {
86
+ return discoverPresetFiles()[0]
87
+ ?? path.join(LOCAL_PKGS, "dsh-preset-lightweight-local", "cordis.patch.yml");
88
+ }
89
+
90
+ /**
91
+ * Rows to apply. `enable: true` means the plugin takes this preset; `false`
92
+ * restores its own native row. Non-selected presets are reset to native so a
93
+ * previous switch cannot leak into a preset the user did not select.
94
+ */
95
+ function selection(action) {
96
+ if (presetArg >= 0) {
97
+ return [{ file: argv[presetArg + 1], enable: action === "enable" }];
98
+ }
99
+ const files = discoverPresetFiles();
100
+ if (action === "disable") return files.map((file) => ({ file, enable: false }));
101
+ if (allLocal) return files.map((file) => ({ file, enable: true }));
102
+ const settings = readSettings();
103
+ if (settings?.scope === "global") return files.map((file) => ({ file, enable: true }));
104
+ if (settings?.scope === "preset") {
105
+ const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
106
+ const planned = files.map((file) => {
107
+ const id = presetIdOf(fs.readFileSync(file, "utf8"));
108
+ return { file, id, enable: id !== undefined && selected.has(id) };
109
+ });
110
+ for (const id of selected) {
111
+ if (!planned.some((entry) => entry.id === id)) {
112
+ console.error(`warn: selected preset "${id}" has no editable handoff slot; left native`);
113
+ }
114
+ }
115
+ return planned;
39
116
  }
40
- return [presetArg >= 0 ? argv[presetArg + 1] : DEFAULT_PRESET];
117
+ return [{ file: defaultPresetFile(), enable: action === "enable" }];
118
+ }
119
+
120
+ function statusPlan() {
121
+ if (presetArg >= 0) return [{ file: argv[presetArg + 1] }];
122
+ const settings = readSettings();
123
+ const files = discoverPresetFiles();
124
+ if (settings?.scope === "global") return files.map((file) => ({ file, selected: true }));
125
+ if (settings?.scope === "preset") {
126
+ const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
127
+ return files.map((file) => ({ file, selected: selected.has(presetIdOf(fs.readFileSync(file, "utf8"))) }));
128
+ }
129
+ return [{ file: defaultPresetFile() }];
130
+ }
131
+
132
+ function pluginRow(presetId) {
133
+ const text = fs.readFileSync(path.join(HERE, "row-plugin.yml"), "utf8");
134
+ const trace = path.join(homedir(), ".dsh-handoff-compaction", "trace.log").replace(/\\/g, "/");
135
+ const withId = (presetId === undefined
136
+ ? text
137
+ : text.replace(/^(\s*presetId:\s*).*$/m, `$1${presetId}`))
138
+ .replace(/^(\s*traceFile:\s*).*$/m, `$1"${trace}"`);
139
+ return indentRow(withId);
41
140
  }
42
141
 
43
142
  function indentRow(text) {
@@ -47,23 +146,43 @@ function indentRow(text) {
47
146
 
48
147
  function loadYaml() {
49
148
  try {
50
- return createRequire("C:/Users/Me/AppData/Roaming/dsh-desktop/harness/profiles/web/")("js-yaml");
149
+ return createRequire(import.meta.url)("js-yaml");
150
+ } catch {
151
+ // The engine's own dependency is preferred; a profile-installed copy is the fallback.
152
+ }
153
+ try {
154
+ const profileRoot = path.join(APP_DATA, "dsh-desktop", "harness", "profiles", "web");
155
+ return createRequire(path.join(profileRoot, "package.json"))("js-yaml");
51
156
  } catch {
52
157
  return undefined;
53
158
  }
54
159
  }
55
160
 
56
- function validate(text) {
161
+ function yamlSchema(yaml) {
162
+ const jsTag = new yaml.Type("tag:yaml.org,2002:js", { kind: "scalar", construct: (d) => d });
163
+ return yaml.DEFAULT_SCHEMA.extend([jsTag]);
164
+ }
165
+
166
+ function validatePreset(text) {
57
167
  const yaml = loadYaml();
58
168
  if (yaml === undefined) return "skipped (js-yaml unavailable)";
59
- const jsTag = new yaml.Type("tag:yaml.org,2002:js", { kind: "scalar", construct: (d) => d });
60
- const doc = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA.extend([jsTag]) });
169
+ const doc = yaml.load(text, { schema: yamlSchema(yaml) });
61
170
  const group = doc[0].insert[0].config.plugins.find((entry) => entry.id === "compaction");
62
171
  const rows = group.config.filter((entry) => entry.id === "compaction-basic");
63
172
  if (rows.length !== 1) throw new Error(`expected exactly one compaction-basic row, found ${rows.length}`);
64
173
  return `ok -> ${rows[0].name}`;
65
174
  }
66
175
 
176
+ function validateProfile(text) {
177
+ const yaml = loadYaml();
178
+ if (yaml === undefined) return "skipped (js-yaml unavailable)";
179
+ const doc = yaml.load(text, { schema: yamlSchema(yaml) });
180
+ if (!Array.isArray(doc)) throw new Error("profile patch: expected a top-level array");
181
+ const row = doc.find((entry) => entry?.id === "compaction-basic" && entry?.name === "@deepseek-ai/dsh-compaction-basic");
182
+ if (row === undefined) throw new Error("profile patch: native compaction-basic entry not found");
183
+ return `ok -> ${row.name}, auto=${row.config?.auto === undefined ? "default" : String(row.config.auto)}`;
184
+ }
185
+
67
186
  function slotBounds(lines) {
68
187
  const begin = lines.findIndex((line) => line.includes(SLOT_BEGIN));
69
188
  const end = lines.findIndex((line) => line.includes(SLOT_END));
@@ -81,7 +200,148 @@ function activeName(lines) {
81
200
  return "unknown";
82
201
  }
83
202
 
84
- for (const file of targets()) {
203
+ /** Locate the top-level native compaction-basic entry that carries a name + config. */
204
+ function nativeBounds(lines) {
205
+ const starts = [];
206
+ for (let i = 0; i < lines.length; i += 1) {
207
+ if (/^-\s+id:\s*compaction-basic\s*$/.test(lines[i])) starts.push(i);
208
+ }
209
+ for (let index = 0; index < starts.length; index += 1) {
210
+ const start = starts[index];
211
+ const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
212
+ const block = lines.slice(start, end).join("\n");
213
+ if (/name:\s*["']?@deepseek-ai\/dsh-compaction-basic["']?/.test(block) && /^\s{2}config:\s*$/m.test(block)) {
214
+ return { start, end };
215
+ }
216
+ }
217
+ return undefined;
218
+ }
219
+
220
+ function configLine(lines, bounds) {
221
+ for (let i = bounds.start + 1; i < bounds.end; i += 1) {
222
+ if (/^\s{2}config:\s*$/.test(lines[i])) return i;
223
+ }
224
+ return -1;
225
+ }
226
+
227
+ function guardBounds(lines, bounds) {
228
+ let begin = -1;
229
+ let end = -1;
230
+ for (let i = bounds.start; i < bounds.end; i += 1) {
231
+ if (lines[i].includes(GUARD_BEGIN)) begin = i;
232
+ if (lines[i].includes(GUARD_END)) end = i;
233
+ }
234
+ return begin >= 0 && end > begin ? { begin, end } : undefined;
235
+ }
236
+
237
+ function autoLine(lines, configIdx, bounds) {
238
+ for (let i = configIdx + 1; i < bounds.end; i += 1) {
239
+ if (/^- /.test(lines[i])) break;
240
+ if (/^\s{2}[A-Za-z]/.test(lines[i])) break;
241
+ if (/^\s{4}auto:\s*/.test(lines[i])) return i;
242
+ }
243
+ return -1;
244
+ }
245
+
246
+ function guardState(lines, bounds) {
247
+ const cfg = configLine(lines, bounds);
248
+ if (cfg < 0) return "no config";
249
+ const guard = guardBounds(lines, bounds);
250
+ if (guard !== undefined) {
251
+ const hasFalse = lines.slice(guard.begin + 1, guard.end).some((line) => /^\s*auto:\s*false\s*$/.test(line));
252
+ return hasFalse ? "auto:false (plugin guard)" : "guarded (auto missing!)";
253
+ }
254
+ const auto = autoLine(lines, cfg, bounds);
255
+ return auto < 0 ? "absent (default true)" : `auto:${lines[auto].split(":")[1].trim()}`;
256
+ }
257
+
258
+ function editProfilePatch(file, action) {
259
+ if (!fs.existsSync(file)) {
260
+ console.error(`skip (missing profile patch): ${file}`);
261
+ return;
262
+ }
263
+ const original = fs.readFileSync(file, "utf8");
264
+ const lines = original.split(/\r?\n/);
265
+ const bounds = nativeBounds(lines);
266
+ if (bounds === undefined) {
267
+ console.error(`skip (no native compaction-basic entry): ${file}`);
268
+ return;
269
+ }
270
+ const cfg = configLine(lines, bounds);
271
+ if (cfg < 0) {
272
+ console.error(`skip (native entry has no config block): ${file}`);
273
+ return;
274
+ }
275
+ const guard = guardBounds(lines, bounds);
276
+ let nextLines = [...lines];
277
+
278
+ if (action === "enable") {
279
+ if (guard !== undefined) {
280
+ const hasAuto = nextLines.slice(guard.begin + 1, guard.end).some((line) => /^\s*auto:\s*/.test(line));
281
+ nextLines = nextLines.map((line, index) => (index > guard.begin && index < guard.end && /^\s*auto:\s*/.test(line)
282
+ ? line.replace(/^(\s*)auto:\s*.*$/, "$1auto: false")
283
+ : line));
284
+ if (!hasAuto) nextLines.splice(guard.begin + 1, 0, " auto: false");
285
+ } else {
286
+ const auto = autoLine(nextLines, cfg, bounds);
287
+ if (auto >= 0) {
288
+ nextLines[auto] = nextLines[auto].replace(/^(\s*)auto:\s*.*$/, "$1auto: false");
289
+ } else {
290
+ nextLines.splice(cfg + 1, 0,
291
+ " " + GUARD_BEGIN,
292
+ " auto: false",
293
+ " " + GUARD_END);
294
+ }
295
+ }
296
+ } else {
297
+ if (guard !== undefined) {
298
+ nextLines.splice(guard.begin, guard.end - guard.begin + 1);
299
+ } else {
300
+ const auto = autoLine(nextLines, cfg, bounds);
301
+ if (auto >= 0) nextLines[auto] = nextLines[auto].replace(/^(\s*)auto:\s*.*$/, "$1auto: true");
302
+ }
303
+ }
304
+
305
+ const next = nextLines.join("\n");
306
+ if (next === original) {
307
+ console.log(`${file}\n -> native ${action === "enable" ? "auto:false" : "auto:true"} [no change]`);
308
+ return;
309
+ }
310
+ const verdict = validateProfile(next);
311
+ if (!verdict.startsWith("ok")) {
312
+ console.error(`aborted (validation): ${verdict}`);
313
+ process.exitCode = 1;
314
+ return;
315
+ }
316
+ fs.copyFileSync(file, `${file}.bak-scope`);
317
+ fs.writeFileSync(file, next);
318
+ console.log(`${file}\n -> native ${action === "enable" ? "auto:false" : "auto:true"} [${verdict}]`);
319
+ }
320
+
321
+ function profileStatus() {
322
+ if (skipProfile) return;
323
+ if (!fs.existsSync(profilePatch)) {
324
+ console.log(`${profilePatch}\n missing`);
325
+ return;
326
+ }
327
+ const original = fs.readFileSync(profilePatch, "utf8");
328
+ const lines = original.split(/\r?\n/);
329
+ const bounds = nativeBounds(lines);
330
+ if (bounds === undefined) {
331
+ console.log(`${profilePatch}\n native compaction-basic entry not found`);
332
+ return;
333
+ }
334
+ console.log(`${profilePatch}\n 原生守卫: ${guardState(lines, bounds)}\n YAML: ${validateProfile(original)}`);
335
+ }
336
+
337
+ if (mode !== "status" && mode !== "enable" && mode !== "disable") {
338
+ console.error("usage: preset-scope.mjs <status|enable|disable> [--preset <file>] [--profile-patch <file>] [--all-local] [--skip-profile]");
339
+ process.exit(2);
340
+ }
341
+
342
+ const plan = mode === "status" ? statusPlan() : selection(mode);
343
+ for (const entry of plan) {
344
+ const file = entry.file;
85
345
  if (!fs.existsSync(file)) {
86
346
  console.error(`skip (missing): ${file}`);
87
347
  continue;
@@ -96,27 +356,34 @@ for (const file of targets()) {
96
356
 
97
357
  if (mode === "status") {
98
358
  const current = activeName(lines);
99
- const kind = current === "dsh-handoff-compaction" ? "插件接管(原生关闭)"
359
+ const kind = current === "dsh-handoff-compaction" || current === "dsh-auto-compaction" ? "插件接管(原生关闭)"
100
360
  : current === "@deepseek-ai/dsh-compaction-basic" ? "DSH 原生压缩"
101
361
  : "未知";
102
- console.log(`${file}\n 当前: ${current}\n 含义: ${kind}\n YAML: ${validate(original)}`);
362
+ const selected = entry.selected === undefined ? "" : `\n 勾选: ${entry.selected ? "是(插件)" : "否(原生)"}`;
363
+ console.log(`${file}\n 当前: ${current}\n 含义: ${kind}${selected}\n YAML: ${validatePreset(original)}`);
103
364
  continue;
104
365
  }
105
366
 
106
- if (mode !== "enable" && mode !== "disable") {
107
- console.error("usage: preset-scope.mjs <status|enable|disable> [--preset <file>] [--all-local]");
108
- process.exit(2);
367
+ const id = presetIdOf(original);
368
+ if (entry.enable && id === undefined) {
369
+ console.error(`skip (cannot read preset id): ${file}`);
370
+ continue;
109
371
  }
110
-
111
- const rowFile = path.join(HERE, mode === "enable" ? "row-plugin.yml" : "row-native.yml");
112
- const row = indentRow(fs.readFileSync(rowFile, "utf8"));
372
+ const row = entry.enable
373
+ ? pluginRow(id)
374
+ : indentRow(fs.readFileSync(path.join(HERE, "row-native.yml"), "utf8"));
113
375
  const next = [
114
376
  ...lines.slice(0, bounds.begin + 1),
115
377
  ...row,
116
378
  ...lines.slice(bounds.end),
117
379
  ].join("\n");
118
380
 
119
- const verdict = validate(next);
381
+ if (next === original) {
382
+ console.log(`${file}\n -> ${activeName(lines)} [no change]`);
383
+ continue;
384
+ }
385
+
386
+ const verdict = validatePreset(next);
120
387
  if (!verdict.startsWith("ok")) {
121
388
  console.error(`aborted (validation): ${verdict}`);
122
389
  process.exit(1);
@@ -126,4 +393,14 @@ for (const file of targets()) {
126
393
  console.log(`${file}\n -> ${activeName(next.split(/\r?\n/))} [${verdict}]`);
127
394
  }
128
395
 
129
- if (mode === "enable" || mode === "disable") console.log("重启 DSH 后生效。");
396
+ if (mode === "status") {
397
+ profileStatus();
398
+ } else {
399
+ // The root guard only disables DSH's profile-level duplicate engine. Each
400
+ // preset's own compaction row stays native unless the plan selected it, so
401
+ // this is not "native off for every preset".
402
+ const anyPlugin = plan.some((entry) => entry.enable);
403
+ if (!skipProfile) editProfilePatch(profilePatch, mode === "enable" && anyPlugin ? "enable" : "disable");
404
+ if (mode === "enable" && !anyPlugin) console.error("warn: no selected preset uses the plugin; root native fallback left on");
405
+ console.log("重启 DSH 后生效。");
406
+ }
@@ -7,4 +7,4 @@
7
7
  idleDelayMs: 1500
8
8
  dryRun: false
9
9
  presetId: lightweight-local
10
- traceFile: "C:/Users/Me/.dsh-handoff-work/trace.log"
10
+ traceFile: ""