dsh-prime-memory 0.12.0 → 0.13.0

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.
@@ -9,16 +9,8 @@
9
9
  * (与 `memory_receipts` 一样:同一份 `ReceiptsView` 供工具与端点共用)。
10
10
  */
11
11
  import type { L1Store } from './store/l1.js';
12
- /** 裁决结果的对外形状(snake_case,工具与端点共用)。 */
13
- export interface ConflictResolutionView {
14
- pair_id: string;
15
- outcome: string;
16
- /** 裁决时刻(ISO)。空串 = 未生效。 */
17
- resolved_at: string;
18
- /** 因裁决从检索中退场的记录 id(无则空串)。 */
19
- removed_record_id: string;
20
- notice?: string;
21
- }
12
+ import type { ConflictPairView, ConflictsResponse as ConflictsView, ConflictResolveResponse as ConflictResolutionView } from './contract.js';
13
+ export type { ConflictPairView, ConflictsView, ConflictResolutionView };
22
14
  export interface ConflictResolveDeps {
23
15
  l1: Pick<L1Store, 'listConflictPending' | 'resolveConflictPending' | 'deleteBatch' | 'syncGraphDisputed'>;
24
16
  /** `conflictFreeze.enabled`。未开启时队列恒空,直接给出提示而非静默无操作。 */
@@ -36,3 +28,28 @@ export interface ConflictResolveDeps {
36
28
  export declare function resolveConflictPair(deps: ConflictResolveDeps, pairId: string, outcome: string): Promise<ConflictResolutionView>;
37
29
  /** 裁决结果的人类可读渲染(工具路径用)。schema 产出的是可选字段,故按部分取值渲染。 */
38
30
  export declare function renderConflictResolution(v: Partial<ConflictResolutionView>): string;
31
+ /** 一条待裁决对的对外形状见 `contract.ts` 的 `ConflictPairView`(此处只引用)。 */
32
+ /** 队列读取的上限(与 `records-delete` 同量级:够人看,不把页面拖死)。 */
33
+ export declare const CONFLICT_LIST_LIMIT_MAX = 200;
34
+ /** 默认取多少条。 */
35
+ export declare const CONFLICT_LIST_LIMIT_DEFAULT = 50;
36
+ export interface ConflictListDeps {
37
+ l1: Pick<L1Store, 'listConflictPending' | 'countConflictPendingUnresolved' | 'getByIds'>;
38
+ /** `conflictFreeze.enabled`。 */
39
+ conflictFreezeEnabled: boolean;
40
+ }
41
+ /**
42
+ * 列出待裁决对。
43
+ *
44
+ * **正文必须带上**:人工裁决的对象就是"这两条到底说了什么",只给 id 等于让人盲判。
45
+ * 取不到正文时留空串 —— 面板据此区分"记录已不在检索库"与"内容为空",
46
+ * 而不是拿一句"(无内容)"把两种情形糊在一起。
47
+ *
48
+ * 未开启冻结时返回 `enabled:false` + 空列表 + `notice`,**不抛错**:开关没开是
49
+ * 部署状态,不是调用错误(与 `resolveConflictPair` 对同一情形的处理一致)。
50
+ */
51
+ export declare function listConflictPairs(deps: ConflictListDeps, opts?: {
52
+ limit?: number;
53
+ }): ConflictsView;
54
+ /** 列表结果的人类可读渲染(工具路径用)。 */
55
+ export declare function renderConflicts(v: ConflictsView): string;
@@ -63,3 +63,73 @@ export function renderConflictResolution(v) {
63
63
  : '\n未移除任何记录。';
64
64
  return `已裁决待裁决对 ${v.pair_id ?? ''}\n结论:${outcome}(${label})\n裁决时刻:${v.resolved_at ?? ''}${removed}`;
65
65
  }
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // 读方向:列出待裁决对
68
+ //
69
+ // 与 `resolveConflictPair` 同理由共用本模块:工具(`memory_conflicts`)与
70
+ // RPC 端点(`dsh-memory/conflicts`)必须是**同一份形状** —— 面板与模型看同一队列,
71
+ // 否则"人看到的那条"和"模型能裁决的那条"会对不上。
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+ /** 一条待裁决对的对外形状见 `contract.ts` 的 `ConflictPairView`(此处只引用)。 */
74
+ /** 队列读取的上限(与 `records-delete` 同量级:够人看,不把页面拖死)。 */
75
+ export const CONFLICT_LIST_LIMIT_MAX = 200;
76
+ /** 默认取多少条。 */
77
+ export const CONFLICT_LIST_LIMIT_DEFAULT = 50;
78
+ /**
79
+ * 列出待裁决对。
80
+ *
81
+ * **正文必须带上**:人工裁决的对象就是"这两条到底说了什么",只给 id 等于让人盲判。
82
+ * 取不到正文时留空串 —— 面板据此区分"记录已不在检索库"与"内容为空",
83
+ * 而不是拿一句"(无内容)"把两种情形糊在一起。
84
+ *
85
+ * 未开启冻结时返回 `enabled:false` + 空列表 + `notice`,**不抛错**:开关没开是
86
+ * 部署状态,不是调用错误(与 `resolveConflictPair` 对同一情形的处理一致)。
87
+ */
88
+ export function listConflictPairs(deps, opts = {}) {
89
+ const raw = Math.floor(Number(opts.limit));
90
+ const limit = Number.isFinite(raw) && raw > 0 ? Math.min(raw, CONFLICT_LIST_LIMIT_MAX) : CONFLICT_LIST_LIMIT_DEFAULT;
91
+ if (!deps.conflictFreezeEnabled) {
92
+ return {
93
+ enabled: false,
94
+ total: 0,
95
+ items: [],
96
+ notice: '矛盾冻结未开启(conflictFreeze.enabled=false):队列恒空,没有待裁决对。',
97
+ };
98
+ }
99
+ const pending = deps.l1.listConflictPending({ limit });
100
+ const ids = new Set();
101
+ for (const p of pending) {
102
+ ids.add(p.winnerId);
103
+ ids.add(p.loserId);
104
+ }
105
+ const contentById = new Map();
106
+ for (const r of deps.l1.getByIds([...ids]))
107
+ contentById.set(r.id, r.content);
108
+ const items = pending.map((p) => ({
109
+ pair_id: p.pairId,
110
+ run_id: p.runId,
111
+ winner_id: p.winnerId,
112
+ winner_content: contentById.get(p.winnerId) ?? '',
113
+ loser_id: p.loserId,
114
+ loser_content: contentById.get(p.loserId) ?? '',
115
+ created_at: p.createdAt,
116
+ }));
117
+ return { enabled: true, total: deps.l1.countConflictPendingUnresolved(), items };
118
+ }
119
+ /** 列表结果的人类可读渲染(工具路径用)。 */
120
+ export function renderConflicts(v) {
121
+ if (!v.enabled)
122
+ return v.notice ?? '矛盾冻结未开启:没有待裁决对。';
123
+ if (v.items.length === 0)
124
+ return '没有待裁决的冲突对(队列为空)。';
125
+ const more = v.total > v.items.length ? `\n(共 ${v.total} 对,此处显示前 ${v.items.length} 对)` : '';
126
+ const rows = v.items.map((p, i) => {
127
+ const w = p.winner_content || '(该记录已不在检索库)';
128
+ const l = p.loser_content || '(该记录已不在检索库)';
129
+ return (`${i + 1}. pair_id ${p.pair_id} (${p.created_at})\n` +
130
+ ` LLM 建议胜方 ${p.winner_id}:${w}\n` +
131
+ ` LLM 建议败方 ${p.loser_id}:${l}`);
132
+ });
133
+ return `待裁决冲突对 ${v.items.length} 条${more}\n\n${rows.join('\n\n')}\n\n` +
134
+ '用 memory_resolve_conflict 给出结论:winner / loser / both。';
135
+ }
@@ -754,6 +754,75 @@ export interface EmbeddingModelDeleteResponse {
754
754
  ok: boolean;
755
755
  error?: string;
756
756
  }
757
+ /** 一条待裁决冲突对(与 `ConflictPair` 同源,但只暴露人要看的那几个字段)。 */
758
+ export interface ConflictPairView {
759
+ pair_id: string;
760
+ /** 产生该冻结的 L1 蒸馏批次 id(接 §B 凭证链)。 */
761
+ run_id: string;
762
+ /** LLM 建议的胜方 id。**只是进入队列时的排序位,不代表结论**。 */
763
+ winner_id: string;
764
+ /** 胜方正文;**空串 = 该记录已不在检索库**(被合并/删掉了),不是"内容为空"。 */
765
+ winner_content: string;
766
+ loser_id: string;
767
+ loser_content: string;
768
+ created_at: string;
769
+ }
770
+ /** `dsh-memory/conflicts` 请求(读待裁决队列)。 */
771
+ export interface ConflictsRequest {
772
+ /** 最多返回多少对(默认 50,上限 200)。 */
773
+ limit?: number;
774
+ }
775
+ /** `dsh-memory/conflicts` 响应。 */
776
+ export interface ConflictsResponse {
777
+ /**
778
+ * `conflictFreeze.enabled`。**必须与 `items: []` 分开呈现** ——
779
+ * "开关没开"与"开了但没有待裁决"在面板上是两件不同的事。
780
+ */
781
+ enabled: boolean;
782
+ /** 未裁决总数(可能大于 `items.length`)。 */
783
+ total: number;
784
+ items: ConflictPairView[];
785
+ notice?: string;
786
+ }
787
+ /** `dsh-memory/conflict-resolve` 请求。 */
788
+ export interface ConflictResolveRequest {
789
+ pairId: string;
790
+ /** `winner` | `loser` | `both`。 */
791
+ outcome: string;
792
+ }
793
+ /** `dsh-memory/conflict-resolve` 响应(与 `memory_resolve_conflict` 工具同形状)。 */
794
+ export interface ConflictResolveResponse {
795
+ pair_id: string;
796
+ outcome: string;
797
+ /** 裁决时刻(ISO);空串 = 未生效。 */
798
+ resolved_at: string;
799
+ /** 因裁决从检索中退场的记录 id(无则空串)。 */
800
+ removed_record_id: string;
801
+ notice?: string;
802
+ }
803
+ /** `dsh-memory/receipts` 请求(两维回溯,至少给一个)。 */
804
+ export interface ReceiptsRequest {
805
+ recordId?: string;
806
+ runId?: string;
807
+ limit?: number;
808
+ }
809
+ /** 一条决策凭证(与 `memory_receipts` 工具同形状)。 */
810
+ export interface ReceiptItemView {
811
+ receipt_id: string;
812
+ run_id: string;
813
+ record_id: string;
814
+ /** `store` / `update` / `merge` / `skip` / `conflict` / `skip_missing`。 */
815
+ kind: string;
816
+ input_digest: string;
817
+ decided_at: string;
818
+ }
819
+ /** `dsh-memory/receipts` 响应。 */
820
+ export interface ReceiptsResponse {
821
+ /** `record` / `run` / `both` / `none`。 */
822
+ dimension: string;
823
+ items: ReceiptItemView[];
824
+ total: number;
825
+ }
757
826
  export interface DshMemoryRequestMap {
758
827
  'dsh-memory/stats': Record<string, never>;
759
828
  'dsh-memory/token-cost': TokenCostRequest;
@@ -764,6 +833,9 @@ export interface DshMemoryRequestMap {
764
833
  'dsh-memory/settings-set': SettingsSetRequest;
765
834
  'dsh-memory/list-records': ListRecordsRequest;
766
835
  'dsh-memory/records-delete': RecordsDeleteRequest;
836
+ 'dsh-memory/receipts': ReceiptsRequest;
837
+ 'dsh-memory/conflicts': ConflictsRequest;
838
+ 'dsh-memory/conflict-resolve': ConflictResolveRequest;
767
839
  'dsh-memory/graph-search': GraphSearchRequest;
768
840
  'dsh-memory/graph-node-get': GraphNodeGetRequest;
769
841
  'dsh-memory/scenes': Record<string, never>;
@@ -795,6 +867,9 @@ export interface DshMemoryResponseMap {
795
867
  'dsh-memory/settings-set': SettingsSetResponse;
796
868
  'dsh-memory/list-records': ListRecordsResponse;
797
869
  'dsh-memory/records-delete': RecordsDeleteResponse;
870
+ 'dsh-memory/receipts': ReceiptsResponse;
871
+ 'dsh-memory/conflicts': ConflictsResponse;
872
+ 'dsh-memory/conflict-resolve': ConflictResolveResponse;
798
873
  'dsh-memory/graph-search': GraphSearchResponse;
799
874
  'dsh-memory/graph-node-get': GraphNodeGetResponse;
800
875
  'dsh-memory/scenes': ScenesResponse;
package/dist/stats.d.ts CHANGED
@@ -22,7 +22,7 @@ export interface MemoryStatusSource {
22
22
  pending(): number;
23
23
  }
24
24
  /**
25
- * 端点全集运行时清单(31 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
25
+ * 端点全集运行时清单(33 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
26
26
  * contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
27
27
  * 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
28
28
  * (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
package/dist/stats.js CHANGED
@@ -20,13 +20,13 @@ import { emptyRecallStats } from './hooks/recall.js';
20
20
  import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChainOrNull, resolveModelContextWindow, resolveModelEfforts, resolveModelRoute } from './llm.js';
21
21
  import { projectDistillChain, validateDistillChain } from './settings.js';
22
22
  import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
23
- import { resolveConflictPair } from './conflict-service.js';
23
+ import { resolveConflictPair, listConflictPairs } from './conflict-service.js';
24
24
  import { errDetail } from './util/filelog.js';
25
25
  import { snapshotTokenCost } from './token-cost.js';
26
26
  const require = createRequire(import.meta.url);
27
27
  export const PLUGIN_VERSION = require('../package.json').version;
28
28
  /**
29
- * 端点全集运行时清单(31 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
29
+ * 端点全集运行时清单(33 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
30
30
  * contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
31
31
  * 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
32
32
  * (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
@@ -47,6 +47,7 @@ export const MEMORY_ENDPOINTS = [
47
47
  'dsh-memory/list-records',
48
48
  'dsh-memory/records-delete',
49
49
  'dsh-memory/receipts',
50
+ 'dsh-memory/conflicts',
50
51
  'dsh-memory/conflict-resolve',
51
52
  'dsh-memory/graph-search',
52
53
  'dsh-memory/graph-node-get',
@@ -719,6 +720,14 @@ export async function handleEndpoint(endpoint, payload, deps) {
719
720
  };
720
721
  return resp;
721
722
  }
723
+ // ── §C 矛盾冻结的**读**方向:与 memory_conflicts 工具共用同一形状 ──
724
+ // 裁决端点(conflict-resolve)早就在,但只有"写"没有"读" —— 于是 pair_id
725
+ // 无处可得:模型被指到不存在的 memory_conflicts 工具,人也没有面板。
726
+ // 未开启冻结时同样走返回体(enabled:false + notice)而非抛错,理由同上。
727
+ case 'dsh-memory/conflicts': {
728
+ const p = (payload ?? {});
729
+ return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
730
+ }
722
731
  // ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
723
732
  // 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
724
733
  // 部署状态,故它走返回体(带 notice)而非抛错;pair_id/outcome 缺参才抛。
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from '../store/receipts.js';
3
- import { renderConflictResolution, resolveConflictPair } from '../conflict-service.js';
3
+ import { listConflictPairs, renderConflictResolution, renderConflicts, resolveConflictPair } from '../conflict-service.js';
4
4
  import { normPersistence, normScope, resolveRecordScope } from '../types.js';
5
5
  import { scopeFilterOf, workspaceIdOf } from '../workspace.js';
6
6
  import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
@@ -816,6 +816,56 @@ ruminate) {
816
816
  return { dimension, items: rows.map(toReceiptView), total: stores.l1.countReceipts(query) };
817
817
  },
818
818
  }));
819
+ // ── memory_conflicts: §C 待裁决队列的**读**出口 ──
820
+ // `memory_resolve_conflict` 的描述里早就写着"待裁决对可用 memory_conflicts 查看",
821
+ // 但那个工具**一直不存在** —— 模型照着描述调用只会拿到"工具不存在"。
822
+ // 裁决端点在、读端点与读工具两端都缺,队列于是成了只进不出的黑洞
823
+ // (安全阀超时自动了结会成为唯一出路,那正是 §C 想避免的)。
824
+ ctx.tools.register(defineTool({
825
+ name: 'memory_conflicts',
826
+ description: '列出**矛盾冻结**的待裁决对(§C)。冻结不自动裁决:新记忆照常入库,与它冲突的旧记忆作为**一对**停在队列里,双方内容都不被改写,直到人给出结论。返回每对的 pair_id、**双方正文**与 LLM 建议的胜负方(id 只是进入队列时的排序位,不代表结论)。看完用 memory_resolve_conflict 给出结论:winner / loser / both。',
827
+ parameters: {
828
+ limit: { type: 'number', description: '最多返回多少对(默认 50,上限 200)' },
829
+ },
830
+ output: {
831
+ schema: {
832
+ type: 'object',
833
+ properties: {
834
+ enabled: { type: 'boolean', description: '矛盾冻结是否开启;关闭时队列恒空,与"开启但没有待裁决"是两回事' },
835
+ total: { type: 'number', description: '未裁决总数(可能大于 items.length)' },
836
+ items: {
837
+ type: 'array',
838
+ description: '待裁决对(最多 limit 条)',
839
+ items: {
840
+ type: 'object',
841
+ properties: {
842
+ pair_id: { type: 'string' },
843
+ run_id: { type: 'string', description: '产生该冻结的蒸馏批次 id(可交给 memory_receipts 追该轮判了什么)' },
844
+ winner_id: { type: 'string' },
845
+ winner_content: { type: 'string', description: 'LLM 建议胜方的正文;空串 = 该记录已不在检索库' },
846
+ loser_id: { type: 'string' },
847
+ loser_content: { type: 'string', description: '同上' },
848
+ created_at: { type: 'string' },
849
+ },
850
+ additionalProperties: false,
851
+ },
852
+ },
853
+ notice: { type: 'string', description: '非结果的状态提示(如冻结未开启 / 本会话记忆已关闭)' },
854
+ },
855
+ additionalProperties: false,
856
+ },
857
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? renderConflicts(value) }],
858
+ },
859
+ execute: async (args, exec) => {
860
+ // 档位拒读门与 memory_receipts 同款:off 会话对记忆系统完全隐身,
861
+ // 不该反过来能内省"库里有哪些自相矛盾的记忆"。
862
+ const family = familyOfCaller(exec);
863
+ if (family === null) {
864
+ return { enabled: false, total: 0, items: [], notice: blockNoticeOf(exec) };
865
+ }
866
+ return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, { limit: typeof args.limit === 'number' ? args.limit : undefined });
867
+ },
868
+ }));
819
869
  // ── memory_resolve_conflict: §C 矛盾冻结的人工裁决出口 ──
820
870
  // 冻结把裁决权交还给人,那么**必须**有一个"人能把结论说回去"的出口——
821
871
  // 否则待裁决队列是个只进不出的黑洞,安全阀(task_24)会成为唯一出路,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",
7
- "tag": "dsh-0.1.2"
7
+ "tag": "dsh-0.1.5"
8
8
  },
9
9
  "description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
10
10
  "type": "module",