dsh-engram 0.7.0 → 0.7.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/README.md +97 -16
- package/README.zh.md +69 -15
- package/cordis.patch.yml +18 -5
- package/docs/CONTEXT-GC-GUIDE.zh.md +37 -6
- package/docs/RELEASE-NOTES-v0.7.1.md +88 -0
- package/docs/RELEASE-NOTES-v0.7.2.md +48 -0
- package/lib/boundary-prune.js +40 -0
- package/lib/client.js +3 -2
- package/lib/compaction.js +11 -0
- package/lib/context-gc.js +47 -0
- package/lib/index.js +74 -11
- package/lib/web-provision.js +324 -91
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +196 -53
package/lib/web-provision.js
CHANGED
|
@@ -26,19 +26,40 @@
|
|
|
26
26
|
* "stock" status fail (status "custom"/"layout-mismatch") and the provisioner
|
|
27
27
|
* leaves the file alone instead of guessing.
|
|
28
28
|
*
|
|
29
|
+
* Two preset generations are supported, and the row matching is
|
|
30
|
+
* INDENTATION-AGNOSTIC (the row is located structurally inside the
|
|
31
|
+
* `compaction` group), because the same row lives at different depths:
|
|
32
|
+
*
|
|
33
|
+
* - DSH ≤ 0.1.x — one composition per preset:
|
|
34
|
+
* `<root>/<id>/agent.cordis.yml` (row at 4 spaces), `preset.path` is known;
|
|
35
|
+
* - DSH ≥ 0.2.0 — declarative presets, one patch file per preset:
|
|
36
|
+
* `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` (row at 14 spaces
|
|
37
|
+
* inside the `- insert:` operation), and `preset.path` does not exist — the
|
|
38
|
+
* id is mapped to the shipped file by {@link discoverPresetsDir}.
|
|
39
|
+
*
|
|
29
40
|
* Flip `autoWebCompaction:false` to opt out; `revertWebCompaction` restores
|
|
30
41
|
* the stock row on uninstall.
|
|
31
42
|
* @module dsh-engram/web-provision
|
|
32
43
|
*/
|
|
33
44
|
|
|
34
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
45
|
+
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
46
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
47
|
+
import { createRequire } from "node:module";
|
|
48
|
+
import { dirname, join } from "node:path";
|
|
35
49
|
|
|
36
50
|
/** The engine package subpath a preset row mounts dsh-engram's engine under. */
|
|
37
51
|
export const ENGRAM_ROW_NAME = "dsh-engram/compaction";
|
|
38
52
|
|
|
39
|
-
/** The stock row the shipped
|
|
40
|
-
export const
|
|
41
|
-
|
|
53
|
+
/** The stock row name the shipped presets mount inside the `compaction` group. */
|
|
54
|
+
export const STOCK_ROW_NAME = "'@deepseek-ai/dsh-compaction-basic'";
|
|
55
|
+
|
|
56
|
+
/** Package name part of {@link STOCK_ROW_NAME} (quote-free, for matching). */
|
|
57
|
+
const STOCK_NAME = "@deepseek-ai/dsh-compaction-basic";
|
|
58
|
+
|
|
59
|
+
/** Row ids the provisioner looks for (list-item `- id:` values). */
|
|
60
|
+
const STOCK_ROW_ID = "compaction-basic";
|
|
61
|
+
const ENGRAM_ROW_ID = "engram-compaction";
|
|
62
|
+
const COMPACTION_GROUP_ID = "compaction";
|
|
42
63
|
|
|
43
64
|
/** Engine config the plugin writes into each wired preset row (settings-driven). */
|
|
44
65
|
export const DEFAULT_ROW_CONFIG = Object.freeze({
|
|
@@ -47,19 +68,37 @@ export const DEFAULT_ROW_CONFIG = Object.freeze({
|
|
|
47
68
|
});
|
|
48
69
|
|
|
49
70
|
/**
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
71
|
+
* The stock `compaction-basic` row at a given indentation (the row sits at
|
|
72
|
+
* different depths across preset generations: 4 in DSH ≤ 0.1.x composition
|
|
73
|
+
* files, 14 in the DSH ≥ 0.2.0 declarative patch files).
|
|
74
|
+
* @param {number} [indent] - leading spaces before `- id:`.
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function stockCompactionRowText(indent = 4) {
|
|
78
|
+
const pad = " ".repeat(indent);
|
|
79
|
+
return `${pad}- id: ${STOCK_ROW_ID}\n${pad} name: ${STOCK_ROW_NAME}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The legacy 4-space stock row (back-compat export). */
|
|
83
|
+
export const STOCK_COMPACTION_ROW = stockCompactionRowText(4);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The engram row text for a given engine config. Indentation must match the
|
|
87
|
+
* stock row it replaces (`- id:` at `indent`, keys at `indent + 2`, sub-keys
|
|
88
|
+
* at `indent + 4`).
|
|
52
89
|
* @param {{ gcReplacesCompaction?: boolean; gcNarrative?: boolean }} [config]
|
|
90
|
+
* @param {number} [indent] - leading spaces before `- id:`.
|
|
53
91
|
* @returns {string}
|
|
54
92
|
*/
|
|
55
|
-
export function engramCompactionRowText(config = {}) {
|
|
93
|
+
export function engramCompactionRowText(config = {}, indent = 4) {
|
|
56
94
|
const { gcReplacesCompaction = true, gcNarrative = true } = config;
|
|
95
|
+
const pad = " ".repeat(indent);
|
|
57
96
|
return [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
`${pad}- id: ${ENGRAM_ROW_ID}`,
|
|
98
|
+
`${pad} name: dsh-engram/compaction`,
|
|
99
|
+
`${pad} config:`,
|
|
100
|
+
`${pad} gcReplacesCompaction: ${gcReplacesCompaction}`,
|
|
101
|
+
`${pad} gcNarrative: ${gcNarrative}`,
|
|
63
102
|
].join("\n");
|
|
64
103
|
}
|
|
65
104
|
|
|
@@ -81,6 +120,109 @@ function codeOf(error) {
|
|
|
81
120
|
: undefined;
|
|
82
121
|
}
|
|
83
122
|
|
|
123
|
+
/** Leading-space width of one line. */
|
|
124
|
+
function indentOf(line) {
|
|
125
|
+
return line.length - line.trimStart().length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `{ indent, id }` for a YAML list item line `- id: <id>`, else undefined. */
|
|
129
|
+
function itemOf(line) {
|
|
130
|
+
const match = /^(\s*)- id:\s*['"]?([^'"\s#]+)['"]?\s*$/.exec(line);
|
|
131
|
+
if (match === null) return undefined;
|
|
132
|
+
return { indent: match[1].length, id: match[2] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* End (exclusive line index) of the YAML block starting at `start`: the first
|
|
137
|
+
* blank line, or the first line indented no deeper than the item itself.
|
|
138
|
+
* @param {string[]} lines
|
|
139
|
+
* @param {number} start
|
|
140
|
+
*/
|
|
141
|
+
function blockEnd(lines, start) {
|
|
142
|
+
const indent = indentOf(lines[start]);
|
|
143
|
+
let end = start + 1;
|
|
144
|
+
while (end < lines.length) {
|
|
145
|
+
const line = lines[end];
|
|
146
|
+
if (line.trim() === "") break;
|
|
147
|
+
if (indentOf(line) <= indent) break;
|
|
148
|
+
end += 1;
|
|
149
|
+
}
|
|
150
|
+
return end;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The `name:` value of the item block starting at `index` (quotes stripped). */
|
|
154
|
+
function nameOf(lines, index) {
|
|
155
|
+
const end = blockEnd(lines, index);
|
|
156
|
+
for (let i = index + 1; i < end; i += 1) {
|
|
157
|
+
const match = /^\s*name:\s*['"]?([^'"\s#]+)['"]?\s*$/.exec(lines[i]);
|
|
158
|
+
if (match !== null) return match[1];
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Spans covering the items of every `compaction` group in the composition (a
|
|
165
|
+
* list item whose block carries `group: true`). The group exists in both
|
|
166
|
+
* preset generations; only its indentation differs, which is why nothing here
|
|
167
|
+
* depends on column positions.
|
|
168
|
+
* @param {string[]} lines
|
|
169
|
+
* @returns {Array<{ start: number; end: number }>}
|
|
170
|
+
*/
|
|
171
|
+
function compactionGroupSpans(lines) {
|
|
172
|
+
const spans = [];
|
|
173
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
174
|
+
const head = itemOf(lines[i]);
|
|
175
|
+
if (head === undefined || head.id !== COMPACTION_GROUP_ID) continue;
|
|
176
|
+
const end = blockEnd(lines, i);
|
|
177
|
+
const isGroup = lines
|
|
178
|
+
.slice(i + 1, end)
|
|
179
|
+
.some((line) => /^\s*group:\s*true\s*$/.test(line));
|
|
180
|
+
if (isGroup) spans.push({ start: i + 1, end });
|
|
181
|
+
}
|
|
182
|
+
return spans;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Locate the stock and engram rows of a composition. Rows are validated by
|
|
187
|
+
* BOTH the `- id:` value and the `name:` value, so a hand-edited backend under
|
|
188
|
+
* the same id is never mistaken for a row we may rewrite.
|
|
189
|
+
* @param {string} text
|
|
190
|
+
* @returns {{ lines: string[]; stock: number[]; engram: number[]; ambiguous: boolean }}
|
|
191
|
+
*/
|
|
192
|
+
function locateRows(text) {
|
|
193
|
+
const lines = text.split("\n");
|
|
194
|
+
const groups = compactionGroupSpans(lines);
|
|
195
|
+
// Two compaction groups in one composition: refuse to guess which one runs.
|
|
196
|
+
if (groups.length > 1) return { lines, stock: [], engram: [], ambiguous: true };
|
|
197
|
+
const span = groups[0] ?? { start: 0, end: lines.length, fallback: true };
|
|
198
|
+
const valid = (rowId, name) => {
|
|
199
|
+
const hits = [];
|
|
200
|
+
for (let i = span.start; i < span.end; i += 1) {
|
|
201
|
+
const head = itemOf(lines[i]);
|
|
202
|
+
if (head === undefined || head.id !== rowId) continue;
|
|
203
|
+
if (span.fallback === true && head.indent === 0) continue;
|
|
204
|
+
if (nameOf(lines, i) !== name) continue;
|
|
205
|
+
hits.push(i);
|
|
206
|
+
}
|
|
207
|
+
return hits;
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
lines,
|
|
211
|
+
stock: valid(STOCK_ROW_ID, STOCK_NAME),
|
|
212
|
+
engram: valid(ENGRAM_ROW_ID, ENGRAM_ROW_NAME),
|
|
213
|
+
ambiguous: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Replace lines `[start, end)` with `replacement` (joined by newlines). */
|
|
218
|
+
function replaceBlock(lines, start, end, replacement) {
|
|
219
|
+
return [
|
|
220
|
+
...lines.slice(0, start),
|
|
221
|
+
...replacement.split("\n"),
|
|
222
|
+
...lines.slice(end),
|
|
223
|
+
].join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
84
226
|
/**
|
|
85
227
|
* Classify a preset composition's compaction wiring.
|
|
86
228
|
* - `wired` — already carries `dsh-engram/compaction`;
|
|
@@ -90,52 +232,23 @@ function codeOf(error) {
|
|
|
90
232
|
* @returns {"wired" | "stock" | "custom"}
|
|
91
233
|
*/
|
|
92
234
|
export function compactionWireStatus(text) {
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
return "custom";
|
|
235
|
+
const { stock, engram } = locateRows(text);
|
|
236
|
+
if (engram.length > 0) return stock.length > 0 ? "custom" : "wired";
|
|
237
|
+
return stock.length > 0 ? "stock" : "custom";
|
|
96
238
|
}
|
|
97
239
|
|
|
98
240
|
/** Post-swap sanity invariants the result must satisfy. */
|
|
99
241
|
function invariantsOk(text) {
|
|
100
242
|
return (
|
|
101
243
|
text.includes(ENGRAM_ROW_NAME) &&
|
|
102
|
-
!text.includes(
|
|
244
|
+
!text.includes(STOCK_NAME) &&
|
|
103
245
|
text.includes("command-compact") &&
|
|
104
246
|
text.includes("tool-result-pruner") &&
|
|
105
|
-
text.includes("
|
|
247
|
+
text.includes("cordis:group") &&
|
|
106
248
|
text.includes("isolate:")
|
|
107
249
|
);
|
|
108
250
|
}
|
|
109
251
|
|
|
110
|
-
/**
|
|
111
|
-
* Locate the engram row block (the two fixed lines plus any deeper-indented
|
|
112
|
-
* `config:` lines) inside a wired composition.
|
|
113
|
-
* @param {string} text
|
|
114
|
-
* @returns {{ start: number; end: number } | null} line indices `[start, end)`
|
|
115
|
-
*/
|
|
116
|
-
function engramRowBlockBounds(text) {
|
|
117
|
-
const lines = text.split("\n");
|
|
118
|
-
const start = lines.findIndex((line) => line === " - id: engram-compaction");
|
|
119
|
-
if (start === -1) return null;
|
|
120
|
-
let end = start + 1;
|
|
121
|
-
while (end < lines.length) {
|
|
122
|
-
const line = lines[end];
|
|
123
|
-
if (line.trim() === "") break;
|
|
124
|
-
if (line.startsWith(" - id:")) break;
|
|
125
|
-
if (line.startsWith(" ")) { end += 1; continue; }
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
return { start, end };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Does the file already carry exactly the expected row text? */
|
|
132
|
-
function rowMatches(text, rowText) {
|
|
133
|
-
const bounds = engramRowBlockBounds(text);
|
|
134
|
-
if (bounds === null) return false;
|
|
135
|
-
const current = text.split("\n").slice(bounds.start, bounds.end).join("\n");
|
|
136
|
-
return current === rowText;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
252
|
/**
|
|
140
253
|
* Produce the wired composition for `rowConfig`. Input may be `stock` (first
|
|
141
254
|
* wiring: the stock row is replaced) or `wired` (refresh: the existing engram
|
|
@@ -147,26 +260,27 @@ function rowMatches(text, rowText) {
|
|
|
147
260
|
* @returns {string | null}
|
|
148
261
|
*/
|
|
149
262
|
export function swapCompactionRow(text, rowConfig = {}) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
if (
|
|
154
|
-
const
|
|
263
|
+
const { lines, stock, engram, ambiguous } = locateRows(text);
|
|
264
|
+
if (ambiguous) return null;
|
|
265
|
+
if (engram.length > 0) {
|
|
266
|
+
if (engram.length > 1 || stock.length > 0) return null;
|
|
267
|
+
const index = engram[0];
|
|
268
|
+
const end = blockEnd(lines, index);
|
|
269
|
+
const rowText = engramCompactionRowText(rowConfig, indentOf(lines[index]));
|
|
270
|
+
if (lines.slice(index, end).join("\n") === rowText) return text; // already this config
|
|
271
|
+
const next = replaceBlock(lines, index, end, rowText);
|
|
155
272
|
return invariantsOk(next) ? next : null;
|
|
156
273
|
}
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return invariantsOk(next) ? next : null;
|
|
168
|
-
}
|
|
169
|
-
return null;
|
|
274
|
+
if (stock.length !== 1) return null;
|
|
275
|
+
const index = stock[0];
|
|
276
|
+
const end = blockEnd(lines, index);
|
|
277
|
+
const next = replaceBlock(
|
|
278
|
+
lines,
|
|
279
|
+
index,
|
|
280
|
+
end,
|
|
281
|
+
engramCompactionRowText(rowConfig, indentOf(lines[index])),
|
|
282
|
+
);
|
|
283
|
+
return invariantsOk(next) ? next : null;
|
|
170
284
|
}
|
|
171
285
|
|
|
172
286
|
/**
|
|
@@ -177,41 +291,121 @@ export function swapCompactionRow(text, rowConfig = {}) {
|
|
|
177
291
|
* @returns {string | null}
|
|
178
292
|
*/
|
|
179
293
|
export function restoreCompactionRow(text) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const next = [
|
|
185
|
-
...lines.slice(0, bounds.start),
|
|
186
|
-
STOCK_COMPACTION_ROW,
|
|
187
|
-
...lines.slice(bounds.end),
|
|
188
|
-
].join("\n");
|
|
294
|
+
const { lines, engram, ambiguous } = locateRows(text);
|
|
295
|
+
if (ambiguous || engram.length !== 1) return null;
|
|
296
|
+
const index = engram[0];
|
|
297
|
+
const end = blockEnd(lines, index);
|
|
298
|
+
const next = replaceBlock(lines, index, end, stockCompactionRowText(indentOf(lines[index])));
|
|
189
299
|
return compactionWireStatus(next) === "stock" ? next : null;
|
|
190
300
|
}
|
|
191
301
|
|
|
302
|
+
/** Environment override for the shipped declarative preset directory. */
|
|
303
|
+
export const PRESETS_DIR_ENV = "DSH_WEB_PRESETS_DIR";
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resolve the shipped declarative preset directory
|
|
307
|
+
* (`@deepseek-ai/dsh-web-app/presets`, DSH ≥ 0.2.0). Both the plugin and the
|
|
308
|
+
* standalone CLI run OUTSIDE that package, so the directory is resolved
|
|
309
|
+
* through the `dsh` installation that launched the process (`process.argv[1]`,
|
|
310
|
+
* plus `opts.anchors` — the CLI passes the `dsh` bin it finds on `PATH`).
|
|
311
|
+
*
|
|
312
|
+
* Resolution order: `opts.presetsDir` (explicit; an empty string disables
|
|
313
|
+
* discovery) → `DSH_WEB_PRESETS_DIR` → package resolution from each anchor.
|
|
314
|
+
* @param {{ presetsDir?: string; anchors?: string[] }} [opts]
|
|
315
|
+
* @returns {string | undefined}
|
|
316
|
+
*/
|
|
317
|
+
export function discoverPresetsDir(opts = {}) {
|
|
318
|
+
if (typeof opts.presetsDir === "string") {
|
|
319
|
+
return opts.presetsDir.length > 0 ? opts.presetsDir : undefined;
|
|
320
|
+
}
|
|
321
|
+
const fromEnv = process.env[PRESETS_DIR_ENV];
|
|
322
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
|
|
323
|
+
for (const anchor of [process.argv[1], ...(opts.anchors ?? [])]) {
|
|
324
|
+
const dir = presetsDirFromAnchor(anchor);
|
|
325
|
+
if (dir !== undefined) return dir;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** `presets/` beside the `@deepseek-ai/dsh-web-app` package, via one anchor. */
|
|
331
|
+
function presetsDirFromAnchor(anchor) {
|
|
332
|
+
if (typeof anchor !== "string" || anchor.length === 0) return undefined;
|
|
333
|
+
let file;
|
|
334
|
+
try {
|
|
335
|
+
file = realpathSync(anchor);
|
|
336
|
+
} catch {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
let require;
|
|
340
|
+
try {
|
|
341
|
+
require = createRequire(file);
|
|
342
|
+
} catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const pkg = require.resolve("@deepseek-ai/dsh-web-app/package.json");
|
|
347
|
+
const dir = join(dirname(pkg), "presets");
|
|
348
|
+
if (existsSync(dir)) return dir;
|
|
349
|
+
} catch {}
|
|
350
|
+
// Fallback: dsh's own nested node_modules (covers layouts where only `dsh`
|
|
351
|
+
// resolves from the anchor, e.g. a global bin without a hoisted web-app).
|
|
352
|
+
try {
|
|
353
|
+
const dshPkg = require.resolve("@deepseek-ai/dsh/package.json");
|
|
354
|
+
const dir = join(dirname(dshPkg), "node_modules", "@deepseek-ai", "dsh-web-app", "presets");
|
|
355
|
+
if (existsSync(dir)) return dir;
|
|
356
|
+
} catch {}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** `<presetsDir>/<id>.patch.yml`, when that shipped file exists. */
|
|
361
|
+
function declarativePresetFile(presetsDir, id) {
|
|
362
|
+
if (presetsDir === undefined || typeof id !== "string" || id.length === 0) return undefined;
|
|
363
|
+
const file = join(presetsDir, `${id}.patch.yml`);
|
|
364
|
+
return existsSync(file) ? file : undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Every `*.patch.yml` in the presets directory, sorted, as `{ id, path }`. */
|
|
368
|
+
async function declarativePresetFiles(presetsDir) {
|
|
369
|
+
let entries = [];
|
|
370
|
+
try {
|
|
371
|
+
entries = await readdir(presetsDir, { withFileTypes: true });
|
|
372
|
+
} catch {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
return entries
|
|
376
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".patch.yml"))
|
|
377
|
+
.map((entry) => ({
|
|
378
|
+
id: entry.name.slice(0, -".patch.yml".length),
|
|
379
|
+
path: join(presetsDir, entry.name),
|
|
380
|
+
}))
|
|
381
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
382
|
+
}
|
|
383
|
+
|
|
192
384
|
/**
|
|
193
385
|
* Collect the presets to consider — the resolved DEFAULT plus every preset on
|
|
194
|
-
* the roster (`service.list()` when present).
|
|
195
|
-
*
|
|
386
|
+
* the roster (`service.list()` when present). Each preset is mapped to a
|
|
387
|
+
* composition file: legacy presets carry `path`; declarative presets
|
|
388
|
+
* (DSH ≥ 0.2.0) are mapped by id to `<presetsDir>/<id>.patch.yml`. A preset
|
|
389
|
+
* without a file (user-defined declarative presets live in the profile patch)
|
|
390
|
+
* is skipped with a log line. Deduped by path; broken presets are skipped.
|
|
391
|
+
* Never throws.
|
|
196
392
|
* @returns {Promise<Array<{ id: string; path: string }>>}
|
|
197
393
|
*/
|
|
198
|
-
async function collectPresetTargets(service, log) {
|
|
394
|
+
async function collectPresetTargets(service, log, opts) {
|
|
395
|
+
const presetsDir = discoverPresetsDir(opts);
|
|
199
396
|
const targets = [];
|
|
200
397
|
const seen = new Set();
|
|
201
|
-
const push = (
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`engram web-provision: preset "${preset.id}" is broken (${preset.broken}) — leaving it alone`,
|
|
206
|
-
);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (seen.has(preset.path)) return;
|
|
210
|
-
seen.add(preset.path);
|
|
211
|
-
targets.push(preset);
|
|
398
|
+
const push = (id, path) => {
|
|
399
|
+
if (typeof path !== "string" || path.length === 0 || seen.has(path)) return;
|
|
400
|
+
seen.add(path);
|
|
401
|
+
targets.push({ id, path });
|
|
212
402
|
};
|
|
403
|
+
|
|
404
|
+
const roster = [];
|
|
405
|
+
let rosterOk = false;
|
|
213
406
|
try {
|
|
214
|
-
|
|
407
|
+
const resolvedPreset = await service.resolve();
|
|
408
|
+
if (resolvedPreset !== undefined && resolvedPreset !== null) roster.push(resolvedPreset);
|
|
215
409
|
} catch (error) {
|
|
216
410
|
log(
|
|
217
411
|
`engram web-provision: default agent preset unavailable (${describe(error)}) — continuing with the roster`,
|
|
@@ -219,13 +413,52 @@ async function collectPresetTargets(service, log) {
|
|
|
219
413
|
}
|
|
220
414
|
if (typeof service.list === "function") {
|
|
221
415
|
try {
|
|
222
|
-
|
|
416
|
+
const listed = await service.list();
|
|
417
|
+
if (Array.isArray(listed)) {
|
|
418
|
+
roster.push(...listed);
|
|
419
|
+
rosterOk = true;
|
|
420
|
+
}
|
|
223
421
|
} catch (error) {
|
|
224
422
|
log(
|
|
225
423
|
`engram web-provision: preset roster unavailable (${describe(error)}) — using the default only`,
|
|
226
424
|
);
|
|
227
425
|
}
|
|
228
426
|
}
|
|
427
|
+
|
|
428
|
+
for (const preset of roster) {
|
|
429
|
+
if (preset === undefined || preset === null) continue;
|
|
430
|
+
if (preset.broken !== undefined && preset.broken !== null) {
|
|
431
|
+
log(
|
|
432
|
+
`engram web-provision: preset "${preset.id}" is broken (${preset.broken}) — leaving it alone`,
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// DSH ≤ 0.1.x: the preset knows its composition file.
|
|
437
|
+
if (typeof preset.path === "string") {
|
|
438
|
+
push(preset.id, preset.path);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
// DSH ≥ 0.2.0: a declarative preset — map its id to the shipped patch file.
|
|
442
|
+
const file = declarativePresetFile(presetsDir, preset.id);
|
|
443
|
+
if (file !== undefined) {
|
|
444
|
+
push(preset.id, file);
|
|
445
|
+
} else if (presetsDir === undefined) {
|
|
446
|
+
log(
|
|
447
|
+
`engram web-provision: preset "${preset.id}" has no composition path and no shipped preset `
|
|
448
|
+
+ `directory was found (set ${PRESETS_DIR_ENV} or run from a normal dsh install) — skipping`,
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
log(
|
|
452
|
+
`engram web-provision: preset "${preset.id}" has no shipped file in ${presetsDir} — skipping `
|
|
453
|
+
+ "(user-defined declarative presets live in the profile patch and are not wireable here)",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Roster entirely unavailable: fall back to the shipped declarative files.
|
|
459
|
+
if (!rosterOk && targets.length === 0 && presetsDir !== undefined) {
|
|
460
|
+
for (const entry of await declarativePresetFiles(presetsDir)) push(entry.id, entry.path);
|
|
461
|
+
}
|
|
229
462
|
return targets;
|
|
230
463
|
}
|
|
231
464
|
|
|
@@ -257,7 +490,7 @@ export async function provisionWebCompaction(service, opts = {}) {
|
|
|
257
490
|
opts.writeBackup ?? ((path, text) => writeFile(path, text, { flag: "wx" }));
|
|
258
491
|
const backupSuffix = opts.backupSuffix ?? BACKUP_SUFFIX;
|
|
259
492
|
|
|
260
|
-
const presets = await collectPresetTargets(service, log);
|
|
493
|
+
const presets = await collectPresetTargets(service, log, opts);
|
|
261
494
|
if (presets.length === 0) {
|
|
262
495
|
log("engram web-provision: no usable agent preset found — Context GC auto-wiring skipped");
|
|
263
496
|
return { action: "no-default", presets: [] };
|
|
@@ -371,7 +604,7 @@ export async function revertWebCompaction(service, opts = {}) {
|
|
|
371
604
|
const readText = opts.readText ?? ((preset) => readFile(preset.path, "utf8"));
|
|
372
605
|
const writeText = opts.writeText ?? ((preset, text) => writeFile(preset.path, text, "utf8"));
|
|
373
606
|
|
|
374
|
-
const presets = await collectPresetTargets(service, log);
|
|
607
|
+
const presets = await collectPresetTargets(service, log, opts);
|
|
375
608
|
if (presets.length === 0) return { action: "no-default", presets: [] };
|
|
376
609
|
|
|
377
610
|
const results = [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engram",
|
|
3
3
|
"description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|