dsh-lite-memory 0.18.40 → 0.18.41

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.
package/client.js CHANGED
@@ -1006,6 +1006,8 @@ window.__ModuleLoader__.load({
1006
1006
  });
1007
1007
  return counts;
1008
1008
  };
1009
+ /** 真被用过才算读过:`lastUsed` 有创建时间回退值。 */
1010
+ const usedOf = (record) => (record && record.used !== undefined ? record.used === true : Boolean(record && record.lastUsed));
1009
1011
  /** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
1010
1012
  const filterRecords = (records, query, hub) => {
1011
1013
  const rows = Array.isArray(records) ? records : EMPTY;
@@ -1020,7 +1022,7 @@ window.__ModuleLoader__.load({
1020
1022
  if (kind === 'duplicate' || kind === 'unread' || kind === 'noWhen') {
1021
1023
  const counts = titleCountsOf(rows);
1022
1024
  return rows.filter((record) => {
1023
- if (kind === 'unread') return !record.lastUsed;
1025
+ if (kind === 'unread') return !usedOf(record);
1024
1026
  if (kind === 'noWhen') return !record.when;
1025
1027
  const title = typeof record.title === 'string' ? record.title.trim() : '';
1026
1028
  return Boolean(title) && counts.get(title) > 1;
@@ -1043,7 +1045,7 @@ window.__ModuleLoader__.load({
1043
1045
  const globalRow = (Array.isArray(scopes) ? scopes : EMPTY).find((scope) => scope && scope.kind === 'global');
1044
1046
  return {
1045
1047
  duplicate,
1046
- unread: rows.filter((record) => !record.lastUsed).length,
1048
+ unread: rows.filter((record) => !usedOf(record)).length,
1047
1049
  globalEntries: globalRow && Number.isInteger(globalRow.entries) ? globalRow.entries : null,
1048
1050
  globalMissingWhen: globalSelected ? rows.filter((record) => !record.when).length : null,
1049
1051
  };
@@ -0,0 +1,26 @@
1
+ # 0.18.41:把「从未读过」变成真话
2
+
3
+ ## 问题
4
+
5
+ 面板「结构体检」里的「从未读过」和筛选器按 `lastUsed` 是否为空判断。但 `lastUsed`
6
+ 在新建、编辑时都会写入回退值,于是这个数字恒为 0,筛选也永远筛不出东西——一条从没被
7
+ 召回过的记忆看起来像读过。
8
+
9
+ ## 改法
10
+
11
+ - 记录新增 `everUsed`:只有真正送达模型并完成使用记账(`touchUsed`)才置真;
12
+ 第一次被用到时即使还在 `lastUsed` 的十分钟防抖窗口内,也只补标记、不额外落盘。
13
+ - 旧数据没有这个标记,用时间戳差异回填:`lastUsed` 与创建/更新时间都不同才算用过,
14
+ 因此升级后「从未读过」直接可信,不需要迁移脚本。
15
+ - 面板记录多带一个 `used` 布尔值;筛选项与体检计数都改看它,`lastUsed` 只继续用于
16
+ 陈旧度排序。界面文案不变。
17
+ - 没有新增常驻提示、没有改召回顺序、没有增加单次注入字节。
18
+
19
+ ## 验证
20
+
21
+ - `test/recall.test.mjs`:新建条目 `used === false`;同一回合送达后变 `true`,
22
+ 且未送达的回合不变。
23
+ - `test/client.test.mjs`:带 `lastUsed` 回退值但 `used === false` 的条目仍计入
24
+ 「从未读过」,`used === true` 的排除。
25
+ - 全量 `npm test` 通过;三次基准 P50 67–69 ms,发生在系统负载 8.6 的机器上,
26
+ 与 0.18.40 的 48.6 ms 差异来自负载,上下文字节仍为 368 B。
package/index.js CHANGED
@@ -476,6 +476,7 @@ function keepHandler(webServer, cwdForSession) {
476
476
  origin: originOut(record.origin),
477
477
  updated: record.updated || '',
478
478
  lastUsed: record.lastUsed || '',
479
+ used: record.used === true,
479
480
  bytes: Number(record.bytes) || 0,
480
481
  experimental: isExperimentalRecord(record),
481
482
  learning: record.learning ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.40",
3
+ "version": "0.18.41",
4
4
  "description": "On-demand memory for the DeepSeek Harness: nothing resident, at most three small cards on a real hit, bodies read on demand.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/store.js CHANGED
@@ -115,6 +115,9 @@ function toFlat(entry) {
115
115
  supersededBy: typeof entry.supersededBy === 'string' ? entry.supersededBy : '',
116
116
  supersedeReason: typeof entry.supersedeReason === 'string' ? entry.supersedeReason : '',
117
117
  lastUsed: maintenance.lastUsed || entry.updatedAt || entry.createdAt || '',
118
+ /** 是否真被用过(送达模型并记账过);`lastUsed` 有创建/更新时间回退值,不能拿它判断「从未读过」。 */
119
+ used: maintenance.everUsed === true
120
+ || Boolean(maintenance.lastUsed && maintenance.lastUsed !== entry.createdAt && maintenance.lastUsed !== entry.updatedAt),
118
121
  revision: Number(entry.revision) || 1,
