dsh-engram 0.7.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 +70 -16
- package/README.zh.md +50 -15
- package/cordis.patch.yml +18 -5
- package/docs/CONTEXT-GC-GUIDE.zh.md +35 -6
- package/docs/RELEASE-NOTES-v0.7.1.md +88 -0
- package/lib/boundary-prune.js +27 -0
- package/lib/client.js +3 -2
- package/lib/compaction.js +11 -0
- package/lib/context-gc.js +47 -0
- package/lib/index.js +54 -8
- package/lib/web-provision.js +324 -91
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +196 -53
package/lib/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { registerTools } from "./tools.js";
|
|
|
24
24
|
import { makeCaptureHandler } from "./capture.js";
|
|
25
25
|
import { installGoalCapture } from "./goal-capture.js";
|
|
26
26
|
import { installPreferenceCapture } from "./preference.js";
|
|
27
|
-
import { installBoundaryPrune } from "./boundary-prune.js";
|
|
27
|
+
import { boundaryPruneAvailability, installBoundaryPrune } from "./boundary-prune.js";
|
|
28
28
|
import { resultPackDir, archivePrunedResults, resultRecallTool, sweepExpired } from "./result-pack.js";
|
|
29
29
|
import { recordBoundaryPruneFire, recordRecallInjection, readPackTelemetry, engramHome } from "./pack-telemetry.js";
|
|
30
30
|
import { createLedger, estimatePrefixChars } from "./ledger.js";
|
|
@@ -359,6 +359,27 @@ export function apply(ctx, config) {
|
|
|
359
359
|
log,
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
// Expose the ledger to the per-session web plane. Agent presets mount
|
|
363
|
+
// `dsh-engram/compaction` in their own isolate realm, so a closure from this
|
|
364
|
+
// apply() is unreachable there; a plain (non-isolated) service value is the
|
|
365
|
+
// bridge: the row credits compaction savings through it and `ledger` stays
|
|
366
|
+
// the single writer of the durable rollup.
|
|
367
|
+
if (ledger !== null) {
|
|
368
|
+
try {
|
|
369
|
+
// `ctx.provide` is fiber-scoped (cordis removes the service when this
|
|
370
|
+
// plugin unloads), so the returned disposer needs no bookkeeping here.
|
|
371
|
+
ctx.provide?.("engramLedger", {
|
|
372
|
+
noteCompaction: (sessionId, workspace, opts) => ledger.noteCompaction(sessionId, workspace, opts),
|
|
373
|
+
notePrune: (sessionId, workspace, opts) => ledger.notePrune(sessionId, workspace, opts),
|
|
374
|
+
noteRewrite: (sessionId, workspace, opts) => ledger.noteRewrite(sessionId, workspace, opts),
|
|
375
|
+
summary: () => ledger.summary(),
|
|
376
|
+
flush: () => ledger.flush(),
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
log?.warn?.(`engram ledger service unavailable: ${String(error?.message ?? error)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
362
383
|
// Ledger-driven off-switch for BoundaryPrune (PROPOSAL-evolution v0.6-B):
|
|
363
384
|
// per-workspace, 30s-cached, reads the same summary /stats exposes. Evidence
|
|
364
385
|
// can only turn pruning OFF. No ledger → allow (repayment gate still runs).
|
|
@@ -422,7 +443,9 @@ export function apply(ctx, config) {
|
|
|
422
443
|
/** Live Context GC status (API overview + durable status file). */
|
|
423
444
|
compactionStatus: {
|
|
424
445
|
host: "pending",
|
|
425
|
-
|
|
446
|
+
// `disabled` (not `pending`) when the web opt-in is off: the status file
|
|
447
|
+
// must not look like a wiring run that never finished.
|
|
448
|
+
web: resolved.autoWebCompaction === false ? { action: "disabled", presets: [] } : { action: "pending", presets: [] },
|
|
426
449
|
config: { autoWebCompaction: resolved.autoWebCompaction, gcReplacesCompaction: resolved.gcReplacesCompaction, gcNarrative: resolved.gcNarrative },
|
|
427
450
|
},
|
|
428
451
|
/** Recompute the exact [ENGRAM] block for a workspace (GUI overview cost). */
|
|
@@ -666,6 +689,16 @@ export function apply(ctx, config) {
|
|
|
666
689
|
return null;
|
|
667
690
|
}
|
|
668
691
|
},
|
|
692
|
+
// Context Ledger credit: the Context GC engine measures the span it
|
|
693
|
+
// replaced with a checkpoint (`span − summary` chars) and reports it
|
|
694
|
+
// here; the ledger stops the "savings are always 0" asymmetry.
|
|
695
|
+
onCompacted: ({ session, workspace, charsSaved }) => {
|
|
696
|
+
try {
|
|
697
|
+
ledger?.noteCompaction(session?.id, workspace ?? wk(session?.header?.cwd), { charsSaved });
|
|
698
|
+
} catch (error) {
|
|
699
|
+
log?.warn?.(`engram ledger compaction credit failed: ${String(error)}`);
|
|
700
|
+
}
|
|
701
|
+
},
|
|
669
702
|
});
|
|
670
703
|
disposers.push(() => {
|
|
671
704
|
// The engine's service registration is fiber-scoped and auto-removed on
|
|
@@ -680,6 +713,10 @@ export function apply(ctx, config) {
|
|
|
680
713
|
let hostSettled = false;
|
|
681
714
|
let webSettled = resolved.autoWebCompaction === false;
|
|
682
715
|
let statusTimer = void 0;
|
|
716
|
+
// Authoritative L1 availability, captured where BoundaryPrune is actually
|
|
717
|
+
// installed (the pruner may land after the first status flush on a slow
|
|
718
|
+
// boot; the live check is only a fallback).
|
|
719
|
+
let boundaryPruneState = void 0;
|
|
683
720
|
const flushStatus = () => {
|
|
684
721
|
if (!hostSettled || !webSettled) return;
|
|
685
722
|
if (statusTimer !== void 0) { clearTimeout(statusTimer); statusTimer = void 0; }
|
|
@@ -687,6 +724,9 @@ export function apply(ctx, config) {
|
|
|
687
724
|
planes: resolved.autoWebCompaction === false ? ["host"] : ["host", "web"],
|
|
688
725
|
host: service.compactionStatus.host,
|
|
689
726
|
web: service.compactionStatus.web,
|
|
727
|
+
// L1 availability is the missing credit path behind a "节省 always 0"
|
|
728
|
+
// ledger: record whether BoundaryPrune can actually fire here.
|
|
729
|
+
boundaryPrune: boundaryPruneState ?? boundaryPruneAvailability(ctx, resolved),
|
|
690
730
|
config: service.compactionStatus.config,
|
|
691
731
|
configSource: {
|
|
692
732
|
autoWebCompaction: resolved.autoWebCompaction,
|
|
@@ -713,12 +753,15 @@ export function apply(ctx, config) {
|
|
|
713
753
|
// inside the agent preset's own realm, which a profile patch cannot reach.
|
|
714
754
|
// When the `agentPresets` service is present, swap the stock
|
|
715
755
|
// `compaction-basic` row for `dsh-engram/compaction` in every stock-layout
|
|
716
|
-
// preset
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
756
|
+
// preset. The row is located structurally (indentation-agnostic) inside the
|
|
757
|
+
// `compaction` group, so both preset generations are covered: legacy
|
|
758
|
+
// compositions with `preset.path` (DSH ≤ 0.1.x) and the declarative
|
|
759
|
+
// `@deepseek-ai/dsh-web-app/presets/<id>.patch.yml` files (DSH ≥ 0.2.0,
|
|
760
|
+
// where the row sits at 14 spaces and the id maps to the shipped file).
|
|
761
|
+
// Surgical, idempotent, backed up, validated; a preset with a custom
|
|
762
|
+
// compaction layout (or none) is never touched. The row's engine config is
|
|
763
|
+
// derived from the settings knobs (`gcReplacesCompaction` / `gcNarrative`),
|
|
764
|
+
// so changing them re-provisions the web rows on the next boot. Flip
|
|
722
765
|
// `autoWebCompaction:false` to stop auto-wiring on later boots (it does
|
|
723
766
|
// not un-wire already-wired presets — use `npm run web-compaction:revert`
|
|
724
767
|
// for that). Best-effort: a failure only warns.
|
|
@@ -1010,6 +1053,9 @@ export function apply(ctx, config) {
|
|
|
1010
1053
|
},
|
|
1011
1054
|
}, { allowFire: boundaryGate });
|
|
1012
1055
|
disposers.push(disposeBoundaryPrune);
|
|
1056
|
+
// BoundaryPrune is installed here, so this is the moment L1 provably can
|
|
1057
|
+
// (or cannot) fire — record it for the durable status snapshot.
|
|
1058
|
+
boundaryPruneState = boundaryPruneAvailability(ctx, resolved);
|
|
1013
1059
|
|
|
1014
1060
|
// ResultPack L2 tool (default ON with L1 since v0.6): `result_recall(callId,
|
|
1015
1061
|
// offset?)` pages back an archived middle. Independent of the memory
|
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.1",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|