dsh-lite-memory 0.17.0 → 0.18.19

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 (42) hide show
  1. package/README.md +15 -15
  2. package/TRIGGERS.md +1 -1
  3. package/client.js +343 -44
  4. package/docs/BENCH.md +10 -8
  5. package/docs/RELEASE-0.18.0.md +35 -0
  6. package/docs/RELEASE-0.18.1.md +18 -0
  7. package/docs/RELEASE-0.18.10.md +25 -0
  8. package/docs/RELEASE-0.18.11.md +48 -0
  9. package/docs/RELEASE-0.18.12.md +27 -0
  10. package/docs/RELEASE-0.18.13.md +13 -0
  11. package/docs/RELEASE-0.18.14.md +13 -0
  12. package/docs/RELEASE-0.18.15.md +5 -0
  13. package/docs/RELEASE-0.18.2.md +21 -0
  14. package/docs/RELEASE-0.18.3.md +16 -0
  15. package/docs/RELEASE-0.18.4.md +15 -0
  16. package/docs/RELEASE-0.18.5.md +21 -0
  17. package/docs/RELEASE-0.18.6.md +18 -0
  18. package/docs/RELEASE-0.18.7.md +23 -0
  19. package/docs/RELEASE-0.18.8.md +18 -0
  20. package/docs/RELEASE-0.18.9.md +24 -0
  21. package/index.js +403 -131
  22. package/learning.js +65 -6
  23. package/package.json +7 -4
  24. package/state.js +56 -8
  25. package/store.js +361 -33
  26. package/test/client.test.mjs +79 -17
  27. package/test/contract.test.mjs +231 -93
  28. package/test/control.test.mjs +2 -2
  29. package/test/crossing.test.mjs +22 -1
  30. package/test/eval-seed.test.mjs +113 -0
  31. package/test/evolution-read.test.mjs +33 -10
  32. package/test/ha.test.mjs +186 -0
  33. package/test/layer-read.test.mjs +293 -0
  34. package/test/learning.test.mjs +80 -0
  35. package/test/palace.test.mjs +2 -0
  36. package/test/recall.test.mjs +80 -9
  37. package/test/record.test.mjs +130 -0
  38. package/test/safety.test.mjs +119 -0
  39. package/test/state.test.mjs +57 -1
  40. package/test/store.test.mjs +49 -5
  41. package/test/view.test.mjs +3 -0
  42. package/view.js +97 -13
@@ -28,7 +28,7 @@ assert.equal(panel.name, 'shell.overlay');
28
28
  assert.equal(panel.order, 30);
29
29
 
30
30
  const EMPTY_CHILDREN = [];
31
- const { metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, MemoryPalace, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf } = seen;
31
+ const { metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, MemoryPalace, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf, TurnMemory, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, recallLineOf, agoLabel, checkupOf, titleCountsOf, rowKeyOf, rowKeepFlags } = seen;
32
32
 
33
33
  // 图谱只按真实数据画:共享标签成枢纽,显式关联成边,位置由 id 决定。
