@fanchao8609/agent_brain_sync 1.19.3 → 1.19.5

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.19.3",
3
+ "version": "1.19.5",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/lock.js CHANGED
@@ -15,22 +15,17 @@ export class LockTimeout extends Error {}
15
15
  let formatGate = null;
16
16
  export function setFormatGate(fn) { formatGate = fn; }
17
17
 
18
- /** 三个受管文件名 —— 它们存在就是为了当“数据库表”,格式必须恒定。 */
19
- const MANAGED = new Set(['index.md', 'log.md', 'todo.md']);
20
-
21
18
  /** 懒加载闸门:**漏了 setFormatGate 就等于没闸门**,而注册是 import 副作用,
22
19
  * 谁少 import 一次 store.js 就静默裂开(2026-10-18 实测:只 import lock.js 时
23
20
  * 裸散文直接写进了 index.md)。故这里不信任外部注册,缺失时自己补上。
24
21
  *
25
- * 补不上(= store.js 没调 setFormatGate)就对受管文件硬拒:宁可写失败也不要格崩坏
26
- * ——写不进去能重试,写脏了没人知道。 */
22
+ * 返回 null 表示「该文件不归闸门管」(非受管文件,或闸门自己说不管)—— 原样写盘。
23
+ * 依赖:`store.js` 顶层无条件调 `setFormatGate`(见那里)。若那行被删,受管文件
24
+ * 会退回静默直写 —— 这是已知的单点,防它靠 review 不靠代码(加守卫就是给不可能发生的
25
+ * 情况多写 6 行,2026-10-18 审查定)。 */
27
26
  async function gateFor(file) {
28
27
  if (!formatGate) await import('./store.js'); // 它的模块顶层会调 setFormatGate
29
- if (formatGate) return formatGate;
30
- if (MANAGED.has(basename(file))) {
31
- throw new Error(`✗ ${basename(file)} 是受管文件,但格式闸门未注册 —— 拒绝写入(防止格崩坏)`);
32
- }
33
- return null;
28
+ return formatGate;
34
29
  }
35
30
  const LOCK_WAIT_BASE_MS = 15; // 指数退避起始重试间隔
36
31
  const LOCK_WAIT_MAX_MS = 150; // 指数退避上限
package/src/store.js CHANGED
@@ -259,7 +259,14 @@ export async function checkBrainShape(root) {
259
259
  if (next2 === cur) return SKIP;
260
260
  changed = r.fixed.length ? r.fixed : ['结构按标准重排'];
261
261
  return { text: next2 };
262
- }).catch(() => {});
262
+ }).catch((e) => {
263
+ // 不能吞:load 是开机第一入口,静默跳过会让人以为库是健康的。
264
+ // 典型触发:文件里有孤儿子任务(真错,不修而报)。
265
+ // 只报**具体问题行**(以 `·` 开头的那些),不把整个错误头拼进去。
266
+ const msg = e && e.message ? e.message : '未知错误';
267
+ const detail = msg.split('\n').filter((x) => x.trim().startsWith('·')).map((x) => x.trim().slice(1).trim());
268
+ warn.push(`${file}: 未能自动归一 —— ${detail.length ? detail.join('; ') : msg.split('\n')[0]}`.slice(0, 200));
269
+ });
263
270
  if (changed.length) fixed.push(`${file}: ${changed.join('; ')}`);
264
271
  if (!knownH1) warn.push(`${file}: 标题非标准(读到 "${clip(l1, 24) || '(空)'}")`);
265
272
  }
@@ -315,7 +322,10 @@ export async function cmdLoad({ dir }) {
315
322
  ...shape.fixed.map((a) => ` ✓ 已补: ${a}`),
316
323
  ...shape.warn.map((a) => ` ⚠ ${a}`),
317
324
  ];