119
122
  status: entry.status === 'archived' ? 'archived' : 'active',
120
123
  origin: ORIGINS.includes(entry.origin) ? entry.origin : 'user_explicit',
@@ -172,6 +175,7 @@ function fromFlat(record, status) {
172
175
  learning: cloneJson(record.learning) || cloneJson(record.maintenance?.learning) || undefined,
173
176
  hits: Number(record.hits) || 0,
174
177
  merges: Number(record.merges) || 0,
178
+ everUsed: record.everUsed === true || (record.maintenance && record.maintenance.everUsed === true),
175
179
  lastUsed: record.lastUsed || at,
176
180
  },
177
181
  };
@@ -1519,7 +1523,13 @@ function touchUsed(scope, ids) {
1519
1523
  const stamp = new Date(now).toISOString();
1520
1524
  let changed = false;
1521
1525
  for (const record of index.entries) {
1522
- if (!wanted.has(record.id) || !usedStale(record, now)) continue;
1526
+ if (!wanted.has(record.id)) continue;
1527
+ if (record.everUsed !== true) {
1528
+ /** 第一次被真用到必须记下来,哪怕刚创建、还没过 lastUsed 的防抖门槛。 */
1529
+ record.everUsed = true;
1530
+ changed = true;
1531
+ }
1532
+ if (!usedStale(record, now)) continue;
1523
1533
  record.lastUsed = stamp;
1524
1534
  changed = true;
1525
1535
  }
@@ -339,6 +339,8 @@ assert.deepEqual(checkupOf(checkupRows, checkupScopes, false), { duplicate: 1, u
339
339
  assert.deepEqual(checkupOf(checkupRows, checkupScopes, true), { duplicate: 1, unread: 1, globalEntries: 213, globalMissingWhen: 1 });
340
340
  assert.deepEqual(filterRecords(checkupRows, '', { kind: 'duplicate' }).map((row) => row.id), ['mem_c0000001', 'mem_c0000002']);
341
341
  assert.deepEqual(filterRecords(checkupRows, '', { kind: 'unread' }).map((row) => row.id), ['mem_c0000001']);
342
+ assert.deepEqual(filterRecords([{ id: 'mem_u1', title: '新写的', lastUsed: '2026-01-01T00:00:00.000Z', used: false }], '', { kind: 'unread' }).map((row) => row.id), ['mem_u1'], '有创建时间回退值但没真用过,仍算从未读过');
343
+ assert.equal(checkupOf([{ id: 'mem_u2', title: '用过的', lastUsed: '2026-01-01T00:00:00.000Z', used: true }], [], false).unread, 0);
342
344
  assert.deepEqual(filterRecords(checkupRows, '', { kind: 'noWhen' }).map((row) => row.id), ['mem_c0000001']);
343
345
  assert.deepEqual([...titleCountsOf(checkupRows).values()], [2, 1]);
344
346
  assert.equal(checkupOf([], [], false).globalEntries, null);
@@ -1016,6 +1016,8 @@ console.log('recall.test.mjs: ok');
1016
1016
  const scope = store.scopeOf(cwd);
1017
1017
  const probe = save(scope, { title: '量子纠缠咖啡机', tags: ['记账'], summary: '只有送达才记 lastUsed' });
1018
1018
  const lastUsedOf = () => store.locateByTitle(scope, '量子纠缠咖啡机').record.lastUsed;
1019
+ const usedOf = () => store.locateByTitle(scope, '量子纠缠咖啡机').record.used;
1020
+ assert.equal(usedOf(), false, '新建但没被用过的条目不能算读过');
1019
1021
  const baseline = lastUsedOf();
1020
1022
  const runtime = boot();
1021
1023
  const agent = makeAgent('agent-usage', cwd);
@@ -1027,6 +1029,6 @@ console.log('recall.test.mjs: ok');
1027
1029
  assert.match(runtime.provider.text({ agent, signal: hitSignal }), /量子纠缠咖啡机/);
1028
1030
  assert.equal(lastUsedOf(), baseline, '送达也不在 provider 路径上写盘');
1029
1031
  runtime.handlers.get('agent/turn-stopping').at(-1)({ signal: hitSignal });
1030
- assert.ok(lastUsedOf(), '回合收尾时把真正送达的条目记账');
1032
+ assert.equal(usedOf(), true, '送达并记账后「读过」标记跟着成立');
1031
1033
  assert.equal(store.locateByTitle(scope, '量子纠缠咖啡机').record.id, probe.id);
1032
1034
  }