dsh-prime-memory 0.26.10 → 0.26.12

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 (55) 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 +38 -0
  9. package/CHANGELOG.ru.md +1640 -0
  10. package/INSTALL.de.md +92 -0
  11. package/INSTALL.es.md +92 -0
  12. package/INSTALL.fr.md +92 -0
  13. package/INSTALL.it.md +92 -0
  14. package/INSTALL.ru.md +92 -0
  15. package/README.de.md +1 -1
  16. package/README.en.md +3 -3
  17. package/README.es.md +1 -1
  18. package/README.fr.md +1 -1
  19. package/README.it.md +1 -1
  20. package/README.ja.md +1 -1
  21. package/README.ko.md +1 -1
  22. package/README.md +10 -3
  23. package/README.ru.md +1 -1
  24. package/dist/bench-control.js +3 -1
  25. package/dist/client.js +121 -12
  26. package/dist/clients/drsc.d.ts +34 -0
  27. package/dist/clients/drsc.js +56 -11
  28. package/dist/config.d.ts +10 -1
  29. package/dist/config.js +37 -18
  30. package/dist/contract.d.ts +48 -1
  31. package/dist/graph/gate.d.ts +17 -0
  32. package/dist/graph/gate.js +11 -0
  33. package/dist/graph/preview.d.ts +5 -0
  34. package/dist/graph/preview.js +2 -0
  35. package/dist/index.js +1 -1
  36. package/dist/pipeline/fact-source.d.ts +127 -0
  37. package/dist/pipeline/fact-source.js +192 -0
  38. package/dist/pipeline/rebuild-preserve.d.ts +31 -2
  39. package/dist/pipeline/rebuild-preserve.js +40 -14
  40. package/dist/pipeline/rebuild.d.ts +47 -13
  41. package/dist/pipeline/rebuild.js +153 -57
  42. package/dist/pipeline/runner.d.ts +4 -1
  43. package/dist/pipeline/runner.js +58 -21
  44. package/dist/pipeline/workspace-relink.js +7 -2
  45. package/dist/room-review.d.ts +1 -1
  46. package/dist/room-review.js +10 -5
  47. package/dist/settings.js +4 -0
  48. package/dist/stats.js +10 -3
  49. package/dist/store/graph-store.d.ts +20 -0
  50. package/dist/store/graph-store.js +50 -1
  51. package/dist/store/sqlite.d.ts +42 -0
  52. package/dist/store/sqlite.js +98 -0
  53. package/dist/tools/index.js +4 -3
  54. package/dsh.plugin.json +2 -2
  55. package/package.json +37 -21
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>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 知识图谱投影的**唯一开关判据**(issue #16)。
3
+ *
4
+ * 为什么单独一个模块而不是塞进 runner:
5
+ * - `runner` 的五处门(启动补投影 / 周期泵 / enqueue / reproject / maybeQueue)与
6
+ * `tools` 的写面门(memory_reproject / memory_migrate_relation)必须用**同一个**
7
+ * 判据。判据分散 = 半开态:只改 enqueue 不改泵 → 队列只涨不取;只改泵不改启动 timer
8
+ * → 运行时开启后存量永不进图;只改 runner 不改工具 → 工具说"未开启"、runner 却在跑。
9
+ * - `tools` 不该为此拉入整个 `pipeline/runner` 的依赖图。
10
+ *
11
+ * 语义:部署 `cfg.graph.enabled` **或** 面板 `live.graphEnabled`(取或,照
12
+ * keywordEnabled / validityTiering 先例)。部署可以硬开、面板只能追加开 ——
13
+ * 反过来(取与)会让面板开关变成"只能关不能开",而它本来就只能关到部署默认。
14
+ */
15
+ import type { MemoryConfig } from '../config.js';
16
+ import type { LiveSettingsHandle } from '../settings.js';
17
+ export declare function graphFeatureOn(cfg: MemoryConfig, live?: LiveSettingsHandle): boolean;
@@ -0,0 +1,11 @@
1
+ export function graphFeatureOn(cfg, live) {
2
+ if (cfg.graph?.enabled === true)
3
+ return true;
4
+ try {
5
+ return live?.get()?.graphEnabled === true;
6
+ }
7
+ catch {
8
+ // live 读取失败:不能把已开的部署开关抬下来,也不能把功能卡死 → 按未开处理(零行为变化)。
9
+ return false;
10
+ }
11
+ }
@@ -46,6 +46,11 @@ export interface PreviewOpts {
46
46
  pendingJobs?: number;
47
47
  jobsTotal?: number;
48
48
  deadJobs?: number;
49
+ /**
50
+ * 尚未投影的 L1 条数(issue #16):面板“首次开启投影大概要花多少”的分母。
51
+ * 与 `queueMissing` 同一 WHERE,见 `GraphStore.countMissing`。
52
+ */
53
+ missing?: number;
49
54
  }
50
55
  /** S-2:邻接统计——active 边且两端都可见才计度(双向)。 */
51
56
  export interface DegreeMaps {
@@ -101,6 +101,8 @@ export function buildGraphPreview(nodes, edges, opts = {}) {
101
101
  pendingJobs: opts.pendingJobs ?? 0,
102
102
  jobsTotal: opts.jobsTotal ?? 0,
103
103
  ...(opts.deadJobs != null && opts.deadJobs > 0 ? { deadJobs: opts.deadJobs } : {}),
104
+ // 零漂移:为 0 时不写键(旧消费方拿到的形状与改前逐字一致)。
105
+ ...(opts.missing != null && opts.missing > 0 ? { missing: opts.missing } : {}),
104
106
  };
105
107
  const seedLimit = Math.min(Math.max(Math.floor(opts.seedLimit ?? GRAPH_PREVIEW_SEEDS), 1), 20);
106
108
  const neighborLimit = Math.min(Math.max(Math.floor(opts.neighborLimit ?? GRAPH_PREVIEW_NEIGHBORS), 1), 40);
package/dist/index.js CHANGED
@@ -399,7 +399,7 @@ export async function apply(ctx, config) {
399
399
  modes.setModeChangeHandler((sessionId, oldMode, newMode) => runner.onModeChange(sessionId, oldMode, newMode));
400
400
  // 闲置兜底:静默达标会话的未蒸馏切片自动落袋(idleSeconds=0 关闭)
401
401
  runner.startIdleTimer();
402
- // 图谱周期补投影(cfg.graph.enabled 门控;30 分钟一次低速收敛)
402
+ // 图谱周期补投影(开关在 tick 内判:部署 graph.enabled 或面板 live.graphEnabled;30 分钟一次低速收敛)
403
403
  runner.startGraphBackfillTimer();
404
404
  // 时效标注存量回填(expiry-tiering task_6.3:tieringEnabled 门控,低频、让位蒸馏)
405
405
  runner.startValidityBackfillTimer();