dsh-engram 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -17
- package/README.zh.md +103 -15
- package/docs/PROPOSAL-resultpack.zh.md +7 -3
- package/docs/RELEASE-NOTES-v0.5.0.md +58 -0
- package/lib/api.js +19 -6
- package/lib/boundary-prune.js +113 -13
- package/lib/client.js +12 -0
- package/lib/index-block.js +27 -5
- package/lib/index.js +181 -5
- package/lib/pack-telemetry.js +115 -0
- package/lib/preference.js +276 -0
- package/lib/result-pack.js +276 -0
- package/lib/settings.js +115 -31
- package/lib/store.js +1 -0
- package/lib/tools.js +2 -1
- package/package.json +8 -7
- package/scripts/prepublish-check.mjs +7 -4
package/lib/settings.js
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-engram: user settings binding (host half).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
4
|
+
* Two DSH settings generations are in the wild:
|
|
5
|
+
* - ≤0.1.5 `settings.installSection(ctx, ns, schema, base, hooks)` mounts a
|
|
6
|
+
* free-form namespace (`dsh-engram`) with its own storage + change hooks.
|
|
7
|
+
* - ≥0.1.7 (and 0.2.x) forms are keyed by PROFILE ENTRY ID; editable fields
|
|
8
|
+
* are `.volatile()` fields of the plugin's own `Config` schema, values live
|
|
9
|
+
* in the profile row and hot-commit into the running volatile references
|
|
10
|
+
* (`loader/volatile-update`), so the GUI card needs no separate mount call.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* This module therefore:
|
|
13
|
+
* - exports `makeEngineConfigSchema(defaults)` — the plugin `Config`: every
|
|
14
|
+
* DEFAULTS key so pre-existing profile-patch config survives schema
|
|
15
|
+
* validation, with the curated SETTINGS_KEYS marked `.volatile()` so the
|
|
16
|
+
* settings service exposes exactly the card's knobs as a form;
|
|
17
|
+
* - `installEngramSettings` feature-detects the generation and exposes the
|
|
18
|
+
* single `service.settings` seam `{ ns, describe, mutate, update }` the
|
|
19
|
+
* `/api/dsh-engram/settings` routes use. The seam's `ns` is the ACTIVE
|
|
20
|
+
* namespace — entry id `engram` on new DSH, legacy `dsh-engram` on old —
|
|
21
|
+
* so the API layer never needs to know which generation it talks to.
|
|
22
|
+
*
|
|
23
|
+
* The client level keeps a stable wire label `dsh-engram`: api.js re-labels
|
|
24
|
+
* the describe view onto that label and lets the seam's active `ns` drive
|
|
25
|
+
* mutations, so the shipped client bundle works unchanged on both generations.
|
|
12
26
|
*/
|
|
13
27
|
|
|
14
28
|
import z from "@deepseek-ai/schemastery";
|
|
15
29
|
|
|
16
|
-
/**
|
|
30
|
+
/** Active settings namespace on DSH ≥0.1.7 / 0.2.x — the profile ENTRY ID of
|
|
31
|
+
* the plugin row (`id: engram` in cordis.patch.yml). Must match plugin name. */
|
|
32
|
+
export const SETTINGS_NS = "engram";
|
|
33
|
+
/** Legacy (≤0.1.5) free-form namespace, when installSection is present. */
|
|
34
|
+
export const LEGACY_SETTINGS_NS = "dsh-engram";
|
|
35
|
+
/** Stable wire label the client card filters on (unchanged across generations;
|
|
36
|
+
* api.js re-labels the describe view onto it). */
|
|
37
|
+
export const WIRE_SETTINGS_NS = "dsh-engram";
|
|
38
|
+
/** Back-compat alias some call sites used for the old namespace constant. */
|
|
17
39
|
export const SETTINGS_NS_NAME = "dsh-engram";
|
|
18
|
-
// dsh-settings 0.1.5 exposes the namespace as a validated string at the
|
|
19
|
-
// provider boundary; the legacy branding helper was removed.
|
|
20
|
-
export const SETTINGS_NS = SETTINGS_NS_NAME;
|
|
21
40
|
|
|
22
41
|
/** Fields the GUI exposes (a curated subset of the plugin DEFAULTS). */
|
|
23
42
|
export const SETTINGS_KEYS = [
|
|
24
43
|
"autoCapture",
|
|
44
|
+
"autoCapturePreferences",
|
|
45
|
+
"boundaryPrune",
|
|
46
|
+
"boundaryPruneThresholdChars",
|
|
47
|
+
"boundaryPruneIdleCadence",
|
|
48
|
+
"resultPack",
|
|
25
49
|
"sessionSearch",
|
|
26
50
|
"recallScope",
|
|
27
51
|
"autoRecallOnStart",
|
|
@@ -58,6 +82,11 @@ export function settingsBaseFrom(defaults) {
|
|
|
58
82
|
export function makeSettingsSchema(defaults) {
|
|
59
83
|
return z.object({
|
|
60
84
|
autoCapture: z.boolean().default(defaults.autoCapture),
|
|
85
|
+
autoCapturePreferences: z.boolean().default(defaults.autoCapturePreferences).description("用户纠正/偏好学习(零 LLM):检测用户消息里的显式 prefer/avoid/switch-to 句式,自动沉淀为 preference 记忆并置顶 [ENGRAM] 索引(correx/PAHF 式偏好提取;关掉=只靠手动 engram_store)"),
|
|
86
|
+
boundaryPrune: z.boolean().default(defaults.boundaryPrune).description("ResultPack L1(BoundaryPrune 边界剪枝):在 todo 完成推进 / goal 终态这些子任务边界,主动调用 DSH 的确定性 toolResultPruner 剪掉超阈值旧工具结果(SoL-Pi Online Context Compact;默认关)"),
|
|
87
|
+
boundaryPruneThresholdChars: z.natural().default(0).description("边界剪枝阈值覆盖(字符):宿主 pruner 固定 8192;设更低(如 2048)可更早开剪——L0 仿真显示 T2048 净收益最高(+61.6Mc),前提是 L2 归档已开启可找回被剪中段;0=用宿主 8192"),
|
|
88
|
+
boundaryPruneIdleCadence: z.natural().default(0).description("空闲步数边界(请求计数):最多每 N 个请求触发一次剪枝,覆盖无 todo/goal 的会话(约 62% 会话永不触发事件边界);仍受偿还门约束;0=仅事件边界"),
|
|
89
|
+
resultPack: z.boolean().default(defaults.resultPack).description("ResultPack L2(结果归档召回):BoundaryPrune 剪掉的中段脱敏归档到本地,剪枝节点尾部追加 handle,模型可用 result_recall(callId) 按页召回(完整 ObservationPack;需与 boundaryPrune 一起开,L2 本身默认关)"),
|
|
61
90
|
sessionSearch: z.boolean().default(defaults.sessionSearch),
|
|
62
91
|
recallScope: z.union(["workspace", "global"]).default(defaults.recallScope ?? "workspace").description("召回范围:workspace=严格按当前工作区隔离(默认、token 纪律);global=本工作区命中弱/空时,从其它工作区池按 recency/origin 加权召回并标 [W:<workspace>] 来源。开启会打破跨工作区隔离,请按需选用"),
|
|
63
92
|
autoRecallOnStart: z.boolean().default(defaults.autoRecallOnStart).description("新会话开始时,宿主按首条消息对工作区做确定性召回,把命中记忆直接注入 [RECALL] 块(无需模型自己想起调 engram_recall;关掉=回到纯拉取)"),
|
|
@@ -84,33 +113,80 @@ export function makeSettingsSchema(defaults) {
|
|
|
84
113
|
});
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
/** schemastery prototype for ONE DEFAULTS entry, used for the plugin Config. */
|
|
117
|
+
function configFieldType(value) {
|
|
118
|
+
if (typeof value === "boolean") return z.boolean();
|
|
119
|
+
if (typeof value === "number") return z.number();
|
|
120
|
+
if (typeof value === "string") return z.string();
|
|
121
|
+
if (Array.isArray(value)) return z.array(z.string());
|
|
122
|
+
return z.unknown();
|
|
123
|
+
}
|
|
124
|
+
|
|
87
125
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
126
|
+
* The plugin's full Config schema (schemastery), i.e. the schema DSH ≥0.1.7
|
|
127
|
+
* validates the `engram` profile row against and projects the settings form
|
|
128
|
+
* from.
|
|
129
|
+
*
|
|
130
|
+
* - EVERY DEFAULTS key is declared (non-volatile, with its default) so a
|
|
131
|
+
* profile patch that sets an internal knob (e.g. `gcIntervalHours`,
|
|
132
|
+
* `enabled`) survives schema validation instead of being stripped.
|
|
133
|
+
* - The curated SETTINGS_KEYS are additionally `.volatile()` — the settings
|
|
134
|
+
* service then exposes exactly the card's knobs as an editable form keyed by
|
|
135
|
+
* the entry id, and writes hot-commit into the running volatile references.
|
|
90
136
|
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
* Exported as `Config` by lib/index.js (cordis reads `plugin.Config`).
|
|
138
|
+
*/
|
|
139
|
+
export function makeEngineConfigSchema(defaults) {
|
|
140
|
+
const fields = {};
|
|
141
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
142
|
+
let field = configFieldType(value).default(value);
|
|
143
|
+
if (SETTINGS_KEYS.includes(key)) field = field.volatile();
|
|
144
|
+
fields[key] = field;
|
|
145
|
+
}
|
|
146
|
+
return z.object(fields);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Wire the settings section into `ctx` and expose the normalized
|
|
151
|
+
* `service.settings` seam ({ ns, describe, mutate, update }) for the web API.
|
|
152
|
+
* Called from `apply()`.
|
|
153
|
+
*
|
|
154
|
+
* Generation-agnostic adapter (DSH ≥0.1.7 / 0.2.x on top):
|
|
155
|
+
* - new DSH (≥0.1.7): the form is entry-keyed (`engram`) and its fields are
|
|
156
|
+
* the plugin's volatile Config knobs — there is nothing to "mount":
|
|
157
|
+
* `apply()` already received the validated volatile references and
|
|
158
|
+
* re-syncs `live` on `loader/volatile-update`. The seam just forwards.
|
|
159
|
+
* - legacy DSH (≤0.1.5): `installSection` mounts the free-form `dsh-engram`
|
|
160
|
+
* namespace; the old onChange loop merges stored values onto `live`.
|
|
161
|
+
*
|
|
162
|
+
* When both APIs are absent (headless/other), the seam stays undefined and the
|
|
163
|
+
* /api/dsh-engram/settings routes answer 503 — same as before.
|
|
96
164
|
*/
|
|
97
165
|
export function installEngramSettings(ctx, defaults, live, service) {
|
|
98
|
-
const base = settingsBaseFrom(defaults);
|
|
99
|
-
const schema = makeSettingsSchema(defaults);
|
|
100
|
-
let source = () => base;
|
|
101
166
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
102
|
-
// dsh-settings 0.1.5 moved the optional-consumer wiring onto the provider
|
|
103
|
-
// instance. Keeping the registration on the scoped context preserves the
|
|
104
|
-
// provider attach/detach lifecycle while avoiding a removed helper export.
|
|
105
167
|
const provider = settingsCtx.settings;
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
if (!provider || typeof provider.describe !== "function") return;
|
|
169
|
+
|
|
170
|
+
// New generation: `configure()` exists and `installSection` is gone.
|
|
171
|
+
if (typeof provider.configure === "function") {
|
|
172
|
+
if (service) {
|
|
173
|
+
service.settings = {
|
|
174
|
+
ns: SETTINGS_NS,
|
|
175
|
+
describe: () => provider.describe({ redactSecrets: true }),
|
|
176
|
+
mutate: (ns, ops, expectedRevision) => provider.mutate(ns, ops, expectedRevision),
|
|
177
|
+
update: (ns, patch, expectedRevision) => provider.update(ns, patch, expectedRevision),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
112
181
|
}
|
|
113
|
-
|
|
182
|
+
|
|
183
|
+
// Legacy generation: free-form namespace via installSection.
|
|
184
|
+
if (typeof provider.installSection !== "function") return;
|
|
185
|
+
const base = settingsBaseFrom(defaults);
|
|
186
|
+
const schema = makeSettingsSchema(defaults);
|
|
187
|
+
let source = () => base;
|
|
188
|
+
const ns = LEGACY_SETTINGS_NS;
|
|
189
|
+
provider.installSection(ctx, ns, schema, base, {
|
|
114
190
|
setSource(current) {
|
|
115
191
|
source = current;
|
|
116
192
|
},
|
|
@@ -131,5 +207,13 @@ export function installEngramSettings(ctx, defaults, live, service) {
|
|
|
131
207
|
void value;
|
|
132
208
|
},
|
|
133
209
|
});
|
|
210
|
+
if (service) {
|
|
211
|
+
service.settings = {
|
|
212
|
+
ns,
|
|
213
|
+
describe: () => provider.describe({ redactSecrets: true }),
|
|
214
|
+
mutate: (ns, ops, expectedRevision) => provider.mutate(ns, ops, expectedRevision),
|
|
215
|
+
update: (ns, patch, expectedRevision) => provider.update(ns, patch, expectedRevision),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
134
218
|
});
|
|
135
219
|
}
|
package/lib/store.js
CHANGED
package/lib/tools.js
CHANGED
|
@@ -23,6 +23,7 @@ import { truncate, shortId, slugId, fmtDate, escapeLt, workspaceKey as wk, uuid
|
|
|
23
23
|
import { hasCJK, searchSessionsCJK } from "./session-cjk.js";
|
|
24
24
|
|
|
25
25
|
const KINDS = [
|
|
26
|
+
"preference",
|
|
26
27
|
"decision",
|
|
27
28
|
"error",
|
|
28
29
|
"procedure",
|
|
@@ -99,7 +100,7 @@ function memoryLine(m, verbosity = "short", rel, homeWs) {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/** Stable kind display order for grouped recall output (matches MEMORY_KINDS). */
|
|
102
|
-
const MEMORY_KIND_ORDER = ["decision", "error", "procedure", "fact", "insight", "handoff", "task"];
|
|
103
|
+
const MEMORY_KIND_ORDER = ["preference", "decision", "error", "procedure", "fact", "insight", "handoff", "task"];
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
106
|
* Render recall hits grouped by kind (MemoraX-style memory_type buckets):
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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.
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"packageManager": "pnpm@11.22.0",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
],
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=22.19.0",
|
|
37
|
-
"dsh": ">=0.1.2-alpha.2 <0.
|
|
38
|
+
"dsh": ">=0.1.2-alpha.2 <0.3.0-0"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"setup": "node scripts/setup-links.mjs",
|
|
@@ -74,12 +75,12 @@
|
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
77
|
-
"@deepseek-ai/dsh-storage-domain": ">=0.1.2-alpha.2 <0.
|
|
78
|
-
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2 <0.
|
|
78
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.1.2-alpha.2 <0.3.0-0",
|
|
79
|
+
"@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2 <0.3.0-0",
|
|
79
80
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
80
|
-
"@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.2 <0.
|
|
81
|
-
"@deepseek-ai/dsh-compaction-basic": ">=0.1.2-alpha.2 <0.
|
|
82
|
-
"@deepseek-ai/dsh-llm": ">=0.1.2-alpha.2 <0.
|
|
81
|
+
"@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.2 <0.3.0-0",
|
|
82
|
+
"@deepseek-ai/dsh-compaction-basic": ">=0.1.2-alpha.2 <0.3.0-0",
|
|
83
|
+
"@deepseek-ai/dsh-llm": ">=0.1.2-alpha.2 <0.3.0-0"
|
|
83
84
|
},
|
|
84
85
|
"peerDependenciesMeta": {
|
|
85
86
|
"@deepseek-ai/cordis": {
|
|
@@ -24,8 +24,8 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
24
24
|
const tmp = mkdtempSync(join(tmpdir(), "dsh-engram-pack-"));
|
|
25
25
|
const PEERS = [
|
|
26
26
|
"@deepseek-ai/cordis@^4.0.2",
|
|
27
|
-
"@deepseek-ai/dsh-settings@>=0.1.2-alpha.2 <0.
|
|
28
|
-
"@deepseek-ai/dsh-storage-domain@>=0.1.2-alpha.2 <0.
|
|
27
|
+
"@deepseek-ai/dsh-settings@>=0.1.2-alpha.2 <0.3.0-0",
|
|
28
|
+
"@deepseek-ai/dsh-storage-domain@>=0.1.2-alpha.2 <0.3.0-0",
|
|
29
29
|
"@deepseek-ai/schemastery@^3.18.2",
|
|
30
30
|
];
|
|
31
31
|
|
|
@@ -85,8 +85,11 @@ try {
|
|
|
85
85
|
const manifest = JSON.parse(readFileSync(join(installed, "package.json"), "utf8"));
|
|
86
86
|
const host = await import(pathToFileURL(join(installed, manifest.main)).href);
|
|
87
87
|
// resolveConfig is a deliberate public export (settings test builds the
|
|
88
|
-
// real DEFAULTS from it);
|
|
89
|
-
|
|
88
|
+
// real DEFAULTS from it); Config is read by cordis as runtime.Config (the
|
|
89
|
+
// entry-id / volatile settings model); collectVolatileRefs/syncVolatileRefs
|
|
90
|
+
// back that model's live re-sync and are unit-tested directly. Widen the
|
|
91
|
+
// host surface consciously, not silently.
|
|
92
|
+
const expected = ["Config", "apply", "collectVolatileRefs", "inject", "name", "resolveConfig", "syncVolatileRefs"].sort().join(",");
|
|
90
93
|
const actual = Object.keys(host).sort().join(",");
|
|
91
94
|
if (actual !== expected) {
|
|
92
95
|
throw new Error(`host exports mismatch: got ${actual}, want ${expected}`);
|