dsh-prime-memory 0.14.0-beta.3 → 0.14.0-beta.4

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.
@@ -47,3 +47,18 @@ export declare function withSourceAnchors(metadata: Record<string, unknown> | un
47
47
  * 宁可报告"无锚点",也不把半截坐标喂给下游的证据读取器。
48
48
  */
49
49
  export declare function readSourceAnchors(metadata: unknown): ConversationAnchor[] | undefined;
50
+ /**
51
+ * 锚点的人类可读标签:**UI 契约(`UiRecord.sourceAnchors`)要的字符串形态**。
52
+ *
53
+ * 格式即 `t<turn>` / `t<turn> s<step>`(与 `reconcile.ts` 的证据行同一写法,
54
+ * 见 contract.ts 对 `sourceAnchors` 的说明)。`step` 缺失就**不写**——
55
+ * 不得用 0 或相邻事件的 step 补齐(红线 2:坐标永不推算)。
56
+ *
57
+ * 刻意**不带 sessionId**:该字段用于跨会话归并排序,而 UI 一行里只有
58
+ * "这条记忆出在会话内哪个位置",带上 id 反而把可用信息挤掉。
59
+ */
60
+ export declare function anchorLabel(anchor: ConversationAnchor): string;
61
+ /** 读侧一步到位:`l1_records.metadata_json` → UI 契约的标签数组。
62
+ * 无锚点返回**空数组**(`UiRecord.sourceAnchors` 的契约语义是"空数组 = 无锚点",
63
+ * 与 `readSourceAnchors` 的 `undefined` 区分开,故此处完成转换)。 */
64
+ export declare function sourceAnchorLabels(metadata: unknown): string[];
@@ -88,3 +88,22 @@ export function readSourceAnchors(metadata) {
88
88
  }
89
89
  return out.length > 0 ? out : undefined;
90
90
  }
91
+ /**
92
+ * 锚点的人类可读标签:**UI 契约(`UiRecord.sourceAnchors`)要的字符串形态**。
93
+ *
94
+ * 格式即 `t<turn>` / `t<turn> s<step>`(与 `reconcile.ts` 的证据行同一写法,
95
+ * 见 contract.ts 对 `sourceAnchors` 的说明)。`step` 缺失就**不写**——
96
+ * 不得用 0 或相邻事件的 step 补齐(红线 2:坐标永不推算)。
97
+ *
98
+ * 刻意**不带 sessionId**:该字段用于跨会话归并排序,而 UI 一行里只有
99
+ * "这条记忆出在会话内哪个位置",带上 id 反而把可用信息挤掉。
100
+ */
101
+ export function anchorLabel(anchor) {
102
+ return anchor.step === undefined ? `t${anchor.turn}` : `t${anchor.turn} s${anchor.step}`;
103
+ }
104
+ /** 读侧一步到位:`l1_records.metadata_json` → UI 契约的标签数组。
105
+ * 无锚点返回**空数组**(`UiRecord.sourceAnchors` 的契约语义是"空数组 = 无锚点",
106
+ * 与 `readSourceAnchors` 的 `undefined` 区分开,故此处完成转换)。 */
107
+ export function sourceAnchorLabels(metadata) {
108
+ return (readSourceAnchors(metadata) ?? []).map(anchorLabel);
109
+ }
package/dist/stats.js CHANGED
@@ -21,8 +21,7 @@ import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChai
21
21
  import { projectDistillChain, validateDistillChain } from './settings.js';
22
22
  import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
23
23
  import { resolveConflictPair, listConflictPairs } from './conflict-service.js';
24
- // R7:读回锚点走 anchors.ts 的**唯一入口**(形状校验从严),不在 UI 层自行解析 metadata。
25
- import { readSourceAnchors } from './pipeline/anchors.js';
24
+ import { sourceAnchorLabels } from './pipeline/anchors.js';
26
25
  import { errDetail } from './util/filelog.js';
27
26
  import { snapshotTokenCost } from './token-cost.js';
28
27
  const require = createRequire(import.meta.url);
@@ -491,6 +490,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
491
490
  distillMode: '', directBaseURL: '', directApiKey: '',
492
491
  embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
493
492
  memoryMutate: false,
493
+ conflictFreeze: live?.get()?.conflictFreeze === true,
494
494
  }),
495
495
  // 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
496
496
  ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
@@ -528,8 +528,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
528
528
  throw new Error('开关通道未初始化');
529
529
  const patch = (payload ?? {});
530
530
  const clean = {};
531
- // 布尔开关组:memoryMutate(高权限写删门)与主开关同列
532
- for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate']) {
531
+ // 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
532
+ for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze']) {
533
533
  if (typeof patch[key] === 'boolean')
534
534
  clean[key] = patch[key];
535
535
  }
@@ -729,11 +729,17 @@ export async function handleEndpoint(endpoint, payload, deps) {
729
729
  // 未开启冻结时同样走返回体(enabled:false + notice)而非抛错,理由同上。
730
730
  case 'dsh-memory/conflicts': {
731
731
  const p = (payload ?? {});
732
- return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
732
+ // 冻结开关只有**一个**事实源:与去重管线同一套 effectiveCfg 解析
733
+ // (live.conflictFreeze 覆盖静态 cfg.conflictFreeze.enabled)。此前这里直接读
734
+ // cfg.conflictFreeze.enabled —— 面板开关写的是 live,而部署静态值恒 false,
735
+ // 于是开关已开、settings.yaml 已落 true,本页仍报"矛盾冻结未开启"。
736
+ return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
733
737
  }
734
738
  // ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
735
739
  // 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
736
740
  // 部署状态,故它走返回体(带 notice)而非抛错;pair_id/outcome 缺参才抛。
741
+ // 开关判定与上面 conflicts 同源(effectiveCfg):读端与写端必须看同一份状态,
742
+ // 否则会出现"列表说开着、裁决说没开"的自相矛盾。
737
743
  case 'dsh-memory/conflict-resolve': {
738
744
  const p = (payload ?? {});
739
745
  const pairId = typeof p.pairId === 'string' ? p.pairId.trim() : '';
@@ -742,7 +748,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
742
748
  throw new Error('需要 pairId(待裁决对的 pair_id)');
743
749
  if (!outcome)
744
750
  throw new Error('需要 outcome(winner | loser | both)');
745
- return await resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, pairId, outcome);
751
+ return await resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, pairId, outcome);
746
752
  }