318
- if (shape.warn.length) rows.push(' (无头文件不自动改:结构可能整体脱轨,请手工对齐 .brain/ 模板)');
325
+ // 那句提示只适用于「标题非标准」:其他 warn(如格式不合规)说这句会误导。
326
+ if (shape.warn.some((a) => a.includes('标题非标准'))) {
327
+ rows.push(' (无头文件不自动改:结构可能整体脱轨,请手工对齐 .brain/ 模板)');
328
+ }
319
329
  sections.push('--- 文件形状核对 ---', ...rows, '');
320
330
  }
321
331
  // 未设姓名/姓名是占位名时开场就提醒 —— load 是开机第一屏,不在这里提,
@@ -986,7 +996,6 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
986
996
  let changed = false;
987
997
  let moved = null;
988
998
  const kept = [];
989
- const leftBehind = [];
990
999
  const wantId = String(id).replace(/\u200b/g, '');
991
1000
  for (let i = 0; i < lines.length; i++) {
992
1001
  const l = lines[i];
@@ -1012,15 +1021,9 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
1012
1021
  const head = withDoneKind(`${base}${tail} ${parts.join(' ')}`, kind);
1013
1022
  const bp = [];
1014
1023
  const leftover = [];
1015
- // 子项搬运:已勾选的随任务进 Done;**未勾选的不搬**,留在 Todo 区。
1016
- //
1017
- // 为何(2026-10-18 实测):Done 区被 collapseDone 折叠成计数,搬进去即「看板里
1018
- // 消失」。实测 factory-docs-slim 就带着「待做六链」的 `- [ ]` 落地,那条待办
1019
- // 此后无处可寻,同时父任务盖着【落地】——看板与事实矛盾。
1020
- //
1021
- // 为何不是硬拦(A 方案被否):`abs todo note` 写的子项与断点记录**同形**(都是
1022
- // ` - [ ] …`),无法从字段上区分「待办」与「过程记录」;硬拦会把所有带断点的
1023
- // 任务一起拦住(实测弄挂两条既有测试)。改为「不搬 + 点名」,信息不丢且不误伤。
1024
+ // 子项搬运:**已勾选**的随任务进 Done;未勾选的不搬(下方直接拒)。
1025
+ // 已勾的必须搬:否则任务进 Done 了、子项剩在原区,就是无主孤儿行,
1026
+ // 重建分区时会被清掉(历史上就是这么丢过数据)。
1024
1027
  while (i + 1 < lines.length) {
1025
1028
  const nx = lines[i + 1];
1026
1029
  if (!nx.trim()) break;
@@ -1068,15 +1071,11 @@ async function markDone(file, id, kind = '落地', conclusion, bySid = '') {
1068
1071
  }
1069
1072
  if (!changed || !moved) return SKIP;
1070
1073
  // 归位 + 按日期分组:insertDoneGrouped 一次重建 Done 区(新日期在前,未标日期归尾)
1071
- return { text: insertDoneGrouped(kept.join('\n'), moved), leftBehind };
1074
+ return { text: insertDoneGrouped(kept.join('\n'), moved) };
1072
1075
  });
1073
1076
  return res === SKIP
1074
1077
  ? `[NO_MATCH] 未找到含 "${id}" 的未完成任务行`
1075
- : `✓ 已完成并归位 Done: ${id} 【${kind}】` +
1076
- (res.leftBehind?.length
1077
- ? `\n ⚠ ${res.leftBehind.length} 条未勾子项留在 Todo 区(未跟入 Done,仍可在看板看到):\n` +
1078
- res.leftBehind.map((p) => ` - [ ] ${p}`).join('\n')
1079
- : '');
1078
+ : `✓ 已完成并归位 Done: ${id} 【${kind}】`;
1080
1079
  }
1081
1080
 
1082
1081
  // ---------- show: 查看 index/todo/log(只读面) ----------