34
34
  const graph = graphOf([
@@ -96,23 +96,24 @@ assert.equal(metaOf(settled(null)), null);
96
96
  assert.equal(metaOf(settled([{ kind: 'memory-read' }])), null);
97
97
 
98
98
  // 保存只在真的改了时进引用;正文保存不冒充引用。
99
- const save = citationOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', title: 'A', scope: 'project', revision: 3, changed: false, mode: 'unchanged' }, 'memory-save');
99
+ const save = citationOf({ kind: 'memory-save', title: 'A', scope: 'project', revision: 3, changed: false, mode: 'unchanged' }, 'memory-save');
100
100
  assert.equal(save, null);
101
- const changed = citationOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', title: 'A', scope: 'global', revision: 2, changed: true, mode: 'updated' }, 'memory-save');
101
+ const changed = citationOf({ kind: 'memory-save', title: 'A', scope: 'global', revision: 2, changed: true, mode: 'updated' }, 'memory-save');
102
102
  assert.equal(changed.exposure, 'update');
103
103
  assert.equal(changed.scope, 'global');
104
104
 
105
105
  // 没有真改动、缺版本、缺工作目录的写入行不产生撤销入口。
106
- assert.equal(undoRefOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', revision: 3, changed: false }, '/tmp/p'), null);
107
- assert.equal(undoRefOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', revision: 3 }, '/tmp/p'), null);
108
- assert.equal(undoRefOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', revision: 3, changed: true }, ''), null);
109
- assert.equal(undoRefOf({ kind: 'memory-save', id: 'mem_aaaaaaaa', revision: 2, changed: true }, '/tmp/p').revision, 2);
106
+ assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3, changed: false }, '/tmp/p'), null);
107
+ assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3 }, '/tmp/p'), null);
108
+ assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3, changed: true }, ''), null);
109
+ assert.equal(undoRefOf({ kind: 'memory-save', revision: 2, changed: true }, '/tmp/p'), null);
110
+ assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 2, changed: true }, '/tmp/p').revision, 2);
110
111
 
111
112
  // 搜索的多条引用按 refs 展开;删除/归档状态原样保留。
112
113
  const refs = citationsOfMeta({ kind: 'memory-search', refs: [
113
- { id: 'mem_aaaaaaaa', title: 'A', scope: 'project', revision: 1, status: 'ok' },
114
- { id: 'mem_bbbbbbbb', title: 'B', scope: 'global', revision: 4, status: 'archived' },
115
- { id: 'sug_cccccccc', title: 'C', scope: 'global', revision: 1, status: 'ok' },
114
+ { title: 'A', scope: 'project', revision: 1, status: 'ok' },
115
+ { title: 'B', scope: 'global', revision: 4, status: 'archived' },
116
+ { title: '', scope: 'global', revision: 1, status: 'ok' },
116
117
  ] });
117
118
  assert.equal(refs.length, 2);
118
119
  assert.equal(refs[1].status, 'archived');
@@ -121,17 +122,17 @@ assert.equal(refs[0].exposure, 'reference');
121
122
  // 一轮里 PTC 子结果也算;同 id 同用途只留一条,位置不变、内容取最新。
122
123
  const nodes = [
123
124
  { root: settled({ kind: 'memory-search', refs: [
124
- { id: 'mem_aaaaaaaa', title: 'A', scope: 'project', revision: 1, status: 'ok' },
125
+ { title: 'A', scope: 'project', revision: 1, status: 'ok' },
125
126
  ] }) },
126
- { root: settled({ kind: 'memory-read', id: 'mem_aaaaaaaa', title: 'A2', scope: 'project', revision: 2, status: 'ok' }, { subCalls: [settled({ kind: 'memory-read', id: 'mem_aaaaaaaa', title: 'A3', scope: 'project', revision: 2, status: 'ok' })] }) },
127
- { root: settled({ kind: 'memory-read', id: 'mem_bbbbbbbb', title: 'B', scope: 'global', revision: 1, status: 'ok' }) },
127
+ { root: settled({ kind: 'memory-read', title: 'A', scope: 'project', revision: 2, status: 'ok' }, { subCalls: [settled({ kind: 'memory-read', title: 'A', scope: 'project', revision: 2, status: 'ok' })] }) },
128
+ { root: settled({ kind: 'memory-read', title: 'B', scope: 'global', revision: 1, status: 'ok' }) },
128
129
  ];
129
130
  const turn = dedupe(citationsOfTurn(nodes));
130
131
  assert.equal(turn.length, 2);
131
- assert.equal(turn[0].id, 'mem_aaaaaaaa');
132
+ assert.equal(turn[0].title, 'A');
132
133
  assert.equal(turn[0].revision, 2);
