dsh-engram 0.6.0 → 0.7.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.md +155 -39
- package/README.zh.md +119 -36
- package/cordis.patch.yml +18 -5
- package/docs/CONTEXT-GC-GUIDE.zh.md +35 -6
- package/docs/PROPOSAL-evolution.zh.md +25 -1
- package/docs/RELEASE-NOTES-v0.7.0.md +57 -0
- package/docs/RELEASE-NOTES-v0.7.1.md +88 -0
- package/lib/anchor.js +124 -0
- package/lib/boundary-prune.js +27 -0
- package/lib/client.js +7 -2
- package/lib/compaction.js +11 -0
- package/lib/context-gc.js +47 -0
- package/lib/entity-page.js +200 -0
- package/lib/episode.js +163 -0
- package/lib/index-block.js +74 -4
- package/lib/index.js +136 -9
- package/lib/settings.js +6 -0
- package/lib/store.js +48 -5
- package/lib/tools.js +25 -3
- package/lib/web-provision.js +324 -91
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +196 -53
package/scripts/dsh-engram.mjs
CHANGED
|
@@ -10,16 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* `status`/`doctor` read the durable snapshot the plugin writes at boot
|
|
12
12
|
* (`$DSH_HOME/engram/context-gc.status.json`) and cross-check the live preset
|
|
13
|
-
* files
|
|
14
|
-
*
|
|
13
|
+
* files — both preset generations: legacy compositions
|
|
14
|
+
* (`<root>/<id>/agent.cordis.yml`, DSH ≤ 0.1.x) and the declarative shipped
|
|
15
|
+
* presets (`@deepseek-ai/dsh-web-app/presets/<id>.patch.yml`, DSH ≥ 0.2.0).
|
|
16
|
+
* `enable`/`revert` act on the discovered presets (or a single file via
|
|
17
|
+
* `--file <path>`; `--presets-dir <path>` overrides discovery).
|
|
15
18
|
*/
|
|
16
19
|
import { readFile, readdir, access } from "node:fs/promises";
|
|
17
20
|
import { readFileSync } from "node:fs";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
18
22
|
import { dirname, basename, resolve, join } from "node:path";
|
|
19
23
|
import { homedir } from "node:os";
|
|
20
24
|
import { fileURLToPath } from "node:url";
|
|
21
25
|
import {
|
|
22
26
|
compactionWireStatus,
|
|
27
|
+
discoverPresetsDir,
|
|
23
28
|
provisionWebCompaction,
|
|
24
29
|
revertWebCompaction,
|
|
25
30
|
} from "../lib/web-provision.js";
|
|
@@ -50,32 +55,61 @@ async function compositionIn(dir) {
|
|
|
50
55
|
return undefined;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
const roots = [];
|
|
56
|
-
const seen = new Set();
|
|
57
|
-
const push = (root) => {
|
|
58
|
-
const abs = resolve(root);
|
|
59
|
-
if (seen.has(abs)) return;
|
|
60
|
-
seen.add(abs);
|
|
61
|
-
roots.push(abs);
|
|
62
|
-
};
|
|
63
|
-
// Recorded directly from the last boot (authoritative for real installs).
|
|
64
|
-
for (const entry of status?.web?.presets ?? []) {
|
|
65
|
-
if (typeof entry?.path === "string") push(dirname(entry.path));
|
|
66
|
-
}
|
|
67
|
-
// Well-known roots: shipped harness + the user root.
|
|
68
|
-
push(resolve(REPO_ROOT, "..", "deepseek-harness/apps/cli/config/agent-presets"));
|
|
58
|
+
/** Well-known LEGACY preset roots (DSH ≤ 0.1.x: one directory per preset). */
|
|
59
|
+
function legacyPresetRoots() {
|
|
69
60
|
const dshHome = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
return [
|
|
62
|
+
resolve(REPO_ROOT, "..", "deepseek-harness/apps/cli/config/agent-presets"),
|
|
63
|
+
join(dshHome, ".agent-presets"),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The `dsh` bin on PATH — anchors declarative preset discovery. */
|
|
68
|
+
function dshBinAnchor() {
|
|
69
|
+
try {
|
|
70
|
+
const found = execFileSync("which", ["dsh"], { encoding: "utf8" }).trim();
|
|
71
|
+
return found.length > 0 ? found : undefined;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
+
/** Preset id from a composition file path (both generations). */
|
|
78
|
+
function presetIdFromPath(path) {
|
|
79
|
+
const base = basename(path);
|
|
80
|
+
if (base.endsWith(".patch.yml")) return base.slice(0, -".patch.yml".length);
|
|
81
|
+
return basename(dirname(path));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Discover preset compositions across BOTH generations, first match per id
|
|
86
|
+
* wins (the roster shadows duplicates the same way):
|
|
87
|
+
* 1. paths recorded by the last boot (authoritative for real installs);
|
|
88
|
+
* 2. legacy roots (`<root>/<id>/agent.cordis.yml` or `agent.json`);
|
|
89
|
+
* 3. declarative shipped presets (`<presetsDir>/<id>.patch.yml`).
|
|
90
|
+
*/
|
|
91
|
+
async function discoverPresets(status, presetsDirFlag) {
|
|
77
92
|
const presets = [];
|
|
78
|
-
|
|
93
|
+
const seenId = new Set();
|
|
94
|
+
const seenPath = new Set();
|
|
95
|
+
const push = (id, path) => {
|
|
96
|
+
if (typeof path !== "string" || path.length === 0) return;
|
|
97
|
+
if (seenId.has(id) || seenPath.has(path)) return;
|
|
98
|
+
seenId.add(id);
|
|
99
|
+
seenPath.add(path);
|
|
100
|
+
presets.push({ id, path });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 1. Recorded by the last boot.
|
|
104
|
+
for (const entry of status?.web?.presets ?? []) {
|
|
105
|
+
if (typeof entry?.path !== "string") continue;
|
|
106
|
+
if (!(await exists(entry.path))) continue;
|
|
107
|
+
const id = typeof entry.presetId === "string" ? entry.presetId : presetIdFromPath(entry.path);
|
|
108
|
+
push(id, entry.path);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Legacy roots.
|
|
112
|
+
for (const root of legacyPresetRoots()) {
|
|
79
113
|
let entries = [];
|
|
80
114
|
try {
|
|
81
115
|
entries = await readdir(root, { withFileTypes: true });
|
|
@@ -84,11 +118,27 @@ async function discoverPresets(status) {
|
|
|
84
118
|
}
|
|
85
119
|
for (const entry of entries) {
|
|
86
120
|
if (!entry.isDirectory()) continue;
|
|
87
|
-
if (seen.has(entry.name)) continue;
|
|
88
121
|
const file = await compositionIn(join(root, entry.name));
|
|
89
122
|
if (file === undefined) continue;
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
push(entry.name, file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Declarative shipped presets.
|
|
128
|
+
const presetsDir = discoverPresetsDir({
|
|
129
|
+
presetsDir: presetsDirFlag,
|
|
130
|
+
anchors: [dshBinAnchor()].filter((anchor) => typeof anchor === "string"),
|
|
131
|
+
});
|
|
132
|
+
if (presetsDir !== undefined) {
|
|
133
|
+
let entries = [];
|
|
134
|
+
try {
|
|
135
|
+
entries = await readdir(presetsDir, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
entries = [];
|
|
138
|
+
}
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isFile() || !entry.name.endsWith(".patch.yml")) continue;
|
|
141
|
+
push(entry.name.slice(0, -".patch.yml".length), join(presetsDir, entry.name));
|
|
92
142
|
}
|
|
93
143
|
}
|
|
94
144
|
return presets;
|
|
@@ -112,9 +162,35 @@ function webLine(entry) {
|
|
|
112
162
|
}
|
|
113
163
|
}
|
|
114
164
|
|
|
115
|
-
|
|
165
|
+
/** One-line label for the web PLANE (distinct from a single preset report). */
|
|
166
|
+
function webPlaneLine(web) {
|
|
167
|
+
const action = web?.action ?? "?";
|
|
168
|
+
const count = (web?.presets ?? []).length;
|
|
169
|
+
switch (action) {
|
|
170
|
+
case "pending": return "启动中(agentPresets 尚未就绪)";
|
|
171
|
+
case "disabled": return "未启用(autoWebCompaction=false;或用 npx dsh-engram enable 手动接管)";
|
|
172
|
+
case "no-default": return "未发现可用预设";
|
|
173
|
+
case "failed": return `失败 · ${web?.error ?? "?"}`;
|
|
174
|
+
default: return `上次装配 ${action},${count} 个预设`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The snapshot's web plane, normalized against the recorded opt-in: a `pending`
|
|
180
|
+
* plane while `autoWebCompaction=false` is impossible under the current plugin
|
|
181
|
+
* (that state is `disabled`); old snapshots written before v0.7.1 can carry it.
|
|
182
|
+
*/
|
|
183
|
+
function effectiveWebPlane(status) {
|
|
184
|
+
const web = status.web;
|
|
185
|
+
if (status.configSource?.autoWebCompaction === false && (web === undefined || web.action === undefined || web.action === "pending")) {
|
|
186
|
+
return { action: "disabled", presets: web?.presets ?? [] };
|
|
187
|
+
}
|
|
188
|
+
return web;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function printStatus(presetsDirFlag) {
|
|
116
192
|
const status = await readContextGcStatus();
|
|
117
|
-
const presets = await discoverPresets(status);
|
|
193
|
+
const presets = await discoverPresets(status, presetsDirFlag);
|
|
118
194
|
|
|
119
195
|
log("── dsh-engram Context GC 状态 ──────────────────────────────");
|
|
120
196
|
if (status === undefined) {
|
|
@@ -122,14 +198,31 @@ async function printStatus() {
|
|
|
122
198
|
} else {
|
|
123
199
|
log(` 状态快照:${new Date(status.writtenAt ?? 0).toLocaleString()}(插件上次启动时写入)`);
|
|
124
200
|
log(` host 平面:${hostLine(status.host)}`);
|
|
125
|
-
log(` web 平面:${
|
|
201
|
+
log(` web 平面:${webPlaneLine(effectiveWebPlane(status))}`);
|
|
126
202
|
log(` 配置:autoWebCompaction=${status.configSource?.autoWebCompaction ?? "?"} · gcReplacesCompaction=${status.configSource?.gcReplacesCompaction ?? "?"} · gcNarrative=${status.configSource?.gcNarrative ?? "?"}`);
|
|
203
|
+
if (status.boundaryPrune !== undefined) {
|
|
204
|
+
const bp = status.boundaryPrune;
|
|
205
|
+
log(` L1 边界剪枝:${bp.armed ? "已武装" : "未启用"}${bp.reason ? `(${bp.reason})` : ""}`);
|
|
206
|
+
if (!bp.armed) {
|
|
207
|
+
log(" ⚠ L1 未武装时「节省 · 剪枝/压缩」不会入账(账本净 token 只会是负值);0.2.0 web profile 需要 bundle patch 重新启用 host 平面的 tool-result-pruner 行。");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (status.configSource?.autoWebCompaction === false) {
|
|
211
|
+
log(" ⚠ autoWebCompaction=false:web 平面不会自动接管(设置卡「记忆 GC」里打开后重启 dsh web)");
|
|
212
|
+
}
|
|
127
213
|
}
|
|
128
214
|
|
|
129
215
|
log("");
|
|
130
216
|
if (presets.length === 0) {
|
|
131
|
-
log(" 未发现任何 web
|
|
217
|
+
log(" 未发现任何 web 代理预设(声明式 presets 目录、旧版 shipped 根与用户根都找不到)。");
|
|
218
|
+
log(" 提示:声明式预设需要能找到 dsh 安装(PATH 上的 dsh 或在 dsh 进程内运行),也可用 --presets-dir 指定。");
|
|
132
219
|
} else {
|
|
220
|
+
const declarative = presets.filter((preset) => preset.path.endsWith(".patch.yml")).length;
|
|
221
|
+
if (declarative > 0) {
|
|
222
|
+
log(` 预设布局:声明式(dsh-web-app/presets/*.patch.yml,DSH ≥ 0.2.0)`);
|
|
223
|
+
} else {
|
|
224
|
+
log(" 预设布局:旧版组合文件(agent.cordis.yml,DSH ≤ 0.1.x)");
|
|
225
|
+
}
|
|
133
226
|
log(` ── web 预设现场(${presets.length} 个)──`);
|
|
134
227
|
for (const preset of presets) {
|
|
135
228
|
let text;
|
|
@@ -151,22 +244,27 @@ async function printStatus() {
|
|
|
151
244
|
return status;
|
|
152
245
|
}
|
|
153
246
|
|
|
154
|
-
async function printDoctor() {
|
|
155
|
-
const status = await printStatus();
|
|
247
|
+
async function printDoctor(presetsDirFlag) {
|
|
248
|
+
const status = await printStatus(presetsDirFlag);
|
|
156
249
|
log("");
|
|
157
250
|
log("── 建议(doctor)──────────────────────────────────────────");
|
|
158
|
-
const
|
|
159
|
-
const presets = await discoverPresets(status);
|
|
251
|
+
const presets = await discoverPresets(status, presetsDirFlag);
|
|
160
252
|
const issues = [];
|
|
253
|
+
const notes = [];
|
|
161
254
|
|
|
162
255
|
if (status === undefined) {
|
|
163
256
|
issues.push("尚无状态快照:把 dsh-engram 装进 profile 并启动一次(dsh web / dsh headless),插件会在启动时写入状态与自动装配。");
|
|
164
|
-
} else
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
257
|
+
} else {
|
|
258
|
+
if (status.host !== "context-gc") {
|
|
259
|
+
issues.push(
|
|
260
|
+
`host 平面 ${status.host}:${status.host === "unavailable"
|
|
261
|
+
? "引擎不可用——确认 @deepseek-ai/dsh-compaction-basic 依赖已安装(见启动日志 dsh-compaction-basic unavailable)。"
|
|
262
|
+
: "当前是 DSH 默认 LLM 摘要——要在 host 平面启用 Context GC,把 gcReplacesCompaction 设为 true。"}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (status.configSource?.autoWebCompaction === false) {
|
|
266
|
+
issues.push("autoWebCompaction=false:web 平面不会自动接管(即便预设仍是 stock 布局)。在设置卡「记忆 GC」里打开,或改 profile patch 后重启 dsh web。");
|
|
267
|
+
}
|
|
170
268
|
}
|
|
171
269
|
|
|
172
270
|
const stockLeft = presets.filter((p) => {
|
|
@@ -177,7 +275,7 @@ async function printDoctor() {
|
|
|
177
275
|
}
|
|
178
276
|
});
|
|
179
277
|
if (stockLeft.length > 0) {
|
|
180
|
-
issues.push(`web 平面还有 ${stockLeft.length} 个出厂布局预设未接管(${stockLeft.map((p) => p.id).join("、")}):重启 dsh web
|
|
278
|
+
issues.push(`web 平面还有 ${stockLeft.length} 个出厂布局预设未接管(${stockLeft.map((p) => p.id).join("、")}):重启 dsh web 让自动装配跑一次(需 autoWebCompaction=true),或 npx dsh-engram enable。`);
|
|
181
279
|
}
|
|
182
280
|
const customLeft = presets.filter((p) => {
|
|
183
281
|
try {
|
|
@@ -189,14 +287,48 @@ async function printDoctor() {
|
|
|
189
287
|
if (customLeft.length > 0) {
|
|
190
288
|
issues.push(`${customLeft.length} 个预设是自定义/无 compaction 布局(${customLeft.map((p) => p.id).join("、")}),插件不会碰;需要的话手动把 compaction 组换成 dsh-engram/compaction。`);
|
|
191
289
|
}
|
|
192
|
-
|
|
290
|
+
const wired = presets.filter((p) => compactionWireStatus(readFileSyncSafe(p.path)) === "wired");
|
|
291
|
+
if (wired.length > 0) {
|
|
193
292
|
issues.push("有预设已被接管:卸载 dsh-engram 前先 npx dsh-engram revert,避免预设留下悬空引用。");
|
|
293
|
+
if (wired.some((p) => /[\\/]node_modules[\\/]/.test(p.path))) {
|
|
294
|
+
notes.push("已接管的声明式预设在 node_modules 里(npm 更新 dsh 会恢复出厂布局):保持 autoWebCompaction=true 时下次启动 dsh web 会自动重新接管,否则升级后手动 npx dsh-engram enable。");
|
|
295
|
+
}
|
|
194
296
|
}
|
|
297
|
+
const overrides = await presetOverrides();
|
|
298
|
+
if (overrides.length > 0) {
|
|
299
|
+
notes.push(`profile patch 里有 preset 覆写行(${overrides.join("、")}):Web 编辑器保存的 config.plugins 优先于 shipped 文件,改 shipped 文件可能不生效——请在编辑器里改,或删掉这些覆写行。`);
|
|
300
|
+
}
|
|
301
|
+
|
|
195
302
|
if (issues.length === 0) {
|
|
196
303
|
log(" 一切正常:host 平面 + 全部 stock 预设都已接管 Context GC。");
|
|
197
|
-
|
|
304
|
+
} else {
|
|
305
|
+
issues.forEach((line, index) => log(` ${index + 1}. ${line}`));
|
|
306
|
+
}
|
|
307
|
+
notes.forEach((line) => log(` · ${line}`));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** `preset-*` rows saved in profile patches (Web-editor overrides). */
|
|
311
|
+
async function presetOverrides() {
|
|
312
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
313
|
+
const profilesDir = join(dshHome, "profiles");
|
|
314
|
+
let dirs = [];
|
|
315
|
+
try {
|
|
316
|
+
dirs = await readdir(profilesDir, { withFileTypes: true });
|
|
317
|
+
} catch {
|
|
318
|
+
return [];
|
|
198
319
|
}
|
|
199
|
-
|
|
320
|
+
const hits = new Set();
|
|
321
|
+
for (const dir of dirs) {
|
|
322
|
+
if (!dir.isDirectory()) continue;
|
|
323
|
+
let text;
|
|
324
|
+
try {
|
|
325
|
+
text = await readFile(join(profilesDir, dir.name, "cordis.patch.yml"), "utf8");
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
for (const match of text.matchAll(/^\s*- id: (preset-[\w.-]+)\s*$/gm)) hits.add(match[1]);
|
|
330
|
+
}
|
|
331
|
+
return [...hits].sort();
|
|
200
332
|
}
|
|
201
333
|
|
|
202
334
|
/** Read helper that never throws (used by the sync-style filtering above). */
|
|
@@ -216,27 +348,36 @@ function toService(preset) {
|
|
|
216
348
|
};
|
|
217
349
|
}
|
|
218
350
|
|
|
219
|
-
async function runEnableRevert(command, fileFlag) {
|
|
351
|
+
async function runEnableRevert(command, fileFlag, presetsDirFlag) {
|
|
220
352
|
const status = await readContextGcStatus();
|
|
221
353
|
let presets;
|
|
222
354
|
if (fileFlag !== undefined) {
|
|
223
355
|
const abs = resolve(fileFlag);
|
|
224
356
|
if (!(await exists(abs))) throw new Error(`file not found: ${abs}`);
|
|
225
|
-
presets = [{ id:
|
|
357
|
+
presets = [{ id: presetIdFromPath(abs), path: abs }];
|
|
226
358
|
} else {
|
|
227
|
-
presets = await discoverPresets(status);
|
|
359
|
+
presets = await discoverPresets(status, presetsDirFlag);
|
|
228
360
|
}
|
|
229
361
|
if (presets.length === 0) {
|
|
230
|
-
log("未发现可操作预设。用 --file <path>
|
|
362
|
+
log("未发现可操作预设。用 --file <path> 明确指定一个预设组合文件,或用 --presets-dir <path> 指定声明式预设目录。");
|
|
231
363
|
return;
|
|
232
364
|
}
|
|
233
365
|
|
|
366
|
+
// Mirror the engine config the plugin last recorded, so a manual `enable`
|
|
367
|
+
// does not temporarily flip the toggles away from the settings card.
|
|
368
|
+
const rowConfig = status?.configSource !== undefined
|
|
369
|
+
? {
|
|
370
|
+
gcReplacesCompaction: status.configSource.gcReplacesCompaction,
|
|
371
|
+
gcNarrative: status.configSource.gcNarrative,
|
|
372
|
+
}
|
|
373
|
+
: undefined;
|
|
374
|
+
|
|
234
375
|
log(`dsh-engram: ${command} — ${presets.length} 个预设`);
|
|
235
376
|
let changed = 0;
|
|
236
377
|
for (const preset of presets) {
|
|
237
378
|
const service = toService(preset);
|
|
238
379
|
const report = command === "enable"
|
|
239
|
-
? await provisionWebCompaction(service, { log })
|
|
380
|
+
? await provisionWebCompaction(service, { log, rowConfig })
|
|
240
381
|
: await revertWebCompaction(service, { log });
|
|
241
382
|
const entry = report.presets[0];
|
|
242
383
|
log(` ${preset.id}: ${entry?.action ?? report.action}${entry?.reason ? ` (${entry.reason})` : ""}`);
|
|
@@ -251,12 +392,14 @@ async function main() {
|
|
|
251
392
|
const command = rawCommand ?? "status";
|
|
252
393
|
const fileFlagIndex = rest.indexOf("--file");
|
|
253
394
|
const fileFlag = fileFlagIndex >= 0 ? rest[fileFlagIndex + 1] : undefined;
|
|
395
|
+
const presetsDirIndex = rest.indexOf("--presets-dir");
|
|
396
|
+
const presetsDirFlag = presetsDirIndex >= 0 ? rest[presetsDirIndex + 1] : undefined;
|
|
254
397
|
if (command === "status") {
|
|
255
|
-
await printStatus();
|
|
398
|
+
await printStatus(presetsDirFlag);
|
|
256
399
|
} else if (command === "doctor") {
|
|
257
|
-
await printDoctor();
|
|
400
|
+
await printDoctor(presetsDirFlag);
|
|
258
401
|
} else if (command === "enable" || command === "revert") {
|
|
259
|
-
await runEnableRevert(command, fileFlag);
|
|
402
|
+
await runEnableRevert(command, fileFlag, presetsDirFlag);
|
|
260
403
|
} else {
|
|
261
404
|
throw new Error(`未知命令 "${command}"(用 status | doctor | enable | revert)`);
|
|
262
405
|
}
|