dsh-prime-memory 0.26.10 → 0.26.11

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.
Files changed (43) hide show
  1. package/CHANGELOG.de.md +1638 -0
  2. package/CHANGELOG.en.md +15 -0
  3. package/CHANGELOG.es.md +1641 -0
  4. package/CHANGELOG.fr.md +1643 -0
  5. package/CHANGELOG.it.md +1643 -0
  6. package/CHANGELOG.ja.md +15 -0
  7. package/CHANGELOG.ko.md +15 -0
  8. package/CHANGELOG.md +34 -0
  9. package/CHANGELOG.ru.md +1640 -0
  10. package/README.en.md +2 -2
  11. package/README.md +9 -2
  12. package/dist/bench-control.js +3 -1
  13. package/dist/client.js +121 -12
  14. package/dist/clients/drsc.d.ts +34 -0
  15. package/dist/clients/drsc.js +56 -11
  16. package/dist/config.d.ts +10 -1
  17. package/dist/config.js +37 -18
  18. package/dist/contract.d.ts +48 -1
  19. package/dist/graph/gate.d.ts +17 -0
  20. package/dist/graph/gate.js +11 -0
  21. package/dist/graph/preview.d.ts +5 -0
  22. package/dist/graph/preview.js +2 -0
  23. package/dist/index.js +1 -1
  24. package/dist/pipeline/fact-source.d.ts +127 -0
  25. package/dist/pipeline/fact-source.js +192 -0
  26. package/dist/pipeline/rebuild-preserve.d.ts +31 -2
  27. package/dist/pipeline/rebuild-preserve.js +40 -14
  28. package/dist/pipeline/rebuild.d.ts +47 -13
  29. package/dist/pipeline/rebuild.js +153 -57
  30. package/dist/pipeline/runner.d.ts +4 -1
  31. package/dist/pipeline/runner.js +58 -21
  32. package/dist/pipeline/workspace-relink.js +7 -2
  33. package/dist/room-review.d.ts +1 -1
  34. package/dist/room-review.js +10 -5
  35. package/dist/settings.js +4 -0
  36. package/dist/stats.js +10 -3
  37. package/dist/store/graph-store.d.ts +20 -0
  38. package/dist/store/graph-store.js +50 -1
  39. package/dist/store/sqlite.d.ts +42 -0
  40. package/dist/store/sqlite.js +98 -0
  41. package/dist/tools/index.js +4 -3
  42. package/dsh.plugin.json +1 -1
  43. package/package.json +16 -11
package/README.en.md CHANGED
@@ -565,9 +565,9 @@ directly ("zero source-less facts" invariant: every edge traces back to live L1
565
565
  | Action | Tool | Preconditions | Notes |
566
566
  | --- | --- | --- | --- |
567
567
  | Revise one memory | `memory_update` | high-privilege mode | Read old → create new (`version+1`, new id) → old row retires via the shared soft-delete confluence with a `superseded` marker (restorable) → new version enters retrieval; then both [old, new] ids are force-reprojected so graph edges follow (edges supported only by the old source get tombstoned). Not a physical delete; the JSONL fact source is append-only and never rewritten |
568
- | Force reprojection | `memory_reproject` | `graph.enabled` | Erases the "already projected at current version" receipts and re-enqueues at top priority, bypassing the permanent `projector_version` skip. Idempotent: in-flight jobs never pile up. **Manually added/imported memories that never showed up in the graph: this is the back-fill projection channel** (non-distilled writes now auto-enqueue; the tool remains the correction path for legacy rows and post-edit freshness) |
568
+ | Force reprojection | `memory_reproject` | Overview → Knowledge graph switch, or `graph.enabled` | Erases the "already projected at current version" receipts and re-enqueues at top priority, bypassing the permanent `projector_version` skip. Idempotent: in-flight jobs never pile up. **Manually added/imported memories that never showed up in the graph: this is the back-fill projection channel** (non-distilled writes now auto-enqueue; the tool remains the correction path for legacy rows and post-edit freshness) |
569
569
  | Migrate one relation | `memory_migrate_relation` | high-privilege mode | Source-derived semantics: hard-validate that every `source_record_ids` is live and un-retired → the new relation lands through the deterministic projection branch (same (from, relation) with a new target closes the old edge) → a new L1 carrier record is written as the traceable basis (tagged `proposal_source=tool` plus a decision receipt). Old edges are not flipped back by later rejections: the reprojection batch pulls only the carrier, not sources whose text still describes the old relation |
570
- | Non-distilled writes auto-enqueue | `memory_add` / `memory_import` | `graph.enabled` | After a successful write, records join the projection queue at the same priority as distillation output (`GRAPH_PRIORITY_NEW`) — the "only distillation projects" bypass is closed |
570
+ | Non-distilled writes auto-enqueue | `memory_add` / `memory_import` | Overview → Knowledge graph switch, or `graph.enabled` | After a successful write, records join the projection queue at the same priority as distillation output (`GRAPH_PRIORITY_NEW`) — the "only distillation projects" bypass is closed |
571
571
 
572
572
  Two boundaries worth knowing:
573
573
 
package/README.md CHANGED
@@ -436,6 +436,13 @@ dsh 宿主把插件日志打到控制台;插件另把 info 级以上镜像到
436
436
  前 400 字符;所有失败告警带堆栈首帧。JSONL 事实源按轮次追加、依赖操作系统写回(不做逐条
437
437
  fsync),断电等极端崩溃最多丢最后一小段尾部,检索库可用「重建记忆」从事实源全量重导。
438
438
 
439
+ > **重建的事实源边界(0.26.11 起要说清楚)**:重建只能重蒸**事实源覆盖到的那一刻之前**的记忆。L0 正文自 T8.4 起已停写(历史面由 DRSC 供给),所以水位之后新增的记忆**不会被重新蒸馏**——它们会被**原样保留**(保留理由 `post-watermark`:来源消息不在本次快照内),不会丢。确认弹窗会直接写出「事实源覆盖到 X」与「可重造 N 条 / 原样保留 M 条」;若晚于水位的记录超过一半,启动需要二次确认(`force`)——这是**提示**而非永久拒绝(否则停写的库上重建会被永久锁死)。
440
+
441
+ > **重建的事实源可切换(`rebuild.factSource`,默认 `local`)**:
442
+ > - `local`:本地 `l0_conversations`(已停写的冻结存量),**按会话流式读**——峰值只占单会话,不再把全量正文(3 万条量级约 88MB)驻留整个重建期;
443
+ > - `drsc`(实验性):`reference-session-collector` 的投影——先枚举会话、再逐会话拉全量,**恒带 `redact=1`**(collector 的 `session` 缺省是原文臂,而捕获侧是写前脱敏)。选了 drsc 但 collector 未装配会**显式报错中止**,不静默回退本地表。
444
+ > - 注意:DRSC 无法枚举消息 id(`drsc:<sid>:<seq>` 与事实源的 `source_message_ids` 不同域),所以 `drsc` 模式下所有有来源的记忆都会被判为**不可重蒸并原样保留**(保守侧失败,不丢数据)。
445
+
439
446
  ## 与 MemoryCore 的差异
440
447
 
441
448
  - 内嵌完整管线(不依赖外部 Gateway),蒸馏复用 DSH 自己的 LLM;
@@ -513,9 +520,9 @@ fsync),断电等极端崩溃最多丢最后一小段尾部,检索库可用
513
520
  | 动作 | 工具 | 前提 | 说明 |