133
- assert.equal(turn[0].title, 'A3');
134
- assert.equal(turn[1].id, 'mem_bbbbbbbb');
134
+ assert.equal(turn[1].title, 'B');
135
+ assert.equal('id' in turn[0], false);
135
136
 
136
137
  // 旧行没有元数据:什么也不产出,绝不从文案猜 id。
137
138
  assert.deepEqual(citationsOfTurn([{ root: null }, { root: { kind: 'tool-result', content: [{ type: 'text', text: 'memory · saved "A" mem_aaaaaaaa' }] } }]), []);
@@ -280,5 +281,66 @@ assert.equal(palacePicked, 'mem_a');
280
281
  const palaceEmpty = MemoryPalace({ records: [], selectedId: '', onPick: () => {}, emptyText: '这个项目还没有记忆' });
281
282
  assert.equal(expandNodes(palaceEmpty).some((node) => node.children.includes('这个项目还没有记忆')), true);
282
283
 
283
- console.log('client.test.mjs: 19 passed + palace');
284
+ // 引用行只标层与来源:类型留在数据里给检索用,不上会话界面。
285
+ const typedSearch = citationOf({ title: 'A', scope: 'global', revision: 1, status: 'ok', kind: 'preference' }, 'memory-search');
286
+ assert.equal(typedSearch.entryKind, 'preference');
287
+ assert.equal(refMetaOf(typedSearch), '[全局]');
288
+ assert.equal(citationOf({ kind: 'memory-read', title: 'P', recordKind: 'procedure', scope: 'project', revision: 2 }, 'memory-read').entryKind, 'procedure');
289
+ assert.equal(refMetaOf({ scope: 'project', origin: 'user_explicit' }), '[项目]');
290
+ assert.equal(refMetaOf({ scope: 'global', origin: 'project_evolution' }), '[全局][自进化]');
291
+ /** 更新距今照旧,只是不再拼类型与「你记的」。 */
292
+ const aged = citationOf({ kind: 'preference', title: 'C', scope: 'global', revision: 1, updated: new Date(Date.now() - 3 * 86_400_000).toISOString() }, 'memory-search');
293
+ assert.equal(aged.entryKind, 'preference');
294
+ assert.equal(refMetaOf(aged), '[全局] · 3 天前更新');
295
+ assert.equal(agoLabel(''), '');
296
+ assert.equal(agoLabel(new Date(Date.now() - 40 * 86_400_000).toISOString()), '');
297
+
298
+ // 解释模式按会话存在本机,缺省关闭;关着零命中不出提示,打开后才说原因。
299
+ assert.equal(explainModeOf('s1'), false);
300
+ const quietRefs = [];
301
+ Object.defineProperty(quietRefs, 'noTriggerReason', { value: '当前任务不适用', enumerable: false, configurable: true });
302
+ assert.equal(TurnMemory({ sessionId: 's1', turn: '7', useRefs: () => quietRefs }), null);
303
+ setExplainMode('s1', true);
304
+ assert.equal(explainModeOf('s1'), true);
305
+ assert.equal(explainModeOf('s2'), false);
306
+ const told = TurnMemory({ sessionId: 's1', turn: '7', useRefs: () => quietRefs });
307
+ assert.ok(told);
308
+ assert.equal(JSON.stringify(told).includes('当前任务不适用'), true);
309
+ assert.equal(explainKeyOf('s1').startsWith('lite-memory:explain:'), true);
310
+
311
+
312
+ const checkupRows = [
313
+ { id: 'mem_c0000001', title: '同一件事', when: '', lastUsed: '' },
314
+ { id: 'mem_c0000002', title: '同一件事', when: '当 A 时', lastUsed: '2026-01-01T00:00:00.000Z' },
315
+ { id: 'mem_c0000003', title: '另一件', when: '当 B 时', lastUsed: '2026-01-01T00:00:00.000Z' },
316
+ ];
317
+ const checkupScopes = [{ key: 'p', kind: 'project', entries: 3 }, { key: 'global', kind: 'global', entries: 213 }];
318
+ assert.deepEqual(checkupOf(checkupRows, checkupScopes, false), { duplicate: 1, unread: 1, globalEntries: 213, globalMissingWhen: null });
319
+ assert.deepEqual(checkupOf(checkupRows, checkupScopes, true), { duplicate: 1, unread: 1, globalEntries: 213, globalMissingWhen: 1 });
320
+ assert.deepEqual(filterRecords(checkupRows, '', { kind: 'duplicate' }).map((row) => row.id), ['mem_c0000001', 'mem_c0000002']);
321
+ assert.deepEqual(filterRecords(checkupRows, '', { kind: 'unread' }).map((row) => row.id), ['mem_c0000001']);
322
+ assert.deepEqual(filterRecords(checkupRows, '', { kind: 'noWhen' }).map((row) => row.id), ['mem_c0000001']);
323
+ assert.deepEqual([...titleCountsOf(checkupRows).values()], [2, 1]);
324
+ assert.equal(checkupOf([], [], false).globalEntries, null);
325
+ // 两条一模一样的拦截结果在同一轮里只展示一行:拦截结果没有 id,只能按曝光类型 + 文案 + 正文认。
326
+ const blockedBlock = (text) => ({ kind: 'tool-result', callId: 'call-a', content: [{ type: 'text', text }], subCalls: [] });
327
+ const blockedText = 'memory · 这条记忆的适用条件还没确认,先说明当前任务再读';
328
+ assert.equal(blockedText.includes('mem_'), false);
329
+ const blockedKeys = [rowKeyOf('memory_read', blockedBlock(blockedText)), rowKeyOf('memory_read', blockedBlock(blockedText))];
330
+ assert.equal(blockedKeys[0].length > 0, true);
331
+ assert.deepEqual(rowKeepFlags(blockedKeys), [true, false]);
332
+ assert.equal(blockedKeys.filter((_key, at) => rowKeepFlags(blockedKeys)[at]).length, 1);
333
+ assert.deepEqual(rowKeepFlags([rowKeyOf('memory_search', blockedBlock(blockedText)), rowKeyOf('memory_read', blockedBlock(blockedText))]), [true, true]);
334
+ assert.deepEqual(rowKeepFlags([rowKeyOf('memory_read', blockedBlock('memory · stopped'))]), [true]);
335
+ // 面板要能回答「为什么这次没提醒我」:只给结论和条数,不露原因码。
336
+ assert.equal(recallLineOf(null), '');
337
+ assert.equal(recallLineOf({ reason: 'hit', cards: 1, matched: 1 }), '上次自动提醒 1 条');
338
+ assert.equal(recallLineOf({ reason: 'budget', cards: 0, matched: 2 }), '上次没提醒,2 条相关记忆,本轮自动提醒已用满');
339
+ assert.equal(recallLineOf({ reason: 'needs_context', cards: 0, matched: 1 }), '上次没提醒,1 条相关记忆,适用条件未确认');
340
+ assert.equal(recallLineOf({ reason: 'no_overlap', cards: 0, matched: 0 }), '上次没提醒,没有匹配的记忆');
341
+ assert.equal(recallLineOf({ reason: 'memory_off', cards: 0, matched: 0 }), '上次没提醒,记忆已关闭');
342
+ assert.equal(recallLineOf({ reason: 'tools_only', cards: 0, matched: 0 }), '上次没提醒,只在你问时检索');
343
+ assert.equal(recallLineOf({ reason: 'made_up', cards: 0, matched: 0 }), '');
344
+ assert.doesNotMatch(recallLineOf({ reason: 'needs_context', cards: 0, matched: 1 }), /needs_context|reasonCode/);
345
+ console.log('client.test.mjs: 22 passed + palace');
284
346