package/src/todo/spec.js CHANGED
@@ -84,12 +84,16 @@ export const ENTRY_SHAPES = {
84
84
  // 摘要里不许带 Markdown 结构符(实测污染就是摘要取了页面正文开头)
85
85
  if (/^\s*-\s*\[\[[^\]]+\]\]\s*[—-]\s*#+\s/.test(l)) return '摘要不能带 `## ` 标题前缀(取页面首行标题了)';
86
86
  // ★ 2026-10-18 补:摘要正文本身必须是**纯文本一句话**。
87
- // 原规则只拦了 `## ` 前缀,实测下列全放行:摘要写成 `- 列表项`(变成嵌套列
88
- // 表)、```js(开代码围栏,把后面所有条目吞进去)、`| a | b |`(表格)。
89
- // index 是表,摘要是一列纯文本,不能携带任何块级结构。
87
+ // 原规则只拦了 `## ` 前缀,实测下列全放行:摘要写成 `- 列表项`(看着就像
88
+ // 另一条条目)、```js(开代码围栏把后面全吞)、`| a | b |`(表格)。
89
+ // index 是表,摘要是一列纯文本。
90
+ //
91
+ // 但别拦过头:摘要位置在 `- [[页名]] — ` 之后,是**一行中间**,
92
+ // `+ ` / `* ` / `> ` / `1. ` 在这里不会真渲染成块级结构,都是人话开头
93
+ // (实测放过一次「+ 一点补充」被误拒)。只拦真正会破坏解析的几种。
90
94
  const desc = l.replace(/^\s*-\s*\[\[[^\]]+\]\]\s*[—-]\s*/, '');
91
- if (/^(?:[-*+]\s|```|~~~|\||>\s|#{1,6}\s)/.test(desc)) {
92
- return '摘要必须以文字开头(不能是列表项/代码围栏/表格/引用/标题)';
95
+ if (/^(?:-\s|```|~~~|\|)/.test(desc) || /^#{1,6}\s/.test(desc)) {
96
+ return '摘要必须以文字开头(不能是列表项/代码围栏/表格/标题)';
93
97
  }
94
98
  if (desc.includes('```')) return '摘要不能含代码围栏 ```(会吞掉后续所有条目)';
95
99
  return null;
@@ -185,13 +185,21 @@ export function enforceBrainFormat(text, spec, prev) {
185
185
  const tLines = t.split('\n');
186
186
  const bad = checkFileShape(fname, t).filter(
187
187
  (it) => it.code !== 'STRAY-BLANK-LINE' &&
188
- !(prevSig !== null && prevSig.has(sigOfLine(tLines[it.line - 1] || ''))),
188
+ // ★ 孤儿缩进行**不吃存量豁免**(2026-10-18 用户要求)。
189
+ // 超长断点/奇怪条目放行存量是因为「没有安全的自动修法」;
190
+ // 而孤儿行是**确定性的坏数据**(子项没有父项,语义已丢),放行 = 让损坏持续。
191
+ // 宁可写不进去让人看见,也不要静默接受。
192
+ (it.code === 'TODO-ORPHAN-CHILD' ||
193
+ !(prevSig !== null && prevSig.has(sigOfLine(tLines[it.line - 1] || '')))),
189
194
  );
190
195
  if (bad.length) {
196
+ const onlyOrphan = bad.every((it) => it.code === 'TODO-ORPHAN-CHILD');
191
197
  throw new Error(
192
198
  `✗ ${fname} 格式不合规(写入被拒):\n` +
193
199
  bad.map((it) => ` · ${it.msg}`).join('\n') +
194
- `\n 出路:内容归入现有标签,或走对应入口(页 → concepts/ 等子目录 + index 登记;流水 → abs log;经验 → abs note)。`,
200
+ (onlyOrphan
201
+ ? `\n 出路:子任务必须挂在主任务下。给删掉的父任务行恢复一行任务行,或把那几条子项一起删掉。`
202
+ : `\n 出路:内容归入现有标签,或走对应入口(页 → concepts/ 等子目录 + index 登记;流水 → abs log;经验 → abs note)。`),
195
203
  );
196
204
  }
197
205
  };