514
521
  | --- | --- | --- | --- |
515
522
  | 修订一条记忆 | `memory_update` | 高权限模式 | 读旧 → 造新(`version+1`,新 id)→ 旧行经退场汇流口 `superseded` 软删(可恢复)→ 新版进检索面;随后对 [旧,新] 强制重投,图谱边跟随(旧源支撑的边自动标墓碑)。不是物理删,也不覆写 JSONL 事实源 |
516
- | 强制重投 | `memory_reproject` | 配置 `graph.enabled` | 抹掉"已按当前版本投影"凭证、按高优先级重新入队——绕开 `projector_version` 的永久跳过。幂等:在途任务不堆重。**手加/导入的记忆长期不出现在图谱里,用它补投影**(非蒸馏写入现已自动入队,此工具是给存量与改后保鲜的纠正通道) |
523
+ | 强制重投 | `memory_reproject` | 概览「知识图谱」开关 或 部署 `graph.enabled` | 抹掉"已按当前版本投影"凭证、按高优先级重新入队——绕开 `projector_version` 的永久跳过。幂等:在途任务不堆重。**手加/导入的记忆长期不出现在图谱里,用它补投影**(非蒸馏写入现已自动入队,此工具是给存量与改后保鲜的纠正通道) |
517
524
  | 迁移一条关系 | `memory_migrate_relation` | 高权限模式 | 源导语义:先硬校验 `source_record_ids` 全部"现存未退场" → 新关系经确定性投影支路落库(同 (起点,关系) 换目标自动闭旧边出新边)→ 新增一条承载新关系的 L1 源记录留可回溯根据(带 `proposal_source=tool` 审计标记与决策凭证)。旧边不会因后续重投被翻回:重投批只拉载体记录,不拉描述旧关系的旧源 |
518
- | 非蒸馏写入自动入队 | `memory_add` / `memory_import` | 配置 `graph.enabled` | 落库成功后与蒸馏产出同优先级(`GRAPH_PRIORITY_NEW`)进投影队——"只有蒸馏才投影"的旁路缺口已闭合 |
525
+ | 非蒸馏写入自动入队 | `memory_add` / `memory_import` | 概览「知识图谱」开关 或 部署 `graph.enabled` | 落库成功后与蒸馏产出同优先级(`GRAPH_PRIORITY_NEW`)进投影队——"只有蒸馏才投影"的旁路缺口已闭合 |
519
526
 
520
527
  两条必须知道的边界:
521
528
 
@@ -4,7 +4,9 @@ export const BENCH_CONTROL_SERVICE = 'dsh-memory-bench';
4
4
  /** 注册控制服务,返回注销函数(调用方在插件 dispose 时执行)。 */
