@fanchao8609/agent_brain_sync 1.16.3 → 1.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.16.3",
3
+ "version": "1.16.4",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/relevant.js CHANGED
@@ -139,13 +139,53 @@ export function matchPage(body, pageName, queryWords) {
139
139
  // 查"zzzz不存在"时 qGrams={zz,不存,存在}, 页里恰好有"存在" → ratio=0.5 误命中。
140
140
  // 两个修正: (1) 重复字符的 gram 不算(zz 去重); (2) 至少得命中 2 个不同 gram。
141
141
  const qGrams = new Set(queryWords.flatMap((w) => ngrams(String(w).toLowerCase())));
142
- const pGrams = new Set(ngrams(text.slice(0, 4000)).concat(ngrams(name)));
142
+ const pGrams = pageGrams(text, name);
143
143
  let overlap = 0;
144
144
  for (const g of qGrams) if (pGrams.has(g)) overlap++;
145
145
  const ratio = qGrams.size ? overlap / qGrams.size : 0;
146
146
  return { exact, overlap, ratio };
147
147
  }
148
148
 
149
+ /** 页正文侧 2-gram 集合的缓存。
150
+ *
151
+ * 为何缓存(2026-10-07 实测):load 慢的根因就在这里。
152
+ * queryHint 走 topicStrength → 对【每个词 × 每一页】调 rankPage → matchPage,
153
+ * 而每次都对同一页重算 ngrams(text.slice(0,4000))。
154
+ * 实测规模:17 个活跃词 × 63 页 = 1071 次 matchPage,实测 156ms,
155
+ * 而不同页只有 63 个 —— 同一次展开算了 17 遍。
156
+ * CPU profile:matchPage + ngrams + 那条正则占 load 总耗时的 82%。
157
+ * 端到端:改看板前 191ms / 空看板 40ms(todo 越大词越多 → 越慢,用户实报的「卡」)。
158
+ *
159
+ * 键的构造(关键):用正文当 Map 的键、页名当二级 Map 的键 —— 两级分开存,
160
+ * 而不是拼成一个字符串键。好处:「同正文不同页名」天然是两个不同的表项,
161
+ * 永远不可能互相覆盖(曾用 name+NUL+text 拼串,那是在赌没人会改错键;
162
+ * 现有 20 个测试实际上抓不住那种错,改成两级后这个 bug 类不存在)。
163
+ * 缓存值是**只读的 Set**,matchPage 只查不改 —— 避免把页名 gram 写进缓存。
164
+ *
165
+ * 为何用普通 Map(非 WeakMap):字符串是原始值,不能当 WeakMap 键。
166
+ * 故给容量上限,满了清空(一轮遍历内复用已足够)——
167
+ * ponytail: 不做 LRU,清空式淘汰对本场景够用。
168
+ */
169
+ const PAGE_GRAMS_CACHE = new Map();
170
+ const PAGE_GRAMS_MAX = 500;
171
+
172
+ /** 取(或算)一页的 2-gram 集合(含页名)。返回的 Set 只读,调用方不得修改。 */
173
+ function pageGrams(text, name) {
174
+ // 两级 Map:外层按正文、内层按页名。不用拼串键 —— 见上方注释。
175
+ let inner = PAGE_GRAMS_CACHE.get(text);
176
+ if (inner) {
177
+ const hit = inner.get(name);
178
+ if (hit) return hit;
179
+ } else {
180
+ if (PAGE_GRAMS_CACHE.size >= PAGE_GRAMS_MAX) PAGE_GRAMS_CACHE.clear();
181
+ inner = new Map();
182
+ PAGE_GRAMS_CACHE.set(text, inner);
183
+ }
184
+ const set = new Set(ngrams(text.slice(0, 4000)).concat(ngrams(name)));
185
+ inner.set(name, set);
186
+ return set;
187
+ }
188
+
149
189
  /** 拆字符 2-gram(中英文都适用)—— 模糊匹配的最小单位。
150
190
  * 先过 [\p{L}\p{N}] 再切,单一字符组成的 gram(如 zz) 没区分度,丢掉。 */
151
191
  function ngrams(s) {
package/src/todo.js CHANGED
@@ -392,18 +392,17 @@ export function rebuildStructure(text, spec) {
392
392
  const out = [spec.h1];
393
393
  const pre = trimBlank(preamble);
394
394
  if (pre.length) out.push('', ...pre);
395
- // 空分区之间不插空行(否则每次首跑都会“把空行加进去”而写盘一次,
396
- // 而 load 是好读命令 —— 不该因纯排版差异去改文件)。
397
- // 有内容的第一个分区与前言之间保留一个空行(排版),其余紧凑。
398
- // 但 H1 后必须恒有一个空行(2026-10-05 实测漏网):前言为空且首个分区为空时
399
- // (如 H1 缺失被补回、而 `## Rules` 还没条目),两个条件都不满足 → `# H1`
400
- // 和 `## Rules` 直接相贴。首行是这个文件的门面,不容忍这种粘贴。
401
- let first = true;
395
+ // 每个分区标题前恒有一个空行(含空分区)。
396
+ // 坑(2026-10-06 定位):旧写法对空分区走 `else out.push(name)` —— 不补空行,
397
+ // 于是「有内容分区 → 空分区」的接缝被挤成 `- [[页]] …` 紧贴 `## Sources`。
398
+ // 表现成「index 里那个空行反复丢」:人工补回 → 下次任意写操作(过 enforceBrainFormat)
399
+ // 又归一掉,来回拉锯(本机 corp_agent 实测,17961e5 补、f3f7e84 又丢)。
400
+ // 原注释担心「每次首跑都写盘一次」——实测不会:归一后自身就是稳定形态,
401
+ // 第二次跑 fixed 为空、零字节改动(幂等,见 test/todo 的自检)。
402
+ // H1 后必须恒有空行:`# H1` 与首个分区相贴是门面事故,不容忍。
402
403
  for (const [idx, name] of spec.order.entries()) {
403
404
  const body = trimBlank(bucket.get(name) || []);
404
- if (body.length || first) out.push('', name, ...body);
405
- else out.push(name);
406
- first = false;
405
+ out.push('', name, ...body);
407
406
  }
408
407
  for (const e of extras) {
409
408
  const body = trimBlank(e.lines);