747
753
  case 'dsh-memory/records-delete': {
748
754
  // 面板高权限删除指定记忆;写入删权限门(memoryMutate)防御
@@ -1058,11 +1064,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
1058
1064
  return { cancelled: embedManager.cancelRuntimeInstall() };
1059
1065
  }
1060
1066
  case 'dsh-memory/embedding-reindex': {
1067
+ // 手动触发重建(契约:`EmbeddingReindexStartResponse`,受理即返回,进度照旧轮询
1068
+ // embedding-state-get 的 reindex 字段)。此前只登记了 -cancel:start 既不在白名单
1069
+ // 也没有 case,于是 startReindex() 成了够不着的死代码,端点在面板上静默 404
1070
+ // (契约门禁 tests/contract-keys.test.ts 早已为此标红)。
1071
+ // 拒绝语义交给 startReindex 自己抛(已卸载/在跑/切源占锁/源未就绪),不在端点层复述。
1061
1072
  if (!embedManager)
1062
- throw new Error('嵌入管理器未初始化');
1063
- // 全部门槛判定收在 startReindex 里(含"未就绪时 reindex 会静默 0/0/0"这条),
1064
- // 此处不重复实现一遍——两处判定必然漂移,而这里漂移的后果是谎报成功。
1065
- return embedManager.startReindex();
1073
+ throw new Error('嵌入管理器未初始化(存储不可用)');
1074
+ const r = embedManager.startReindex();
1075
+ deps.logger.info('[memory] 收到嵌入重建指令(设置页按钮)');
1076
+ return r;
1066
1077
  }
1067
1078
  case 'dsh-memory/embedding-reindex-cancel': {
1068
1079
  if (!embedManager)
@@ -1087,19 +1098,14 @@ function hitToUiRecord(r) {
1087
1098
  createdAt: r.createdAt ? new Date(r.createdAt).toISOString() : null,
1088
1099
  updatedAt: r.updatedAt ? new Date(r.updatedAt).toISOString() : null,
1089
1100
  version: r.version ?? 0,
1090
- sourceAnchors: (readSourceAnchors(r.metadata) ?? []).map(formatAnchor),
1101
+ // R7 来源锚点:契约字段是**标签数组**(`t12 s3`),由 metadata 的保留键读出。
1102
+ // 此前这里仍在填已废弃的 `sourceMessageIds`——`l1_records` 从不存那一列,
1103
+ // 该字段恒为 `[]`(死字段),而契约早已换成 `sourceAnchors`,于是
1104
+ // `sourceAnchors` 永远缺失、来源行在 UI 上从未显示过。
1105
+ sourceAnchors: sourceAnchorLabels(r.metadata),
1091
1106
  score: r.score ?? null,
1092
1107
  };
1093
1108
  }
1094
- /**
1095
- * 锚点的展示形态:`t12 s3` / 无 step 时 `t12`。
1096
- *
1097
- * 只做**可读化**,不携带 sessionId——单条记录的来源会话由记录自身语义决定,
1098
- * 把 sessionId 塞进这一行会把 12 个字符的坐标变成 40 个字符。
1099
- */
1100
- function formatAnchor(a) {
1101
- return typeof a.step === 'number' ? `t${a.turn} s${a.step}` : `t${a.turn}`;
1102
- }
1103
1109
  /**
1104
1110
  * 从文件尾反向分块读取最后 N 行:不整读全文件(轮转上限 2MB,整读会
1105
1111
  * 阻塞事件循环数毫秒)。原始 Buffer 拼接后再解码——分块边界可能切在
@@ -863,7 +863,7 @@ ruminate) {
863
863
  if (family === null) {
864
864
  return { enabled: false, total: 0, items: [], notice: blockNoticeOf(exec) };
865
865
  }
866
- return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, { limit: typeof args.limit === 'number' ? args.limit : undefined });
866
+ return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: live.get().conflictFreeze === true }, { limit: typeof args.limit === 'number' ? args.limit : undefined });
867
867
  },
868
868
  }));
869
869
  // ── memory_resolve_conflict: §C 矛盾冻结的人工裁决出口 ──
@@ -901,7 +901,7 @@ ruminate) {
901
901
  const outcome = typeof args.outcome === 'string' ? args.outcome.trim() : '';
902
902
  if (!pairId)
903
903
  return { ...empty, notice: '需要 pair_id:待裁决对没有"全部裁决"这种用法。' };
904
- return resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, pairId, outcome);
904
+ return resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: live.get().conflictFreeze === true }, pairId, outcome);
905
905
  },
906
906
  }));
907
907
  logger.info('[memory] 工具已注册: memory_search / conversation_search / memory_read_scene / memory_receipts / memory_search_graph / memory_expand_graph_node,及高权限 memory_add/memory_import/memory_delete / memory_resolve_conflict / memory_ruminate / memory_ruminate_cancel / memory_ruminate_status');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.14.0-beta.3",
3
+ "version": "0.14.0-beta.4",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",