5
5
  export function registerBenchControl(ctx, rebuild, modes, logger) {
6
6
  const surface = {
7
- rebuildStart: () => rebuild.start(),
7
+ // bench 场景自带可控数据集,需要确定性重建:水位落差的“二次确认”
8
+ // 属于人机交互护栏,不该阻断自动化赛道,故显式 force。
9
+ rebuildStart: () => rebuild.start(true),
8
10
  rebuildStatus: () => rebuild.getStatus(),
9
11
  setSessionMode: (sessionId, mode) => modes.set(sessionId, mode),
10
12
  getSessionMode: (sessionId) => modes.get(sessionId),
package/dist/client.js CHANGED
@@ -49,9 +49,9 @@ var __create = Object.create;
49
49
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
50
50
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
51
51
 
52
- // node_modules/@antv/g6/dist/g6.min.js
52
+ // node_modules/.pnpm/@antv+g6@5.1.1/node_modules/@antv/g6/dist/g6.min.js
53
53
  var require_g6_min = __commonJS({
54
- "node_modules/@antv/g6/dist/g6.min.js"(exports, module2) {
54
+ "node_modules/.pnpm/@antv+g6@5.1.1/node_modules/@antv/g6/dist/g6.min.js"(exports, module2) {
55
55
  !(function(t, e) {
56
56
  "object" == typeof exports && "undefined" != typeof module2 ? e(exports) : "function" == typeof define && define.amd ? define(["exports"], e) : e((t = "undefined" != typeof globalThis ? globalThis : t || self).G6 = {});
57
57
  })(exports, function(t) {
@@ -37508,6 +37508,17 @@ var __create = Object.create;
37508
37508
  return "关闭";
37509
37509
  }
37510
37510
 
37511
+ // client/src/copy.ts
37512
+ var GRAPH_FEATURE_LABEL = "知识图谱";
37513
+ var GRAPH_TOOL_LABEL = "图谱读工具";
37514
+ var GRAPH_BUDGET_LABEL = "图谱投影预算";
37515
+ function graphEnableHint() {
37516
+ return `在设置页「概览 → ${GRAPH_FEATURE_LABEL}」打开开关后,存量记忆会逐批投影成实体与关系。`;
37517
+ }
37518
+ function graphSwitchDesc(on) {
37519
+ return on ? "已开启:L1 记忆会投影成实体与关系,存量在启动/开启后逐批补齐(可与「图谱读工具」独立开关)" : "默认关闭:开启后才会生成知识图谱;这一步会逐条走 LLM,首次开启前会先给出预计耗时";
37520
+ }
37521
+
37511
37522
  // client/src/tabs/ArchiveSection.tsx
37512
37523
  var import_react7 = require("react");
37513
37524
  var import_jsx_runtime7 = require("react/jsx-runtime");
@@ -37788,7 +37799,9 @@ var __create = Object.create;
37788
37799
  ["dedup", "去重"],
37789
37800
  ["l2", "L2 场景"],
37790
37801
  ["l3", "L3 画像"],
37791
- ["graph", "图谱投影"]
37802
+ // 这不是“投影开关”,是投影这一步的**输出预算**(issue #16:旧标签就叫“图谱投影”,
37803
+ // 与概览页那个功能开关撞名,用户点了以为开了投影,实际只是改了 token 预算)。
37804
+ ["graph", GRAPH_BUDGET_LABEL]
37792
37805
  ];
37793
37806
  var SCOPE_KEYS = {
37794
37807
  l1: ["extract", "dedup"],
@@ -39314,6 +39327,7 @@ var __create = Object.create;
39314
39327
  const [confirmOpen, setConfirmOpen] = (0, import_react14.useState)(false);
39315
39328
  const [busy, setBusy] = (0, import_react14.useState)(false);
39316
39329
  const [rbError, setRbError] = (0, import_react14.useState)(null);
39330
+ const [canForce, setCanForce] = (0, import_react14.useState)(false);
39317
39331
  const refresh = (0, import_react14.useCallback)(() => {
39318
39332
  rpc("dsh-memory/rebuild-status", {}).then((r) => {
39319
39333
  if (r && r.ok) setRb(r.value);
@@ -39334,20 +39348,25 @@ var __create = Object.create;
39334
39348
  if (!rbRaw || rbRaw.supported === false) return null;
39335
39349
  const rb = rbRaw;
39336
39350
  ensureThemeStyle();
39337
- const start = () => {
39351
+ const start = (force = false) => {
39338
39352
  setBusy(true);
39339
39353
  setRbError(null);
39340
- rpc("dsh-memory/rebuild-start", {}).then((r) => {
39354
+ setCanForce(false);
39355
+ rpc("dsh-memory/rebuild-start", force ? { force: true } : {}).then((r) => {
39341
39356
  setBusy(false);
39342
39357
  if (r && r.ok) {
39343
39358
  setConfirmOpen(false);
39344
39359
  setRb(r.value);
39345
39360
  } else {
39346
- setRbError(r && r.error ? r.error.message : "启动失败");
39361
+ const msg = r && r.error ? r.error.message : "启动失败";
39362
+ setRbError(msg);
39363
+ setCanForce(/force/.test(msg));
39347
39364
  }
39348
39365
  }).catch((e) => {
39349
39366
  setBusy(false);
39350
- setRbError(String(e && e.message || e));
39367
+ const msg = String(e && e.message || e);
39368
+ setRbError(msg);
39369
+ setCanForce(/force/.test(msg));
39351
39370
  });
39352
39371
  };
39353
39372
  const cancel = () => {
@@ -39391,7 +39410,13 @@ var __create = Object.create;
39391
39410
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dsh-mem-rb-muted", style: { whiteSpace: "nowrap" }, children: "产出 " + rb.recordsBuilt + " 条" })
39392
39411
  ] }) : null,
39393
39412
  lastNote ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: lastNote }) : null,
39394
- rbError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: rbError }) : null,
39413
+ rb.rederivableCount !== null || rb.preserveCount !== null ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 6 }, children: "本次计划:可重造 " + (rb.rederivableCount ?? 0) + " 条 · 原样保留 " + (rb.preserveCount ?? 0) + " 条(无来源或来源不在本次快照)" }) : null,
39414
+ rbError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: [
39415
+ rbError,
39416
+ canForce ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(NButton, { disabled: busy, onClick: () => {
39417
+ start(true);
39418
+ }, children: busy ? "启动中…" : "我已了解,仍要继续(不重蒸水位后的记录)" }) }) : null
39419
+ ] }) : null,
39395
39420
  confirmOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
39396
39421
  NModal,
39397
39422
  {
@@ -39411,7 +39436,9 @@ var __create = Object.create;
39411
39436
  },
39412
39437
  "cancel"
39413
39438
  ),
39414
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(NButton, { variant: "primary", disabled: busy, onClick: start, children: busy ? "启动中…" : "开始重建" }, "confirm")
39439
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(NButton, { variant: "primary", disabled: busy, onClick: () => {
39440
+ start();
39441
+ }, children: busy ? "启动中…" : "开始重建" }, "confirm")
39415
39442
  ],
39416
39443
  children: [
39417
39444
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
@@ -39419,6 +39446,11 @@ var __create = Object.create;
39419
39446
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("b", { children: rb.sessionCount + " 个会话 · " + rb.messageCount + " 条消息" }),
39420
39447
  ",预计 ≥" + rb.estCalls + " 次蒸馏调用。"
39421
39448
  ] }),
