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/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 > MAX_JSON_BODY_BYTES) {
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 msg = errorHead(result);
113
- if (msg !== null) {
114
- entry = { kind: "error", text: `${name} failed: ${msg}`, signal: 0.25, tags: ["error", "auto-captured"], filePath: errPath || null };
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: "sessionSearch", label: "\u4F1A\u8BDD\u5386\u53F2\u641C\u7D22", hint: "engram_recall \u652F\u6301\u8DE8\u4F1A\u8BDD FTS \u515C\u5E95", kind: "bool" }
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 { loadCompactionEngine, collectPointerContext } from "./context-gc.js";
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 loadCompactionEngine(ctx, { auto: true }, deps, { mode })
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
  }