dsh-lite-memory 0.18.38 → 0.18.39

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.
@@ -0,0 +1,8 @@
1
+ # 0.18.39:读记忆找不到时直接给完整标题
2
+
3
+ - 上下文被压缩后常只剩缩短的标题;按短标题读会精确匹配失败,以前只报「没有这条」,模型只能再猜一轮。
4
+ - 现在失败时就地列出最多 3 条相近的完整标题,模型一次就能换对;成功路径与常驻提示都不变,多出的字只出现在失败回执里。
5
+ - 候选只从当前会话能看到的项目层与全局层取,不跨项目;仍然不按标题模糊读正文,避免读错条目。
6
+ - 补回归:存一条带括号后缀的标题,用前缀读要报出完整标题,再用完整标题读到正文。
7
+
8
+ 验证:`npm test` 全绿;常驻工具文本仍是 13375 B。
package/index.js CHANGED
@@ -1005,6 +1005,28 @@ function ambiguousTitleError(title, hits, label) {
1005
1005
  return `${label}「${title}」有 ${hits.length} 条同名,无法确定是哪一条:${detail}。先归档其中一条,或换一个能区分的标题再试`;
1006
1006
  }
1007
1007
 
1008
+ /**
1009
+ * 标题解析失败时给几条相近的完整标题:上下文里常只剩缩短的标题,
1010
+ * 精确匹配失败就地列出候选,比让模型再猜一次省一轮。
1011
+ */
1012
+ function nearTitleHints(targets, title) {
1013
+ const out = [];
1014
+ const seen = new Set();
1015
+ for (const target of targets) {
1016
+ let index;
1017
+ try { index = loadIndex(target); } catch { continue; }
1018
+ for (const entry of [...(index.entries || []), ...(index.archived || [])]) {
1019
+ const current = String(entry && entry.title || '').trim();
1020
+ if (!current || current === title || seen.has(current)) continue;
1021
+ if (!current.includes(title) && !title.includes(current)) continue;
1022
+ seen.add(current);
1023
+ out.push(current);
1024
+ if (out.length >= 3) return out;
1025
+ }
1026
+ }
1027
+ return out;
1028
+ }
1029
+
1008
1030
  /**
1009
1031
  * 返回 { id } 或抛出:显式层只查那一层;默认先项目后全局(别的项目不参与解析)。
1010
1032
  * 同一层出现多条同名一律报歧义,绝不取第一条。
@@ -1030,6 +1052,10 @@ function refIdOf(scope, args, label = '记忆') {
1030
1052
  if (hits.length > 1) throw new Error(ambiguousTitleError(title, hits, label));
1031
1053
  if (hits.length === 1) return hits[0].id;
1032
1054
  }
1055
+ const near = nearTitleHints(sources, title);
1056
+ if (near.length) {
1057
+ throw new Error(`没有「${title}」这条${label}。相近的里有:${near.map((item) => `「${item}」`).join('、')};用完整标题或 id 再试`);
1058
+ }
1033
1059
  throw new Error(`没有「${title}」这条${label};先用 memory_search 对一下标题`);
1034
1060
  }
1035
1061
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.38",
3
+ "version": "0.18.39",
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",
@@ -287,6 +287,15 @@ assertSchema('memory_save', twinUpdate);
287
287
  assert.equal(store.readEntry(twinScope, twinAId).body, '正文甲', '按 id 读不能被同名条目串走');
288
288
  assert.equal(store.readEntry(twinScope, twinBId).body, '正文乙');
289
289
 
290
+ /** 上下文里常只剩缩短的标题:精确匹配失败时要把完整标题就地报出来,不让模型再猜一轮。 */
291
+ store.saveEntry(twinScope, { title: '生效验证顺序(含:桌面端没有 Cmd+R)', summary: '缩短标题的探针', body: '正文', writeMode: 'patch' });
292
+ await assert.rejects(
293
+ () => exec('memory_read', { title: '生效验证顺序' }),
294
+ /相近的里有:「生效验证顺序(含:桌面端没有 Cmd\+R)」/,
295
+ );
296
+ const hintedRead = await exec('memory_read', { title: '生效验证顺序(含:桌面端没有 Cmd+R)' });
297
+ assert.equal(hintedRead.body, '正文');
298
+
290
299
  /** 重命名后仍按原 id 定位到同一条;归档后重建同名条目也不能冒领旧 id。 */
291
300
  const renameSaved = store.saveEntry(twinScope, { title: '改名前的探针', summary: '旧身份', body: '旧正文', writeMode: 'patch' });
292
301
  const renameSeedId = renameSaved.record?.id ?? renameSaved.id;