39449
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { marginTop: 8 }, children: [
39450
+ "事实源覆盖到:",
39451
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("b", { children: rb.factWatermark ? fmtTime(rb.factWatermark) : "无(本地 L0 没有任何正文)" }),
39452
+ rb.factWatermark ? ";晚于此时刻的记忆会被原样保留、不重新蒸馏(不会丢)。" : "。"
39453
+ ] }),
39422
39454
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { marginTop: 8 }, children: "现有 L1 记忆 / L2 场景 / L3 画像会整体归档(*.bak.时间戳,可手工找回),随后清空重建;重建期间可正常对话,新对话的蒸馏优先进行;中途可取消,已重建部分保留。" })
39423
39455
  ]
39424
39456
  }
@@ -39929,6 +39961,7 @@ var __create = Object.create;
39929
39961
  const [stats, setStats] = (0, import_react17.useState)(null);
39930
39962
  const [settingsData, setSettingsData] = (0, import_react17.useState)(null);
39931
39963
  const [error, setError] = (0, import_react17.useState)(null);
39964
+ const [graphConfirm, setGraphConfirm] = (0, import_react17.useState)(null);
39932
39965
  const load = (0, import_react17.useCallback)(() => {
39933
39966
  rpc("dsh-memory/stats", {}).then((r) => {
39934
39967
  if (r && r.ok) setStats(r.value);
@@ -39966,6 +39999,14 @@ var __create = Object.create;
39966
39999
  setError("开关写入失败:" + String(e && e.message || e));
39967
40000
  });
39968
40001
  };
40002
+ const requestGraphOn = () => {
40003
+ rpc("dsh-memory/graph-preview", {}).then((r) => {
40004
+ const missing = r && r.ok ? r.value.stats?.missing ?? 0 : 0;
40005
+ setGraphConfirm({ missing });
40006
+ }).catch(() => {
40007
+ setGraphConfirm({ missing: 0 });
40008
+ });
40009
+ };
39969
40010
  const setNum = (key, value) => {
39970
40011
  if (!settingsData) return;
39971
40012
  const prev = settingsData;
@@ -40021,6 +40062,7 @@ var __create = Object.create;
40021
40062
  const toolMutate = settingsData && settingsData.settings ? settingsData.settings.toolMutate !== false : true;
40022
40063
  const toolSkills = settingsData && settingsData.settings ? settingsData.settings.toolSkills !== false : true;
40023
40064
  const skillsEnabled = settingsData && settingsData.settings ? settingsData.settings.skillsEnabled === true : false;
40065
+ const graphEnabled = settingsData && settingsData.settings ? settingsData.settings.graphEnabled === true : false;
40024
40066
  const keywordEnabled = settingsData && settingsData.settings ? settingsData.settings.keywordEnabled === true : false;
40025
40067
  const redistillEnabled = settingsData && settingsData.settings ? settingsData.settings.redistillEnabled === true : false;
40026
40068
  const mdSyncEnabled = settingsData && settingsData.settings ? settingsData.settings.mdSyncEnabled === true : false;
@@ -40100,6 +40142,23 @@ var __create = Object.create;
40100
40142
  }
40101
40143
  }
40102
40144
  ),
40145
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.panelLabel, children: GRAPH_FEATURE_LABEL }),
40146
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
40147
+ SwitchRow,
40148
+ {
40149
+ label: `${GRAPH_FEATURE_LABEL}投影`,
40150
+ desc: graphSwitchDesc(graphEnabled),
40151
+ checked: graphEnabled,
40152
+ disabled: !master,
40153
+ onChange: (v) => {
40154
+ if (v) {
40155
+ requestGraphOn();
40156
+ } else {
40157
+ toggle("graphEnabled", false);
40158
+ }
40159
+ }
40160
+ }
40161
+ ),
40103
40162
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: S.panelLabel, children: "工具封印" }),
40104
40163
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
40105
40164
  SwitchRow,
@@ -40115,7 +40174,7 @@ var __create = Object.create;
40115
40174
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
40116
40175
  SwitchRow,
40117
40176
  {
40118
- label: "图谱工具",
40177
+ label: GRAPH_TOOL_LABEL,
40119
40178
  desc: toolGraph ? "已注册:memory_search_graph / memory_expand_graph_node" : "已封印:图谱读工具从模型工具列表移除",
40120
40179
  checked: toolGraph,
40121
40180
  onChange: (v) => {
@@ -40271,7 +40330,53 @@ var __create = Object.create;
40271
40330
  })
40272
40331
  ] }),
40273
40332
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ArchiveSection, { rpc }),
40274
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: S.hint, children: "浏览各层记忆内容请切换上方 Tab;原始对话(L0)不入浏览器,可由模型侧 conversation_search 工具查询。" })
40333
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: S.hint, children: "浏览各层记忆内容请切换上方 Tab;原始对话(L0)不入浏览器,可由模型侧 conversation_search 工具查询。" }),
40334
+ graphConfirm ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
40335
+ NModal,
40336
+ {
40337
+ open: true,
40338
+ onClose: () => {
40339
+ setGraphConfirm(null);
40340
+ },
40341
+ title: `开启${GRAPH_FEATURE_LABEL}投影?`,
40342
+ footer: [
40343
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
40344
+ NButton,
40345
+ {
40346
+ onClick: () => {
40347
+ setGraphConfirm(null);
40348
+ },
40349
+ children: "取消"
40350
+ },
40351
+ "cancel"
40352
+ ),
40353
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
40354
+ NButton,
40355
+ {
40356
+ variant: "primary",
40357
+ onClick: () => {
40358
+ setGraphConfirm(null);
40359
+ toggle("graphEnabled", true);
40360
+ },
40361
+ children: "开启"
40362
+ },
40363
+ "confirm"
40364
+ )
40365
+ ],
40366
+ children: [
40367
+ graphConfirm.missing > 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
40368
+ "待投影记忆 ",
40369
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { children: graphConfirm.missing }),
40370
+ " 条,按每批 8 条约 ",
40371
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { children: Math.ceil(graphConfirm.missing / 8) }),
40372
+ " 个作业;每个作业一次 LLM 抽取(单批约 2 分钟),预计总计 ",
40373
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { children: Math.max(1, Math.round(Math.ceil(graphConfirm.missing / 8) * 2 / 60)) }),
40374
+ " 小时量级。"
40375
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: "当前没有待投影的记忆,开启后新蒸馏的记忆会陆续进图。" }),
40376
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { marginTop: 8 }, children: "投影在后台串行跑,不会阻断对话(新蒸馏的记录优先级恒高于存量补投影);进度可在「图谱」页看,随时可关开关停下来。" })
40377
+ ]
40378
+ }
40379
+ ) : null
40275
40380
  ] });
