dsh-engram 0.3.4 → 0.3.6
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 +283 -27
- package/README.zh.md +184 -16
- package/cordis.patch.yml +6 -0
- package/docs/CONTEXT-GC-GUIDE.zh.md +192 -0
- package/docs/PROPOSAL-agentmemory.zh.md +108 -0
- package/docs/PROPOSAL-beads.zh.md +81 -0
- package/docs/PROPOSAL-contextgc.zh.md +188 -0
- package/docs/PROPOSAL-esrtrigger.zh.md +199 -0
- package/docs/PROPOSAL-hindsight.zh.md +156 -0
- package/lib/api.js +76 -2
- package/lib/capture.js +132 -4
- package/lib/client.js +45 -2
- package/lib/compaction.js +2 -18
- package/lib/context-gc.js +234 -2
- package/lib/goal-capture.js +89 -0
- package/lib/index-block.js +96 -1
- package/lib/index.js +233 -5
- package/lib/redact.js +48 -0
- package/lib/session-cjk.js +159 -0
- package/lib/settings.js +13 -0
- package/lib/status.js +57 -0
- package/lib/store.js +411 -6
- package/lib/todo-sink.js +146 -0
- package/lib/tools.js +98 -40
- package/lib/trigger.js +10 -8
- package/lib/usage.js +147 -5
- package/lib/web-provision.js +430 -0
- package/package.json +13 -2
- package/scripts/dsh-engram.mjs +268 -0
- package/scripts/engram-web-compaction.mjs +7 -0
- package/scripts/prepublish-check.mjs +106 -0
- package/scripts/setup-links.mjs +241 -0
package/lib/api.js
CHANGED
|
@@ -18,6 +18,9 @@ export const API_PREFIX = "/api/dsh-engram";
|
|
|
18
18
|
/** Cap on JSON request bodies (archives/deletes are tiny). */
|
|
19
19
|
const MAX_JSON_BODY_BYTES = 32 * 1024;
|
|
20
20
|
|
|
21
|
+
/** Import dumps carry whole workspaces of memories/tasks — allow up to 32 MiB. */
|
|
22
|
+
const MAX_IMPORT_BODY_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
|
|
21
24
|
/** Loopback literal check plus browser same-origin markers. */
|
|
22
25
|
function isLoopbackRequest(request, trustedHosts = []) {
|
|
23
26
|
const address = request.socket?.remoteAddress;
|
|
@@ -67,13 +70,13 @@ function readQuery(req) {
|
|
|
67
70
|
return url.searchParams;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function readJsonBody(req) {
|
|
73
|
+
function readJsonBody(req, maxBytes = MAX_JSON_BODY_BYTES) {
|
|
71
74
|
return new Promise((resolve) => {
|
|
72
75
|
let size = 0;
|
|
73
76
|
const chunks = [];
|
|
74
77
|
req.on("data", (chunk) => {
|
|
75
78
|
size += chunk.length;
|
|
76
|
-
if (size >
|
|
79
|
+
if (size > maxBytes) {
|
|
77
80
|
resolve({ error: "body too large" });
|
|
78
81
|
req.destroy();
|
|
79
82
|
return;
|
|
@@ -157,6 +160,7 @@ export function makeEngramRoutes(service) {
|
|
|
157
160
|
indexes,
|
|
158
161
|
captures: service.captureStats ?? { total: 0, git: 0, file: 0, error: 0 },
|
|
159
162
|
gc: service.gcStats ?? { lastRun: 0, archivedMemories: 0, archivedTasks: 0, removedLinks: 0 },
|
|
163
|
+
compaction: service.compactionStatus ?? { host: "unknown", web: { action: "unknown", presets: [] } },
|
|
160
164
|
config: service.config ?? {},
|
|
161
165
|
anonymous: {
|
|
162
166
|
version: 2,
|
|
@@ -180,6 +184,23 @@ export function makeEngramRoutes(service) {
|
|
|
180
184
|
writeJson(res, 200, { items });
|
|
181
185
|
};
|
|
182
186
|
|
|
187
|
+
// Goal-domain sediment: memories auto-captured from `goal/change` events
|
|
188
|
+
// (tag "goal"). A compact read surface for the GUI, backed by the same
|
|
189
|
+
// searchMemory view as /memories.
|
|
190
|
+
const goals = async (req, res) => {
|
|
191
|
+
const domain = await withDomain(res);
|
|
192
|
+
if (domain === void 0) return;
|
|
193
|
+
const params = readQuery(req);
|
|
194
|
+
const limit = Number(params.get("limit") ?? 50);
|
|
195
|
+
const items = domain.searchMemories({
|
|
196
|
+
workspace: params.get("workspace") || void 0,
|
|
197
|
+
tag: "goal",
|
|
198
|
+
status: "active",
|
|
199
|
+
limit,
|
|
200
|
+
}).map((m) => ({ id: m.id, kind: m.kind, text: m.text, createdAt: m.createdAt, workspace: m.workspace }));
|
|
201
|
+
writeJson(res, 200, { items });
|
|
202
|
+
};
|
|
203
|
+
|
|
183
204
|
const tasks = async (req, res) => {
|
|
184
205
|
const domain = await withDomain(res);
|
|
185
206
|
if (domain === void 0) return;
|
|
@@ -282,6 +303,56 @@ export function makeEngramRoutes(service) {
|
|
|
282
303
|
}
|
|
283
304
|
};
|
|
284
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Data contract — dump every row (memories/tasks/links/entities, archived
|
|
308
|
+
* included) of one workspace (`?workspace=`) or of every workspace, in the
|
|
309
|
+
* machine-readable `dsh-engram/export` v1 format that `POST /import`
|
|
310
|
+
* round-trips. Export is the stable, format-versioned exit door: the memory
|
|
311
|
+
* corpus never depends on engram's internal tables, and a thin adapter
|
|
312
|
+
* (another harness, MCP, a plain backup script) only needs this shape.
|
|
313
|
+
*/
|
|
314
|
+
const exportData = async (req, res) => {
|
|
315
|
+
const domain = await withDomain(res);
|
|
316
|
+
if (domain === void 0) return;
|
|
317
|
+
const workspace = readQuery(req).get("workspace") || void 0;
|
|
318
|
+
try {
|
|
319
|
+
writeJson(res, 200, domain.exportAll(workspace));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
writeError(res, 500, String(error?.message ?? error));
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** Data contract — restore a `dsh-engram/export` dump (see store.importAll:
|
|
326
|
+
* merge = idempotent add-missing, replace = wipe workspace then restore;
|
|
327
|
+
* `dryRun` plans without writing; `confirm: "restore"` is required for the
|
|
328
|
+
* destructive replace mode). Memory text passes the same secret redactor as
|
|
329
|
+
* every other write path. */
|
|
330
|
+
const importData = async (req, res) => {
|
|
331
|
+
const domain = await withDomain(res);
|
|
332
|
+
if (domain === void 0) return;
|
|
333
|
+
const body = await readJsonBody(req, MAX_IMPORT_BODY_BYTES);
|
|
334
|
+
if (body.error) return writeError(res, 400, body.error);
|
|
335
|
+
const { payload, mode, dryRun, workspace, confirm } = body.value ?? {};
|
|
336
|
+
if (mode !== "merge" && mode !== "replace") {
|
|
337
|
+
return writeError(res, 400, "mode must be 'merge' or 'replace'");
|
|
338
|
+
}
|
|
339
|
+
if (mode === "replace" && confirm !== "restore") {
|
|
340
|
+
return writeError(res, 400, "replace mode is destructive — pass confirm: \"restore\" to proceed");
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const report = await domain.importAll(payload, {
|
|
344
|
+
mode: mode ?? "merge",
|
|
345
|
+
dryRun: dryRun === true,
|
|
346
|
+
workspace: typeof workspace === "string" && workspace.length > 0 ? workspace : void 0,
|
|
347
|
+
config: service.config ?? {},
|
|
348
|
+
});
|
|
349
|
+
writeJson(res, 200, report);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const status = error instanceof Error && error.code === "ENGRAM_INVALID_ARGS" ? 400 : 500;
|
|
352
|
+
writeError(res, status, String(error?.message ?? error));
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
285
356
|
const guard = (req, res, method, handler) => {
|
|
286
357
|
if ((req.method ?? "GET").toUpperCase() !== method) {
|
|
287
358
|
writeError(res, 405, "method not allowed");
|
|
@@ -503,6 +574,7 @@ export function makeEngramRoutes(service) {
|
|
|
503
574
|
return [
|
|
504
575
|
make("GET", "/overview", overview),
|
|
505
576
|
make("GET", "/memories", memories),
|
|
577
|
+
make("GET", "/goals", goals),
|
|
506
578
|
makeUnion("/tasks", { GET: tasks, POST: createTask }),
|
|
507
579
|
make("GET", "/links", links),
|
|
508
580
|
make("GET", "/nodes", entities),
|
|
@@ -517,5 +589,7 @@ export function makeEngramRoutes(service) {
|
|
|
517
589
|
make("POST", "/memories/delete", remove),
|
|
518
590
|
make("POST", "/tasks/close", closeTask),
|
|
519
591
|
make("POST", "/gc", gcRun),
|
|
592
|
+
make("GET", "/export", exportData),
|
|
593
|
+
make("POST", "/import", importData),
|
|
520
594
|
];
|
|
521
595
|
}
|
package/lib/capture.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* branch / tag / cherry-pick). Pure plumbing (`push`, `stash`, `status`)
|
|
12
12
|
* is deliberately NOT captured: those are operational echoes, not lasting
|
|
13
13
|
* decisions, and would only drown the memory pool and the [ENGRAM] index.
|
|
14
|
+
* - failing test runs (`npm test` / `node --test` / vitest / jest / pytest)
|
|
15
|
+
* as high-signal error memories tagged `test`.
|
|
14
16
|
* - writes/edits to significant config & doc paths
|
|
15
17
|
* - repeated tool errors (error signal, deduped by message)
|
|
16
18
|
* symbolic-index's own insight: one file edit is noise; accumulated edits and rare
|
|
@@ -22,6 +24,56 @@ import { workspaceKey as workspaceKeyOf } from "./util.js";
|
|
|
22
24
|
|
|
23
25
|
const GIT_MILESTONE_SIGNAL = 0.5;
|
|
24
26
|
const GIT_COMMIT_SIGNAL = 0.55;
|
|
27
|
+
const TEST_FAILURE_SIGNAL = 0.35;
|
|
28
|
+
const FIX_CLOSURE_SIGNAL = 0.45;
|
|
29
|
+
|
|
30
|
+
/** Concrete text: a test-run command (npm/pnpm/yarn/bun/npx/node --test/…). */
|
|
31
|
+
const TEST_RUN_RE = /\b(npm|pnpm|yarn|bun|npx|corepack)\s+(?:run\s+)?test\b|\bnode\s+--test\b|\bvitest\b|\bpytest\b|\bjest\b/;
|
|
32
|
+
/** A result line that reads as a failure point (deterministic keyword scan). */
|
|
33
|
+
const FAILURE_LINE_RE = /(assertionerror|#\s*fail(?:ed)?|not\s+ok|✗|✖|✘|\bFAIL(?:ED)?\b|tests?\s+failed|failed\s+tests?\b|\d+\s+failed|non-zero exit|exception|traceback|error:\s)/i;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a command to a stable short signature tag (first two tokens,
|
|
37
|
+
* spaces → underscores, ≤48 chars) used to pair a failure with its later
|
|
38
|
+
* successful run. Returns null for empty commands.
|
|
39
|
+
*/
|
|
40
|
+
export function cmdTag(command) {
|
|
41
|
+
const tokens = String(command ?? "").trim().split(/\s+/).filter(Boolean).slice(0, 2);
|
|
42
|
+
if (tokens.length === 0) return null;
|
|
43
|
+
return `cmd:${tokens.join("_").slice(0, 48)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Failure→fix closure (zero LLM): when a command that previously failed (an
|
|
48
|
+
* ACTIVE kind:error memory tagged with the same `cmd:` signature) now runs
|
|
49
|
+
* successfully, produce a `procedure` memory plus the ids of the resolved
|
|
50
|
+
* error rows. Deterministic; fires only on genuine same-command
|
|
51
|
+
* success-after-failure.
|
|
52
|
+
*/
|
|
53
|
+
export function fixClosureEntry(name, command, result, previousErrors) {
|
|
54
|
+
if (!["bash", "shell", "exec"].includes(name)) return null;
|
|
55
|
+
if (result?.isError === true) return null; // not a success
|
|
56
|
+
if (typeof command !== "string" || command.trim().length === 0) return null;
|
|
57
|
+
const tag = cmdTag(command);
|
|
58
|
+
if (tag === null) return null;
|
|
59
|
+
const failing = (previousErrors ?? []).filter(
|
|
60
|
+
(m) => m.kind === "error" && m.status === "active" && (m.tags ?? []).includes(tag),
|
|
61
|
+
);
|
|
62
|
+
if (failing.length === 0) return null;
|
|
63
|
+
const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
64
|
+
const isTest = TEST_RUN_RE.test(command);
|
|
65
|
+
const plural = failing.length > 1 ? "s" : "";
|
|
66
|
+
return {
|
|
67
|
+
entry: {
|
|
68
|
+
kind: "procedure",
|
|
69
|
+
text: `fixed: ${head} — ${failing.length} earlier failing run${plural} now succeed`,
|
|
70
|
+
signal: FIX_CLOSURE_SIGNAL,
|
|
71
|
+
tags: ["procedure", "auto-captured", "fix", tag, ...(isTest ? ["test"] : [])],
|
|
72
|
+
filePath: null,
|
|
73
|
+
},
|
|
74
|
+
resolvedIds: failing.map((m) => m.id),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
25
77
|
|
|
26
78
|
/** Git operations that carry lasting meaning (milestones). */
|
|
27
79
|
const GIT_OPS = [
|
|
@@ -109,9 +161,30 @@ export function makeCaptureHandler(store, config, log, stats) {
|
|
|
109
161
|
// Errors take precedence over tool-name classification: a failed edit
|
|
110
162
|
// tool is a FAILURE of that file, not an "Edited" event (filePath anchor).
|
|
111
163
|
const errPath = String(args.path ?? args.file_path ?? args.filePath ?? "");
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
const cmd = cmdTag(args.command); // pairs this failure with a later success
|
|
165
|
+
// A failing test run is the highest-value coding signal: name it
|
|
166
|
+
// explicitly and surface the concrete failure point.
|
|
167
|
+
const testFail = extractTestFailure(name, args.command, result);
|
|
168
|
+
if (testFail !== null) {
|
|
169
|
+
entry = { kind: "error", text: testFail, signal: TEST_FAILURE_SIGNAL, tags: ["error", "test", "auto-captured", cmd].filter(Boolean), filePath: null };
|
|
170
|
+
} else {
|
|
171
|
+
const msg = errorHead(result);
|
|
172
|
+
if (msg !== null) {
|
|
173
|
+
entry = { kind: "error", text: `${name} failed: ${msg}`, signal: 0.25, tags: ["error", "auto-captured", cmd].filter(Boolean), filePath: errPath || null };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else if (isCommandRun(name) && typeof args.command === "string" && store.listMemories !== void 0) {
|
|
177
|
+
// Failure→fix closure: the same command that once failed now succeeds —
|
|
178
|
+
// sediment a `procedure` and resolve the earlier error rows.
|
|
179
|
+
const fix = fixClosureEntry(
|
|
180
|
+
name,
|
|
181
|
+
args.command,
|
|
182
|
+
result,
|
|
183
|
+
store.listMemories(workspaceKeyOf(cwd), 200),
|
|
184
|
+
);
|
|
185
|
+
if (fix !== null) {
|
|
186
|
+
entry = fix.entry;
|
|
187
|
+
entry._resolvedIds = fix.resolvedIds;
|
|
115
188
|
}
|
|
116
189
|
} else if (EDIT_TOOLS.has(name)) {
|
|
117
190
|
const path = String(args.path ?? args.file_path ?? args.filePath ?? "");
|
|
@@ -142,6 +215,7 @@ export function makeCaptureHandler(store, config, log, stats) {
|
|
|
142
215
|
recent.delete(first);
|
|
143
216
|
}
|
|
144
217
|
|
|
218
|
+
const resolvedIds = entry._resolvedIds ?? [];
|
|
145
219
|
void store
|
|
146
220
|
.storeMemory(
|
|
147
221
|
{
|
|
@@ -156,7 +230,13 @@ export function makeCaptureHandler(store, config, log, stats) {
|
|
|
156
230
|
},
|
|
157
231
|
config,
|
|
158
232
|
)
|
|
159
|
-
.then(() => {
|
|
233
|
+
.then(() => {
|
|
234
|
+
// Failure→fix closure: mark the earlier error rows resolved (tag
|
|
235
|
+
// append only — never re-ranks them, per store.tagMemory's contract).
|
|
236
|
+
for (const id of resolvedIds) {
|
|
237
|
+
void store.tagMemory?.(workspaceKeyOf(cwd), id, "resolved")?.catch?.(() => null);
|
|
238
|
+
}
|
|
239
|
+
})
|
|
160
240
|
.catch((error) => log?.warn?.(`engram auto-capture failed: ${String(error)}`));
|
|
161
241
|
} catch (error) {
|
|
162
242
|
// fully contained — a capture bug must never disturb the tool pipeline
|
|
@@ -187,6 +267,11 @@ function extractGit(name, command) {
|
|
|
187
267
|
return null;
|
|
188
268
|
}
|
|
189
269
|
|
|
270
|
+
/** Shell-run tool names whose failures/successes carry a command signature. */
|
|
271
|
+
function isCommandRun(name) {
|
|
272
|
+
return name === "bash" || name === "shell" || name === "exec";
|
|
273
|
+
}
|
|
274
|
+
|
|
190
275
|
function errorHead(result) {
|
|
191
276
|
// canonical value or rendered content
|
|
192
277
|
const value = result?.value;
|
|
@@ -206,6 +291,49 @@ function errorHead(result) {
|
|
|
206
291
|
return null;
|
|
207
292
|
}
|
|
208
293
|
|
|
294
|
+
/**
|
|
295
|
+
* When a bash/shell run of a test command exits non-zero, produce a
|
|
296
|
+
* deterministic failure memory: `tests failed (<cmd>): <failure line>`.
|
|
297
|
+
* Returns null for non-test commands, successful runs, or when no failure
|
|
298
|
+
* line can be pinned down (generic error extraction then applies).
|
|
299
|
+
*/
|
|
300
|
+
function extractTestFailure(name, command, result) {
|
|
301
|
+
if (name !== "bash" && name !== "shell" && name !== "exec") return null;
|
|
302
|
+
if (typeof command !== "string" || !TEST_RUN_RE.test(command)) return null;
|
|
303
|
+
if (result?.isError !== true) return null;
|
|
304
|
+
const line = failureLine(result);
|
|
305
|
+
if (line === null) return null;
|
|
306
|
+
const head = command.trim().split(/\s+/).slice(0, 2).join(" ");
|
|
307
|
+
return `tests failed (${head}): ${line}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** First failure-flavoured line of a result (≤120 chars), else its head. */
|
|
311
|
+
function failureLine(result) {
|
|
312
|
+
const blocks = [];
|
|
313
|
+
const value = result?.value;
|
|
314
|
+
if (typeof value === "string" && value.length > 0) blocks.push(value);
|
|
315
|
+
if (value !== null && typeof value === "object") {
|
|
316
|
+
for (const k of ["error", "stderr", "message", "stdout"]) {
|
|
317
|
+
if (typeof value[k] === "string" && value[k].length > 0) blocks.push(value[k]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const content = result?.content;
|
|
321
|
+
if (Array.isArray(content)) {
|
|
322
|
+
for (const block of content) {
|
|
323
|
+
if (block?.type === "text" && typeof block.text === "string") blocks.push(block.text);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const block of blocks) {
|
|
327
|
+
const lines = block.split("\n");
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
const trimmed = line.trim().replace(/\s+/g, " ").slice(0, 120);
|
|
330
|
+
if (FAILURE_LINE_RE.test(trimmed)) return trimmed;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const first = blocks[0];
|
|
334
|
+
return typeof first === "string" && first.length > 0 ? first.split("\n")[0].trim().slice(0, 120) : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
209
337
|
function dirOf(path) {
|
|
210
338
|
const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
211
339
|
return i > 0 ? path.slice(0, i) : ".";
|
package/lib/client.js
CHANGED
|
@@ -3341,6 +3341,7 @@ var import_react8 = require("react");
|
|
|
3341
3341
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
3342
3342
|
var COMMON_FIELDS = [
|
|
3343
3343
|
{ key: "autoCapture", label: "\u81EA\u52A8\u6355\u83B7", hint: "\u96F6 LLM \u4ECE\u5DE5\u5177\u7ED3\u679C\u63D0\u53D6\u8BB0\u5FC6\uFF08git/\u5173\u952E\u6587\u4EF6/\u9519\u8BEF\uFF09", kind: "bool" },
|
|
3344
|
+
{ key: "autoRecallOnStart", label: "\u4F1A\u8BDD\u542F\u52A8\u81EA\u52A8\u53EC\u56DE", hint: "\u65B0\u4F1A\u8BDD\u6309\u9996\u6761\u6D88\u606F\u81EA\u52A8\u628A\u6700\u76F8\u5173\u8BB0\u5FC6\u6CE8\u5165 [RECALL] \u5757\uFF08\u65E0\u9700\u6A21\u578B\u60F3\u8D77\u8C03 engram_recall\uFF09\uFF1B\u5173\u6389=\u56DE\u5230\u7EAF\u62C9\u53D6", kind: "bool" },
|
|
3344
3345
|
{ key: "maxRecallPerSession", label: "\u6BCF\u4F1A\u8BDD\u53BB\u91CD\u4E0A\u9650", hint: "recall \u6BCF\u4F1A\u8BDD\u6700\u591A\u4FDD\u7559 N \u6761\uFF08\u8DE8\u4F1A\u8BDD\u53BB\u91CD\uFF09", kind: "num", min: 1, max: 10 },
|
|
3345
3346
|
{ key: "expireDays", label: "TTL\uFF08\u5929\uFF09", hint: "0 = \u4E0D\u8FC7\u671F", kind: "num", min: 0, max: 3650 },
|
|
3346
3347
|
{ key: "indexMaxChars", label: "\u7D22\u5F15\u5B57\u7B26\u4E0A\u9650", hint: "[ENGRAM] \u5757 token \u9884\u7B97", kind: "num", min: 0, max: 4e3 }
|
|
@@ -3352,7 +3353,9 @@ var ADVANCED_GROUPS = [
|
|
|
3352
3353
|
description: "\u5176\u4F59\u6355\u83B7/\u641C\u7D22\u65CB\u94AE",
|
|
3353
3354
|
fields: [
|
|
3354
3355
|
{ key: "autoCapturePerSession", label: "\u6BCF\u4F1A\u8BDD\u6355\u83B7\u4E0A\u9650", hint: "\u5355\u4F1A\u8BDD\u81EA\u52A8\u6355\u83B7\u6761\u6570\u4E0A\u9650", kind: "num", min: 0, max: 1e3 },
|
|
3355
|
-
{ key: "
|
|
3356
|
+
{ key: "autoRecallLimit", label: "\u81EA\u52A8\u53EC\u56DE\u6761\u6570", hint: "[RECALL] \u5757\u6700\u591A\u6CE8\u5165\u7684\u8BB0\u5FC6\u6761\u6570\uFF08\u5C11\u800C\u51C6\uFF09", kind: "num", min: 1, max: 8 },
|
|
3357
|
+
{ key: "sessionSearch", label: "\u4F1A\u8BDD\u5386\u53F2\u641C\u7D22", hint: "engram_recall \u652F\u6301\u8DE8\u4F1A\u8BDD FTS \u515C\u5E95", kind: "bool" },
|
|
3358
|
+
{ key: "recallScope", label: "\u53EC\u56DE\u8303\u56F4", hint: "workspace=\u4E25\u683C\u5F53\u524D\u5DE5\u4F5C\u533A\u9694\u79BB\uFF08\u9ED8\u8BA4\uFF0Ctoken \u7EAA\u5F8B\uFF09\uFF1Bglobal=\u672C\u5DE5\u4F5C\u533A\u547D\u4E2D\u5F31/\u7A7A\u65F6\u4ECE\u5176\u5B83\u5DE5\u4F5C\u533A\u6C60\u53EC\u56DE\u5E76\u6807 [W:<workspace>] \u6765\u6E90\uFF08\u4F1A\u6253\u7834\u9694\u79BB\uFF09", kind: "select", options: ["workspace", "global"] }
|
|
3356
3359
|
]
|
|
3357
3360
|
},
|
|
3358
3361
|
{
|
|
@@ -3374,7 +3377,17 @@ var ADVANCED_GROUPS = [
|
|
|
3374
3377
|
{ key: "gcEnabled", label: "\u8BB0\u5FC6 GC", hint: "\u5B9A\u65F6\u56DE\u6536\uFF08\u8FC7\u671F/\u8D85\u5BB9\u91CF/stable \u8D85\u7A97/\u60AC\u7A7A\u94FE\u63A5\uFF09", kind: "bool" },
|
|
3375
3378
|
{ key: "gcNarrative", label: "Context GC \u53D9\u4E8B\u515C\u5E95", hint: "\u81EA\u52A8 compact \u65F6\u65E0\u951A\u8F6E\u6B21\u7528 LLM \u6458\u8981\uFF1B\u5173\u6389=\u7EAF\u673A\u68B0\uFF08\u96F6 LLM\uFF09", kind: "bool" },
|
|
3376
3379
|
{ key: "gcStableRetentionDays", label: "stable \u4EFB\u52A1\u4FDD\u7559\uFF08\u5929\uFF09", hint: "\u8D85\u7A97\u540E\u7531 GC \u5F52\u6863\u3001\u79BB\u5F00 [ESR] \u8868\u9762", kind: "num", min: 0, max: 3650 },
|
|
3377
|
-
{ key: "verifyArtifact", label: "\u6821\u9A8C artifact \u5B58\u5728", hint: "esr_close \u7684 artifact \u987B\u5728\u5DE5\u4F5C\u533A\u78C1\u76D8\u4E0A\u771F\u5B9E\u5B58\u5728\uFF08URL \u8C41\u514D\uFF09\uFF1Bforce:true/\u5173\u6389\u53EF\u8DF3\u8FC7", kind: "bool" }
|
|
3380
|
+
{ key: "verifyArtifact", label: "\u6821\u9A8C artifact \u5B58\u5728", hint: "esr_close \u7684 artifact \u987B\u5728\u5DE5\u4F5C\u533A\u78C1\u76D8\u4E0A\u771F\u5B9E\u5B58\u5728\uFF08URL \u8C41\u514D\uFF09\uFF1Bforce:true/\u5173\u6389\u53EF\u8DF3\u8FC7", kind: "bool" },
|
|
3381
|
+
{ key: "autoSinkTodosOnEnd", label: "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u6C89\u6DC0 todo", hint: "\u4F1A\u8BDD\u7ED3\u675F\u65F6\u4ECD pending \u7684 todo \u81EA\u52A8\u8F6C\u6210 ESR draft\uFF08\u6309\u540D\u53BB\u91CD\u3001\u53D7\u4EFB\u52A1\u4E0A\u9650\u7EA6\u675F\uFF09\uFF1B\u5173\u6389=\u968F\u4F1A\u8BDD\u5F03\u53BB", kind: "bool" },
|
|
3382
|
+
{ key: "autoWebCompaction", label: "web \u9762\u81EA\u52A8\u542F\u7528 Context GC", hint: "\u542F\u52A8\u65F6\u628A\u5168\u90E8 stock \u5E03\u5C40\u7684 agent \u9884\u8BBE\uFF08shipped + \u81EA\u5B9A\u4E49\u6839\uFF09\u7684 compaction \u7EC4\u81EA\u52A8\u6362\u6210 dsh-engram/compaction\uFF1B\u5173\u6389=\u4E0D\u518D\u81EA\u52A8\u63A5\u7BA1\uFF08\u5DF2\u63A5\u7BA1\u7684\u9700 web-compaction:revert \u8FD8\u539F\uFF09", kind: "bool" }
|
|
3383
|
+
]
|
|
3384
|
+
},
|
|
3385
|
+
{
|
|
3386
|
+
id: "semantics-adv",
|
|
3387
|
+
title: "\u8BB0\u5FC6\u8BED\u4E49\uFF08\u9AD8\u7EA7\uFF09",
|
|
3388
|
+
description: "supersedes / contradict \u7684\u542F\u53D1\u5F0F",
|
|
3389
|
+
fields: [
|
|
3390
|
+
{ key: "autoSupersede", label: "\u81EA\u52A8 supersedes", hint: "\u5B9E\u4F53\u951A\u5B9A + \u66FF\u6362\u5F0F\u66F4\u65B0\uFF08\u6539\u7528/\u4E0D\u518D/no longer\u2026\uFF09\u81EA\u52A8\u628A\u65E7\u8BB0\u5FC6\u6807\u4E3A superseded\uFF1B\u65E7\u884C\u4FDD\u7559\u3001\u53EC\u56DE\u964D\u7EA7\u3002\u9ED8\u8BA4\u5173\uFF0C\u663E\u5F0F supersedes \u6C38\u8FDC\u4F18\u5148", kind: "bool" }
|
|
3378
3391
|
]
|
|
3379
3392
|
},
|
|
3380
3393
|
{
|
|
@@ -3641,6 +3654,15 @@ function EngramConfigCard({ scope }) {
|
|
|
3641
3654
|
disabled: !writable,
|
|
3642
3655
|
onChange: (e) => setField(field.key, e.target.checked)
|
|
3643
3656
|
}
|
|
3657
|
+
) : field.kind === "select" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
3658
|
+
"select",
|
|
3659
|
+
{
|
|
3660
|
+
style: { ...s3.input, width: field.width ?? 160 },
|
|
3661
|
+
value: raw === void 0 ? "" : String(raw),
|
|
3662
|
+
disabled: !writable,
|
|
3663
|
+
onChange: (e) => setField(field.key, e.target.value),
|
|
3664
|
+
children: (field.options ?? []).map((opt) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: opt, children: opt }, opt))
|
|
3665
|
+
}
|
|
3644
3666
|
) : field.kind === "text" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
3645
3667
|
"input",
|
|
3646
3668
|
{
|
|
@@ -3882,6 +3904,26 @@ var TREND_LABEL = {
|
|
|
3882
3904
|
weakening: "\u51CF\u5F31",
|
|
3883
3905
|
stale: "\u9648\u65E7"
|
|
3884
3906
|
};
|
|
3907
|
+
function ContextGcChip({ compaction }) {
|
|
3908
|
+
const host = compaction?.host ?? "unknown";
|
|
3909
|
+
const hostLabel = host === "context-gc" ? "Context GC\xB7\u4E3B\u673A" : host === "default" ? "\u9ED8\u8BA4\u6458\u8981" : host === "unavailable" ? "GC \u4E0D\u53EF\u7528" : "GC \u72B6\u6001\u2026";
|
|
3910
|
+
const wired = (compaction?.web?.presets ?? []).filter((p) => p.action === "swapped" || p.action === "already").length;
|
|
3911
|
+
const webLabel = wired > 0 ? `\xB7${wired} \u9884\u8BBE` : "";
|
|
3912
|
+
const color = host === "context-gc" ? "var(--dsw-alias-success-strong, #059669)" : host === "default" || host === "unavailable" ? "var(--dsw-alias-label-secondary, var(--dsh-color-muted, #6b7280))" : "var(--dsw-alias-label-tertiary, #9ca3af)";
|
|
3913
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
3914
|
+
"span",
|
|
3915
|
+
{
|
|
3916
|
+
title: `host \u5E73\u9762\uFF1A${host}
|
|
3917
|
+
web \u5E73\u9762\uFF1A${compaction?.web?.action ?? "?"}\uFF08${wired} \u4E2A\u9884\u8BBE\u5DF2\u63A5\u7BA1\uFF09
|
|
3918
|
+
\u5173\u6389\u81EA\u52A8\u88C5\u914D\u89C1\u8BBE\u7F6E\u5361\u300Cweb \u9762\u81EA\u52A8\u542F\u7528 Context GC\u300D\uFF1B\u8FD8\u539F\u7528 npx dsh-engram revert`,
|
|
3919
|
+
style: { fontSize: 11, fontWeight: 600, color, border: `1px solid ${color}55`, borderRadius: 999, padding: "2px 8px", whiteSpace: "nowrap" },
|
|
3920
|
+
children: [
|
|
3921
|
+
hostLabel,
|
|
3922
|
+
webLabel
|
|
3923
|
+
]
|
|
3924
|
+
}
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3885
3927
|
function EngramBoard({ api, onRequestClose }) {
|
|
3886
3928
|
const { vars } = useEngramTheme();
|
|
3887
3929
|
const [overview, setOverview] = (0, import_react9.useState)(null);
|
|
@@ -4108,6 +4150,7 @@ function EngramBoard({ api, onRequestClose }) {
|
|
|
4108
4150
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: hb.title, children: "ESR \u4EFB\u52A1\u770B\u677F" }),
|
|
4109
4151
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: hb.sub, children: "draft \u2192 active(\u8BC1\u636E) \u2192 stable \xB7 \u8DE8\u5DE5\u4F5C\u533A \xB7 \u4E0E esr_task/esr_close \u540C\u4E00\u8BC1\u636E\u95E8" }),
|
|
4110
4152
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: hb.legendChip, children: "\u4EC5 ESR \xB7 \u4F1A\u8BDD\u5185 todo \u89C1\u8F93\u5165\u6846\u4E0A\u65B9\u4EFB\u52A1\u6761" }),
|
|
4153
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ContextGcChip, { compaction: overview?.compaction }),
|
|
4111
4154
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: hb.seg, children: [
|
|
4112
4155
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", style: viewMode === "board" ? hb.segActive : hb.segBtn, onClick: () => setViewMode("board"), title: "\u56DB\u5217\u4EFB\u52A1\u770B\u677F", children: "\u770B\u677F" }),
|
|
4113
4156
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", style: viewMode === "graph" ? hb.segActive : hb.segBtn, onClick: () => setViewMode("graph"), title: "\u5B9E\u4F53\u5173\u7CFB\u56FE\u8C31\uFF08esr_node / esr_link\uFF09", children: "\u56FE\u8C31" })
|
package/lib/compaction.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* entry never writes).
|
|
32
32
|
*/
|
|
33
33
|
import { openEngramDomain } from "./store.js";
|
|
34
|
-
import {
|
|
34
|
+
import { mountCompactionEngine, collectPointerContext } from "./context-gc.js";
|
|
35
35
|
import { workspaceKey as wk } from "./util.js";
|
|
36
36
|
|
|
37
37
|
export const name = "dsh-engram-compaction";
|
|
@@ -56,9 +56,6 @@ export function apply(ctx, config = {}) {
|
|
|
56
56
|
const deps =
|
|
57
57
|
mode === "context-gc"
|
|
58
58
|
? {
|
|
59
|
-
narrativeEnabled: config.gcNarrative !== false,
|
|
60
|
-
narrativeMaxTokens: config.gcNarrativeMaxTokens ?? 1024,
|
|
61
|
-
narrativeMaxChars: config.gcNarrativeMaxChars ?? 4000,
|
|
62
59
|
readWorkspace: async (workspace, report) => {
|
|
63
60
|
if (!workspace) return null;
|
|
64
61
|
const domain = await domainFor(ctx);
|
|
@@ -68,18 +65,5 @@ export function apply(ctx, config = {}) {
|
|
|
68
65
|
}
|
|
69
66
|
: {};
|
|
70
67
|
|
|
71
|
-
return
|
|
72
|
-
.then((Engine) => {
|
|
73
|
-
if (!Engine) {
|
|
74
|
-
ctx.logger?.warn?.("engram context-gc: dsh-compaction-basic unavailable — no compaction engine");
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
new Engine();
|
|
78
|
-
ctx.logger?.info?.(
|
|
79
|
-
`engram context-gc: compaction = ${mode === "context-gc" ? "Context GC (mechanical eviction + re-fetch pointers)" : "default LLM summarizer (gcReplacesCompaction:false)"}`,
|
|
80
|
-
);
|
|
81
|
-
})
|
|
82
|
-
.catch((error) => {
|
|
83
|
-
ctx.logger?.warn?.(`engram context-gc mount failed: ${String(error)}`);
|
|
84
|
-
});
|
|
68
|
+
return mountCompactionEngine(ctx, config, deps);
|
|
85
69
|
}
|