40276
40381
  }
40277
40382
 
@@ -42711,7 +42816,11 @@ var __create = Object.create;
42711
42816
  " ",
42712
42817
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(NButton, { onClick: loadBoard, children: "重试" })
42713
42818
  ] }) : null,
42714
- stats && stats.nodes === 0 && !error ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { style: S.intro, children: "图谱还没有内容。它在设置里是独立开关(概览 → 图谱投影),开启后存量记忆会逐批投影成实体与关系。" }) : null,
42819
+ stats && stats.nodes === 0 && !error ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { style: S.intro, children: [
42820
+ "图谱还没有内容。",
42821
+ graphEnableHint(),
42822
+ `(注意:这里指的是「${GRAPH_FEATURE_LABEL}」开关,不是「${GRAPH_TOOL_LABEL}」——后者只管模型能不能读图)`
42823
+ ] }) : null,
42715
42824
  board && board.seeds.length > 1 && view === "full" ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { ...S.flexRow, marginBottom: 8, flexWrap: "wrap", gap: 6 }, children: board.seeds.map((s) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
42716
42825
  "button",
42717
42826
  {
@@ -8,6 +8,10 @@
8
8
  * - 工具面 conversation_search → `op=search&redact=1`(命中基于原文 fts,
9
9
  * 返回 snippet 取脱敏锚定文本)。
10
10
  *
11
+ * 加上 issue #14-B 的**重建事实源**:`op=sessions` 枚举 + `op=session`(**不带 tail**)
12
+ * 逐会话拉全量——对全量重建来说“尾窗”不够用,而 collector 侧不带 tail 即 `ORDER BY seq ASC`
13
+ * 无 LIMIT。此路径同样恒带 `redact=1`。
14
+ *
11
15
  * 硬依赖纪律(对齐 T7.3 裁定):collector 缺席/超时/非 2xx/ok=false 一律抛
12
16
  * {@link DrscClientError}——**绝不回退本地 l0**(表已退役,无退路);调用方
13
17
  * 分口径处理:蒸馏背景窗空窗降级(runner),工具面显式报错(tools)。
@@ -55,6 +59,16 @@ export interface DrscSearchHit {
55
59
  on_surface: number;
56
60
  turn: number;
57
61
  }
62
+ /** `op=sessions` 的一个会话摘要行(分页枚举用;wire 蛇形直取)。 */
63
+ export interface DrscSessionSummary {
64
+ session_id: string;
65
+ title: string;
66
+ cwd: string;
67
+ event_count: number;
68
+ turn_count: number;
69
+ /** 末条消息时间(ms)—— collector 按它降序排,客户端用时注意反转。 */
70
+ last_time_ms: number;
71
+ }
58
72
  /** kind → L0 角色词表(memory 只收 user/assistant 两类,其余(tool/* 等)不映射)。 */
59
73
  export declare function drscRoleOf(kind: string): 'user' | 'assistant' | undefined;
60
74
  /**
@@ -88,6 +102,26 @@ export declare class DrscClient {
88
102
  * collector 无此会话(session:null)返回空数组(新会话首轮的常态,非错误)。
89
103
  */
90
104
  sessionTail(sessionId: string, tail: number): Promise<DrscSessionRow[]>;
105
+ /**
106
+ * **整会话**正文(issue #14-B 重建事实源):`op=session&session=<id>&redact=1`,
107
+ * **不带 `tail`** —— collector 侧 `tail` 未给时走 `ORDER BY s.seq ASC` 无 LIMIT,即全量。
108
+ *
109
+ * `redact=1` 是**硬要求**,不是可选项:collector 的 `session` 缺省是原文臂,
110
+ * 而 memory 捕获侧是写前单点脱敏;重建若走原文臂,产出的 L1 会比正常蒸馏「更脏」,
111
+ * 而且会经 md 投射落盘。
112
+ */
113
+ sessionAll(sessionId: string): Promise<DrscSessionRow[]>;
114
+ /**
115
+ * 会话枚举:`op=sessions&offset&limit`(collector 侧 limit 钳到 1..500)。
116
+ *
117
+ * 重建的事实源没有“全量导出”单口(也不应该有:一个响应装不下几万条正文),
118
+ * 所以走「枚举会话 → 逐会话拉全量」的 N+1 组合。采集端分页是 0-SQL 的内存切片,
119
+ * 代价仅在于请求数。
120
+ */
121
+ listSessions(offset: number, limit: number): Promise<{
122
+ total: number;
123
+ rows: DrscSessionSummary[];
124
+ }>;
91
125
  /** 原史检索:`op=search&q=<query>&limit=N&redact=1` → 脱敏 snippet 命中。 */
92
126
  search(query: string, limit: number): Promise<DrscSearchHit[]>;
93
127
  }
@@ -51,6 +51,26 @@ export function drscRowToConversationMessage(row, sessionId) {
51
51
  }
52
52
  return msg;
53
53
  }
54
+ /**
55
+ * `op=session` 包络 → 行数组。
56
+ *
57
+ * `session:null` = collector 没有这个会话 —— 新会话首轮的常态,**不是错误**
58
+ * (“没有内容”与“没拿到”必须分开:前者空窗降级,后者抛 DrscClientError)。
59
+ */
60
+ function parseSessionRows(body) {
61
+ if (body['session'] === null || body['session'] === undefined)
62
+ return [];
63
+ const rows = Array.isArray(body['rows']) ? body['rows'] : [];
64
+ return rows.map((r) => ({
65
+ seq: Number(r['seq'] ?? 0),
66
+ turn: Number(r['turn'] ?? 0),
67
+ step: Number(r['step'] ?? 0),
68
+ kind: String(r['kind'] ?? ''),
69
+ role: String(r['role'] ?? ''),
70
+ time: Number(r['time'] ?? 0),
71
+ text: String(r['text'] ?? ''),
72
+ }));
73
+ }
54
74
  export class DrscClient {
55
75
  options;
56
76
  fetchImpl;
@@ -108,18 +128,43 @@ export class DrscClient {
108
128
  */
109
129
  async sessionTail(sessionId, tail) {
110
130
  const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&tail=${Math.max(0, Math.floor(tail))}&redact=1`);
111
- if (body['session'] === null || body['session'] === undefined)
112
- return [];
131
+ return parseSessionRows(body);
132
+ }
133
+ /**
134
+ * **整会话**正文(issue #14-B 重建事实源):`op=session&session=<id>&redact=1`,
135
+ * **不带 `tail`** —— collector 侧 `tail` 未给时走 `ORDER BY s.seq ASC` 无 LIMIT,即全量。
136
+ *
137
+ * `redact=1` 是**硬要求**,不是可选项:collector 的 `session` 缺省是原文臂,
138
+ * 而 memory 捕获侧是写前单点脱敏;重建若走原文臂,产出的 L1 会比正常蒸馏「更脏」,
139
+ * 而且会经 md 投射落盘。
140
+ */
141
+ async sessionAll(sessionId) {
142
+ const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&redact=1`);
143
+ return parseSessionRows(body);
144
+ }
145
+ /**
146
+ * 会话枚举:`op=sessions&offset&limit`(collector 侧 limit 钳到 1..500)。
147
+ *
148
+ * 重建的事实源没有“全量导出”单口(也不应该有:一个响应装不下几万条正文),
149
+ * 所以走「枚举会话 → 逐会话拉全量」的 N+1 组合。采集端分页是 0-SQL 的内存切片,
150
+ * 代价仅在于请求数。
151
+ */
152
+ async listSessions(offset, limit) {
153
+ const from = Math.max(0, Math.floor(offset));
154
+ const size = Math.max(1, Math.min(500, Math.floor(limit)));
155
+ const body = await this.getJson(`?op=sessions&offset=${from}&limit=${size}`);
113
156
  const rows = Array.isArray(body['rows']) ? body['rows'] : [];
114
- return rows.map((r) => ({
115
- seq: Number(r['seq'] ?? 0),
116
- turn: Number(r['turn'] ?? 0),
117
- step: Number(r['step'] ?? 0),
118
- kind: String(r['kind'] ?? ''),
119
- role: String(r['role'] ?? ''),
120
- time: Number(r['time'] ?? 0),
121
- text: String(r['text'] ?? ''),
122
- }));
157
+ return {
158
+ total: Number(body['total'] ?? rows.length),
159
+ rows: rows.map((r) => ({
160
+ session_id: String(r['session_id'] ?? ''),
161
+ title: String(r['title'] ?? ''),
162
+ cwd: String(r['cwd'] ?? ''),
163
+ event_count: Number(r['event_count'] ?? 0),
164
+ turn_count: Number(r['turn_count'] ?? 0),
165
+ last_time_ms: Number(r['last_time_ms'] ?? 0),
166
+ })),
167
+ };
123
168
  }
124
169
  /** 原史检索:`op=search&q=<query>&limit=N&redact=1` → 脱敏 snippet 命中。 */
125
170
  async search(query, limit) {
package/dist/config.d.ts CHANGED
@@ -117,6 +117,15 @@ export interface MemoryConfig {
117
117
  /** 单源扫描记录上限(0 = 不限;仍受总护栏)。 */
118
118
  scanMaxRecords: number;
119
119
  };
120
+ /**
121
+ * 重建事实源(issue #14-B)。
122
+ *
123
+ * - `local`(默认):本地 `l0_conversations` —— 已停写的冻结存量;
124
+ * - `drsc`(实验性):`dsh-reference-session-collector` 的投影,枚举会话后逐会话拉全量。
125
+ */
126
+ rebuild: {
127
+ factSource: 'local' | 'drsc';
128
+ };
120
129
  /** 轨2 技能结晶(程序性记忆)。**默认关**:allowPromote 也默认关(上线先只计数不晋升,
121
130
  * distinct-session 达阈后才开);injectionTier 不用 union,消费侧归一。 */
122
131
  skills: {
@@ -193,7 +202,7 @@ export interface MemoryConfig {
193
202
  */
194
203
  workspaceSource: {
195
204
  enabled: boolean;
196
- /** 路由表:按数组序取首个命中——dsh 侧在前,故双项目并存时归 dsh(用户口径)。 */
205
+ /** 路由表:按数组序取首个命中;**默认为空**,需按自己的项目自行配置(issue #13)。 */
197
206
  routes: WorkspaceRoute[];
198
207
  /** 存量 re-link 的单次护栏。 */
199
208
  maxRecords: number;
package/dist/config.js CHANGED
@@ -82,6 +82,17 @@ export function liveSettingsSchema() {
82
82
  conflictFreeze: Schema.boolean().default(false),
83
83
  // 轨2 技能结晶总开关(看板可拨):默认 false。开=注册 memory_skill_* 工具 + 注入 active 技能 + skills-* 端点可用。
84
84
  skillsEnabled: Schema.boolean().default(false),
85
+ /**
86
+ * 知识图谱**投影**开关(issue #16,概览区可拨)。默认 false。
87
+ *
88
+ * 与部署 `graph.enabled` **取或**(照 keywordEnabled / validityTiering 先例):
89
+ * 部署可强制开,面板只能追加开。
90
+ *
91
+ * 为什么它必须在 live:投影是**写侧派生**,关了只是不入队、不丢数据;
92
+ * 而它的入口以前只住 YAML,用户根本找不到(面板上那个“图谱投影”其实是蒸馏预算的
93
+ * 分层标签,点它改的是 token 预算)。
94
+ */
95
+ graphEnabled: Schema.boolean().default(false),
85
96
  // 存量回填/进阶功能的启用开关(概览「存量回填与整形」区可拨):默认 false,
86
97
  // 与部署 cfg.<x>.enabled 取或(经 effectiveCfg)——部署可强制开,live 只能追加开。
87
98
  // 存在的理由:这些面都有对应按钮/端点,若开关只住 YAML,用户点了只会报“未开启”却没处翻。
@@ -184,6 +195,16 @@ export const memorySchema = Schema.object({
184
195
  // 单源扫描上限(防极端大包在事件循环里阻塞 RPC;scan 是手动触发,仍留护栏)。
185
196
  scanMaxRecords: Schema.number().min(0).max(20_000).default(5000),
186
197
  }),
198
+ // 重建事实源(issue #14-B):默认 local(行为与改前一致);drsc 需显式选,
199
+ // 且 collector 缺席时**报错中止**(不清库)——不静默回退本地,避免“以为用了新源其实还是旧表”。
200
+ rebuild: Schema.object({
201
+ /**
202
+ * 'local' = 本地 l0_conversations;'drsc' = reference-session-collector 投影。
203
+ *
204
+ * drsc 路径恒带 `redact=1`(collector 的 session 缺省是原文臂,而 capture 是写前脱敏)。
205
+ */
206
+ factSource: Schema.union(['local', 'drsc']).default('local'),
207
+ }),
187
208
  // 轨2 技能结晶:默认关 + allowPromote 关(先只计数不晋升)。
188
209
  skills: Schema.object({
189
210
  enabled: Schema.boolean().default(false),
@@ -210,7 +231,19 @@ export const memorySchema = Schema.object({
210
231
  }),
211
232
  /**
212
233
  * 内容路由:默认**开**(用户口径:谈到本仓/dsh 生态的记忆要归到本仓 workspace,
213
- * 而不是全托底到 `<dataDir>/memories`)。默认路由表就是用户给的两条归类规则(路径可改,不需改码)。
234
+ * 而不是全托底到 `<dataDir>/memories`)。
235
+ *
236
+ * ⚠️ **默认路由表为空**(issue #13)。此前默认值写死了作者本机的 `E:\test\rewrite-agently`
237
+ * 与 `E:\test\agent-no-dsh`:装好即生效,凡正文含 `dsh`/`cordis`/`prime-memory` 等词的记忆
238
+ * 都被写上了别人机器上的路径。别人的机器没这个盘,于是
239
+ * - `targetFor` 的护栏判定目录不存在 → 投射落点静默退回单根(日志一条 warn 而已);
240
+ * - 更严重的是 `scope: workspace` 时这些记录在**真实工作区**里按 workspace 过滤不可见,
241
+ * 表现为"记忆莫名其妙丢了",而根源是默认值指向别人的机器。
242
+ *
243
+ * 默认值属于**可移植性契约**:不得含任何具体主机的绝对路径。要么由用户显式配置路由、
244
+ * 要么用 `$workspace` 之类的占位(由调用方在运行期替换为当前会话工作区),
245
+ * 要么不路由(`routes: []` → 全部落单根,行为与 `enabled:false` 等价但语义更诚实)。
246
+ * 想恢复旧的按内容归类,请在配置里自行写下自己的两条规则。
214
247
  */
215
248
  workspaceSource: Schema.object({
216
249
  enabled: Schema.boolean().default(true),
@@ -219,31 +252,17 @@ export const memorySchema = Schema.object({
219
252
  * 归到哪个 workspace(**workspace 根**,绝对路径;写入前过 normalizeWorkspacePath)。
220
253
  *
221
254
  * 存的不包含 `.agents/memory`:投射落点由 `targetFor` 算,它会在本值后拼上
222
- * `WORKSPACE_SUBDIR = ['.agents','memory']`。旧默认值把它写成了
255
+ * `WORKSPACE_SUBDIR = ['.agents','memory']`。旧默认值曾把它写成
223
256
  * `E:\test\rewrite-agently\.agents`,于是落点变成 `.agents\.agents\memory\`——
224
257
  * 而 `.agents` 目录确实存在,"目录不存在则退回单根"的护栏救不了这种形状,
225
- * 等于在仓里挂一棵没人认领的孤儿树。新增用例钉住这个不变式。
258
+ * 等于在仓里挂一棵没人认领的孤儿树。有用例钉住这个不变式。
226
259
  */
227
260
  path: Schema.string().default(''),
228
261
  /** 命中任一关键词即归此路(小写子串匹配,已 NFKC 归一)。 */
229
262
  anyOf: Schema.array(Schema.string()).default([]),
230
263
  /** 出现任一排除词则此路不命中(用于「无 dsh 才算非 dsh 项目」)。 */
231
264
  noneOf: Schema.array(Schema.string()).default([]),
232
- })).default([
233
- {
234
- path: 'E:\\test\\rewrite-agently',
235
- // 'dsh' 子串已覆盖 mine-dsh-plugins 下所有插件名;把目录名单列出来是因为
236
- // 有些记忆只写了"某个插件"而没写 dsh- 前缀,那时这一项才顶用。
237
- anyOf: ['dsh', 'mine-dsh-plugins', 'cordis', 'prime-memory', 'brief-sidebar', 'host farm', 'inferglow-dsh'],
238
- noneOf: [],
239
- },
240
- {
241
- path: 'E:\\test\\agent-no-dsh',
242
- anyOf: ['hanako', 'hanaagent', 'openhanako', 'inferglow', 'pi-agent'],
243
- // 同一段内容又谈到 dsh 时属于 dsh 生态(如 Hanako 逆向用于反推 dsh 机制),归 dsh 侧。
244
- noneOf: ['dsh'],
245
- },
246
- ]),
265
+ })).default([]),
247
266
  maxRecords: Schema.number().min(1).max(10_000).default(300),
248
267
  timeBudgetMs: Schema.number().min(1000).max(1_800_000).default(120_000),
249
268
  }),
@@ -111,6 +111,16 @@ export interface MemoryLiveSettings {
111
111
  /** 轨2 技能结晶总开关(看板可拨):true = 注册 memory_skill_* 工具、注入 active 技能、skills-* 端点可用。
112
112
  * 默认 false(新功能默认关);与部署 cfg.skills.enabled 取或(任一开则生效)。 */
113
113
  skillsEnabled: boolean;
114
+ /**
115
+ * 知识图谱**投影**开关(概览区可拨,issue #16)。
116
+ *
117
+ * true = 把 L1 记忆投影成 graph_nodes/graph_edges,并在启动/周期补齐存量;
118
+ * 与部署 `cfg.graph.enabled` **取或**(任一开则生效)。
119
+ *
120
+ * 注意与 `toolGraph` 的区别:后者封印的是**读**工具(memory_search_graph 等),
121
+ * 本键控的是**写**面(生成图谱)。两者同名接近,UI 文案必须分开写。
122
+ */
123
+ graphEnabled: boolean;
114
124
  /** 关键词回填启用开关(概览区可拨):true = 放行 dsh-memory/keyword-backfill。与部署 cfg.keyword.enabled 取或。 */
115
125
  keywordEnabled: boolean;
116
126
  /** 重蒸馏整形启用开关(概览区可拨):true = 放行 dsh-memory/redistill-backfill。与部署 cfg.redistill.enabled 取或。 */
@@ -185,6 +195,34 @@ export interface RebuildStatus {
185
195
  * null 表示尚未进入准备阶段。
186
196
  */
187
197
  preserveNote: string | null;
198
+ /**
199
+ * 本次重建**能够重蒸出来**的条数(来源全部落在 L0 快照内)。
200
+ *
201
+ * 与 `preserveCount` 一起构成"点下去会发生什么"的完整交代:
202
+ * 用户看得到多少条会被重造、多少条会被原样保住。
203
+ * null = 尚未出计划(未进入准备阶段)。
204
+ */
205
+ rederivableCount: number | null;
206
+ /** 本次重建**必须保留**的条数(无来源 / 来源不在快照 / 来源无从证明)。 */
207
+ preserveCount: number | null;
208
+ /**
209
+ * 事实源**覆盖到的时刻**(ISO;本地 L0 写停后即不再前移)。
210
+ *
211
+ * 这是给"这个按钮会不会倒退"提供可判依据的字段:重建只能重造到这个时刻
212
+ * 之前的正文。null = 本地 L0 一条正文都没有。
213
+ */
214
+ factWatermark: string | null;
215
+ }
216
+ /**
217
+ * 重建启动请求。
218
+ *
219
+ * `force` 只用于跨过**水位落差的二次确认**(见 `STALE_RATIO_THRESHOLD`):
220
+ * L0 写停后“记忆晚于水位”是常态,把它做成硬拒会把重建永久锁死;
221
+ * 因此这里只要求调用方显式确认一次。
222
+ */
223
+ export interface RebuildStartRequest {
224
+ /** true = 已确认“这批记录只保留、不重蒸”。 */
225
+ force?: boolean;
188
226
  }
189
227
  /** 反刍阶段。 */
190
228
  export type RuminatePhase = 'idle' | 'refreshing' | 'distilling' | 'consolidating' | 'updating' | 'relabeling' | 'done' | 'cancelled' | 'failed';
@@ -856,6 +894,8 @@ export interface SettingsSetRequest {
856
894
  memoryMutate?: boolean;
857
895
  /** §C 人工冲突裁决总开关(true = 冻结冲突对,停放到待人工裁决区)。 */
858
896
  conflictFreeze?: boolean;
897
+ /** 知识图谱投影开关(issue #16;与部署 cfg.graph.enabled 取或)。 */
898
+ graphEnabled?: boolean;
859
899
  }
860
900
  export interface SettingsSetResponse {
861
901
  ok: true;
@@ -1286,6 +1326,13 @@ export interface GraphPreviewResponse {
1286
1326
  jobsTotal: number;
1287
1327
  /** 永久失败作业数(dead),可选(零漂移先例);>0 → UI 拆成"完成/失败/排队"三档显示。 */
1288
1328
  deadJobs?: number;
1329
+ /**
1330
+ * 尚未投影的 L1 记录条数(issue #16),可选:为 0 时不写键。
1331
+ *
1332
+ * 首次开启投影前用它算“大概要花多少”:条数 ÷ 每批 8 条 = job 数,
1333
+ * 每个 job 一次 LLM 抽取。
1334
+ */
1335
+ missing?: number;
1289
1336
  };
1290
1337
  /** mode==='full' 才出现;全图星图轻量块(可见节点×active 边,截断标记 truncated)。
1291
1338
  * 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。 */
@@ -1868,7 +1915,7 @@ export interface DshMemoryRequestMap {
1868
1915
  'dsh-memory/log-tail': LogTailRequest;
1869
1916
  'dsh-memory/trace-tail': TraceTailRequest;
1870
1917
  'dsh-memory/rebuild-status': Record<string, never>;
1871
- 'dsh-memory/rebuild-start': Record<string, never>;
1918
+ 'dsh-memory/rebuild-start': RebuildStartRequest;
1872
1919
  'dsh-memory/rebuild-cancel': Record<string, never>;
1873
1920
  'dsh-memory/ruminate-status': Record<string, never>;
1874
1921
  'dsh-memory/ruminate-start': Record<string, never>;