@fanchao8609/agent_brain_sync 1.28.6 → 1.29.0
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/bin/abs.js +8 -41
- package/hooks/abs-rounded-editor/format.ts +77 -22
- package/hooks/abs-rounded-editor/index.ts +14 -19
- package/hooks/abs.omp.ts +1 -2
- package/hooks/abs.pi.ts +50 -51
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +68 -68
- package/src/brain/ingest.js +132 -54
- package/src/brain/layers.js +11 -5
- package/src/brain/load.js +50 -65
- package/src/brain/maintain.js +15 -2
- package/src/brain/summon.js +25 -16
- package/src/lint.js +82 -14
- package/src/note.js +7 -0
- package/src/store.js +1 -1
- package/src/text.js +42 -11
- package/src/todo/archive.js +22 -1
- package/src/todo/spec.js +6 -0
- package/src/todo/task-cmd.js +18 -0
- package/src/todo.js +1 -1
|
@@ -1,107 +1,107 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abs-agent-brain-sync
|
|
3
|
-
description: "USE FOR abs / agent-brain-sync
|
|
3
|
+
description: "USE FOR abs / agent-brain-sync cross-session memory and task handoff. Triggers: the user types `abs` alone or `abs` + a command (e.g. 'abs 开工', 'abs 沉淀'), or asks to 开工续接状态 / 沉淀会话收获 / 任务登记不丢 / 经验落成知识页 / 图谱体检."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# abs —
|
|
6
|
+
# abs — cross-session memory
|
|
7
7
|
|
|
8
|
-
`.brain/`
|
|
8
|
+
`.brain/` is your memory across sessions: bugs, decisions, verified fixes.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
**Querying it is your decision, not something you wait to be reminded of.** The moment a
|
|
11
|
+
keyword forms or a question appears ("was this bug solved before?", "who decided this?"),
|
|
12
|
+
search — do not wait for an error or for the user to prompt you.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Startup: one command
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
abs load
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
Returns: unfinished todos + usage + last 3 log lines. If the top shows `⏳ 上会话滞留`,
|
|
21
|
+
reconcile first — finished items get `abs todo done <id> --as 落地|否决|仅方案 "结语"`,
|
|
22
|
+
unfinished ones get `abs todo note <id> --note "where it stopped"`.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
**Never `cat .brain/` end-to-end.** Search by topic; `abs resolve <page>` then read that one page.
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Format gates (reject, do not guess)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
- `todo start/add` without `--note "what"` → rejected. One sentence, verb-first, ≤40 chars.
|
|
29
|
+
- `todo done` without a conclusion → rejected. Every Done line must say what happened.
|
|
30
|
+
- `abs log` without a one-line conclusion → rejected.
|
|
31
|
+
- index rows without a real description (date-template only) → rejected by lint.
|
|
32
|
+
- Sub-items are one sentence each. Reports go to `abs log` / `sources/`, not the board.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
后者只给页名+摘要更省 token。MCP 侧分别对应 `abs_search` / `abs_query`。
|
|
34
|
+
## Find knowledge: three tiers
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
| Situation | Command |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Stuck, no keywords | `abs recall [词…]` — infers from current work |
|
|
39
|
+
| Have keywords, want best recall | `abs search <词>` — luminance + neighbor spread, cross-project |
|
|
40
|
+
| Want exact literal, short output | `abs query <词…>` — multi-word OR |
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
`search` and `query` are detailed vs short, not duplicates. MCP: `abs_search` / `abs_query`.
|
|
43
|
+
Got a page name? `abs resolve <页名>` → read only that page. Cite sources as `[[页面名]]`.
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| 一句经验,还没想清 | `sources/` 待提炼 | `abs note "一句话"` |
|
|
43
|
-
| 要成页(有骨架好填) | `concepts/` | `abs concept <slug> --title "…"` |
|
|
44
|
-
| 一句话记下,日期即时间线 | `concepts/` | `abs capture --layer 2/3/4 "…"` |
|
|
45
|
-
| 违反就必须停下的硬约束 | index 的 `## Rules` | `abs rule add "≤42 字"` |
|
|
45
|
+
## Write knowledge: three sinks, do not mix
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
| What you have | Sink | Command |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| One experience, not yet clear | `sources/` | `abs note "一句话"` |
|
|
50
|
+
| A real page (4-section skeleton) | `concepts/` | `abs concept <slug> --title "…"` |
|
|
51
|
+
| One line, date as timeline | `concepts/` | `abs capture --layer 2/3/4 "…"` |
|
|
52
|
+
| Hard rule that must stop the agent | index `## Rules` | `abs rule add "≤42 字"` |
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
`concept` = four sections (trigger / symptom / fix / verify). `capture` = one line + date.
|
|
55
|
+
Then run `abs lint` (dead links, over-size, NO-TAIL).
|
|
51
56
|
|
|
52
|
-
##
|
|
57
|
+
## Tasks: the board is alive
|
|
53
58
|
|
|
54
59
|
```bash
|
|
55
|
-
abs todo
|
|
56
|
-
abs todo note <id> --note "到哪一步" #
|
|
60
|
+
abs todo start <id> --note "做什么" # register (note required)
|
|
61
|
+
abs todo note <id> --note "到哪一步" # append child (one sentence)
|
|
57
62
|
abs todo done <id> --as 落地|否决|仅方案 "结语"
|
|
58
63
|
abs todo state <id> --note 进行中|讨论中|滞留中|搁置
|
|
59
|
-
abs todo remove <id> #
|
|
64
|
+
abs todo remove <id> # delete incl. children, no undo
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
- `搁置` =
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
## 触发器:意图,不是关键词
|
|
67
|
+
- Register before touching files. A rough id is fine (`rename` later).
|
|
68
|
+
- `done` immediately when a unit of work finishes; do not batch at session end.
|
|
69
|
+
- One `[进行中]` at a time. `搁置` = parked on the board; `remove` = never existed.
|
|
70
|
+
- `memo` for extra notes; it does not occupy the board.
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
## Triggers are intent, not keywords
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
`todo` / `沉淀` / `load` / `note` in a sentence that is discussion, a question, or
|
|
75
|
+
rule-making → respond only, do not execute.
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
Exception: the user types `abs` alone or `abs` + command → execute; no subcommand = startup.
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
2. 不写没验证过的经验和总结。
|
|
78
|
-
3. 层次分明,按前因后果组织,不用自造词。
|
|
79
|
-
4. 禁止反问、猜测。
|
|
80
|
-
5. 拒绝重复,已有文档写过的优先引用。
|
|
81
|
-
6. 禁令直接了当,不唠叨。
|
|
79
|
+
## Document diet (todo / knowledge pages)
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
1. Everything serves the topic; cut the rest.
|
|
82
|
+
2. No unverified summaries. Only what was done, run, tested.
|
|
83
|
+
3. Ordered, layered, plain declarative sentences. No coined words.
|
|
84
|
+
4. No rhetorical questions, no guessing.
|
|
85
|
+
5. No duplication — cite the other page instead.
|
|
86
|
+
6. Bans are direct, not chatty.
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
2. 禁止通读 `.brain/` —— 要状态用 `abs load`,要主题用检索,要单页用 `abs resolve`。
|
|
87
|
-
3. 禁止删知识页表达"已失效" —— 用 `abs supersede <页> --by <新页>`。
|
|
88
|
-
4. 禁止直写 Rules —— 先提 `[Rules 提议]` 问用户,确认后 `abs rule add`。
|
|
89
|
-
5. 禁止用宿主原生 todo 承载跨会话任务 —— 用 `abs todo`。
|
|
90
|
-
6. 禁止编造没跑过的验证结论。
|
|
88
|
+
## ⛔ Never
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
1. Hand-build skeletons or hand-register tasks — `abs init` / `abs todo start`.
|
|
91
|
+
2. Read `.brain/` end-to-end — `abs load` for state, search for topics, resolve for one page.
|
|
92
|
+
3. Delete a knowledge page to mark it obsolete — `abs supersede <页> --by <新页>`.
|
|
93
|
+
4. Write Rules directly — propose `[Rules 提议]`, wait for the user, then `abs rule add`.
|
|
94
|
+
5. Host-native todo for cross-session work — use `abs todo`.
|
|
95
|
+
6. Invent verification results that were not run.
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
## Details live in the graph, not in memory
|
|
95
98
|
|
|
96
|
-
|
|
|
99
|
+
| Need | Read |
|
|
97
100
|
|---|---|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
| `.brain/`
|
|
102
|
-
|
|
|
103
|
-
|
|
104
|
-
## 自我约束
|
|
101
|
+
| Seven layers, `--kind`, deep wrap-up | `abs resolve abs-skill-details` |
|
|
102
|
+
| Session page shapes, ingest hooks | `abs resolve session-digest-design` |
|
|
103
|
+
| New-request protocol (plan → register → start) | `abs resolve abs-skill-details` |
|
|
104
|
+
| `.brain/` layout, frontmatter | `abs resolve abs-skill-details` |
|
|
105
|
+
| Lint codes (NO-INBOUND / NO-TAIL / …) | `abs resolve abs-skill-details` |
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
双链、frontmatter、index 必须自洽 —— 坏链等于掰断接力棒。
|
|
107
|
+
Only read/write `.brain/` and target code. Broken dual-links = broken handoff.
|
package/src/brain/ingest.js
CHANGED
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
// 判断仍是 agent 的 —— 这里不猜语义,只把会话里**已经存在的**事实整理成页。
|
|
13
13
|
//
|
|
14
14
|
// 双向挂钩(用户 2026-11-05 要求「资料要与 pi 的会话挂钩」):
|
|
15
|
-
// 页 →
|
|
16
|
-
// 会话 →
|
|
17
|
-
//
|
|
15
|
+
// 页 → 会话:段内的 `挂钩: session=… session-file=… project=…` 一行
|
|
16
|
+
// 会话 → 页:段标题 `## 会话 <会话 id 前 8 位>`,拿会话文件就能在当天页里反查
|
|
17
|
+
// 有没有被总结过(summon 的 md5 账本同理)。
|
|
18
18
|
// 为什么不把「页路径」回写进 jsonl:pi 的会话文件是宿主的产物,
|
|
19
19
|
// 往里加字段既越界又会让 md5 变化,导致账本把已总结的会话又当成「有更新」。
|
|
20
|
+
//
|
|
21
|
+
// 落点为何是「当天的 log-<日期>.md」而不是「一场一张 session-<日期>-s<id8>.md」
|
|
22
|
+
// (2026-11-08 用户定):同一天多场会话会散成多个文件,而且 todo.md 的归档行用
|
|
23
|
+
// `[[log-<日期>]]`、这里用 `[[session-…]]`,同一个东西两套名字。
|
|
24
|
+
// 用户原话:「生成的文件标签要一致」「这些乱名称 不标准的名称……以后绝对不允许出现」。
|
|
25
|
+
// 现约定:sessions/ 只收 `log-<日期>[-<slug>].md`,lint 的 SESSIONS-NAMING 强制。
|
|
20
26
|
|
|
21
27
|
import { promises as fs } from 'node:fs';
|
|
22
28
|
import { join, basename } from 'node:path';
|
|
@@ -26,6 +32,8 @@ import { requireUser, atTag } from '../userconfig.js';
|
|
|
26
32
|
import { today } from '../todo.js';
|
|
27
33
|
import { registerInIndex } from '../note.js';
|
|
28
34
|
import { clip } from '../text.js';
|
|
35
|
+
import { editFile, SKIP } from '../lock.js';
|
|
36
|
+
import { DIR_LAYER } from './layers.js';
|
|
29
37
|
import { digestSession, sessionsRoot, projectDirName, updateLedger, markSummarized, ledgerPath, md5Of } from './summon.js';
|
|
30
38
|
|
|
31
39
|
/** 会话页的三种形状。形状由「改了什么东西」机械判定,不让模型猜。 */
|
|
@@ -155,39 +163,103 @@ function usableHeadline(u) {
|
|
|
155
163
|
return true;
|
|
156
164
|
}
|
|
157
165
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
/** 会话段的标题:`## 会话 <会话 id 前 8 位>`。
|
|
167
|
+
* 这就是「会话 → 段」的反查凭据 —— 拿会话文件在当天页里搜这行即可。 */
|
|
168
|
+
export function digestSectionOf(short) {
|
|
169
|
+
return `## 会话 ${short || 'unknown'}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 当天页的骨架。会话摘要并进来后,一页就是「这一天的一切」:
|
|
173
|
+
* 多个 `## 会话 <id8>` 段 + `## 📦 任务归档` 段(后者由 abs todo archive 追写)。 */
|
|
174
|
+
export function renderDayPage({ ids, who, section }) {
|
|
175
|
+
const day = ids.day || today();
|
|
176
|
+
return [
|
|
164
177
|
'---',
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
'
|
|
168
|
-
`kind: session-digest`,
|
|
178
|
+
'tags: [session-log, session-digest, 会话]',
|
|
179
|
+
`layer: ${DIR_LAYER.sessions}`,
|
|
180
|
+
'kind: session-digest',
|
|
169
181
|
`author: ${who}`,
|
|
170
|
-
`updated: ${
|
|
182
|
+
`updated: ${day}`,
|
|
171
183
|
'status: draft',
|
|
172
|
-
// ── 与 pi 会话的挂钩(三行缺一不可)──
|
|
173
|
-
// session: pi 的会话 id(首行 type:session 的 id 字段),唯一且不随文件名变
|
|
174
|
-
// session-file: 会话 jsonl 的文件名,能直接定位到文件
|
|
175
|
-
// project: 会话发生时的 cwd,跨项目时靠它知道上下文属于哪
|
|
176
|
-
`session: ${ids.id}`,
|
|
177
|
-
`session-file: ${ids.name}`,
|
|
178
|
-
`project: ${digest.cwd || ''}`,
|
|
179
184
|
'---',
|
|
180
185
|
'',
|
|
181
|
-
`#
|
|
186
|
+
`# ${day} 记录`,
|
|
187
|
+
'',
|
|
188
|
+
'> 本页 = 该日的一切:会话摘要(一场一段 `## 会话 <id8>`)+ 从 `todo.md` Done 区迁出的任务明细。',
|
|
189
|
+
'',
|
|
190
|
+
section.replace(/\s*$/, ''),
|
|
191
|
+
'',
|
|
192
|
+
].join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 已有当天页被并入会话摘要时,补上「这页含会话摘要」的 frontmatter 标记。
|
|
196
|
+
*
|
|
197
|
+
* 为何必须补(2026-11-05 实测的检索保护):空骨架会话段有高词频、正文又短,
|
|
198
|
+
* bm25 天然占优 —— 实测 20 张空骨架把真知识页全挤出结果。保护靠两条,
|
|
199
|
+
* 两条都只认 `kind: session-digest`:db 的 unfilled 列(整页排除出检索)
|
|
200
|
+
* 与 0.5 亮度折扣。缺 kind 就等于这层保护失效。
|
|
201
|
+
* 已存在的 tags/status/updated 一律不动 —— 那是那天的记录,机器不替它改。 */
|
|
202
|
+
export function ensureDigestFrontmatter(body) {
|
|
203
|
+
const t = String(body || '');
|
|
204
|
+
if (!t.startsWith('---\n')) return t;
|
|
205
|
+
const end = t.indexOf('\n---', 3);
|
|
206
|
+
if (end === -1) return t;
|
|
207
|
+
const fm = t.slice(4, end);
|
|
208
|
+
const add = [];
|
|
209
|
+
if (!/^kind:\s/m.test(fm)) add.push('kind: session-digest');
|
|
210
|
+
if (!/^layer:\s/m.test(fm)) add.push(`layer: ${DIR_LAYER.sessions}`);
|
|
211
|
+
if (!add.length) return t;
|
|
212
|
+
return `---\n${fm.replace(/\s*$/, '')}\n${add.join('\n')}${t.slice(end)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 把会话段并进当天页。位置:`## 📦 任务归档` 之前(会话在前、归档在后);
|
|
216
|
+
* 没有归档段就放到 `## 关联连接` 之前;都没有就追加到末尾。 */
|
|
217
|
+
export function insertDigestSection(body, section) {
|
|
218
|
+
const t = String(body || '');
|
|
219
|
+
const lines = t.split('\n');
|
|
220
|
+
const at = ['## 📦 任务归档', '## 关联连接']
|
|
221
|
+
.map((h) => lines.findIndex((l) => l.trim() === h))
|
|
222
|
+
.filter((i) => i !== -1)
|
|
223
|
+
.sort((a, b) => a - b)[0];
|
|
224
|
+
const block = section.replace(/\s*$/, '');
|
|
225
|
+
if (at === undefined) return `${t.replace(/\s*$/, '')}\n\n${block}\n`;
|
|
226
|
+
const head = lines.slice(0, at).join('\n').replace(/\s*$/, '');
|
|
227
|
+
const tail = lines.slice(at).join('\n').replace(/\s*$/, '');
|
|
228
|
+
return `${head}\n\n${block}\n\n${tail}\n`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `--force`:把已有那一段重建回空骨架(段内已填的结论会被覆盖,调用方要先警告)。 */
|
|
232
|
+
export function replaceDigestSection(body, section, short) {
|
|
233
|
+
const t = String(body || '');
|
|
234
|
+
const lines = t.split('\n');
|
|
235
|
+
const i = lines.findIndex((l) => l.trim() === digestSectionOf(short));
|
|
236
|
+
if (i === -1) return insertDigestSection(t, section);
|
|
237
|
+
let j = i + 1;
|
|
238
|
+
while (j < lines.length && !/^#{1,2}\s/.test(lines[j].trim())) j++;
|
|
239
|
+
return [...lines.slice(0, i), ...section.replace(/\s*$/, '').split('\n'), '', ...lines.slice(j)]
|
|
240
|
+
.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** 会话段的 markdown。值留空位 —— 内容由 agent 读会话后填。
|
|
244
|
+
* 段标题层级从 `##` 降为 `###`:外面套了一层 `## 会话 <id8>`,一天多场不会撞标题。 */
|
|
245
|
+
export function renderDigestSection({ shape, digest, ids, who }) {
|
|
246
|
+
const sh = SHAPES[shape];
|
|
247
|
+
const files = [...digest.filesEdited.keys()];
|
|
248
|
+
const head = headlineOf(digest);
|
|
249
|
+
const body = [
|
|
250
|
+
digestSectionOf(ids.short),
|
|
182
251
|
'',
|
|
183
252
|
`> ${ids.day} · ${sh.label} · 用户发言 ${digest.users.length} 句 · 工具调用 ${digest.toolCalls} 次`,
|
|
253
|
+
`> 会话:${head || ids.day}`,
|
|
254
|
+
`> 挂钩: session=${ids.id} · session-file=${ids.name} · project=${digest.cwd || ''}`,
|
|
255
|
+
`> 沉淀者: ${who}`,
|
|
184
256
|
'',
|
|
185
257
|
];
|
|
186
258
|
for (const [title, hint] of sh.sections) {
|
|
187
|
-
body.push(
|
|
259
|
+
body.push(`### ${title}`, `<!-- ${hint} -->`, '');
|
|
188
260
|
}
|
|
189
261
|
if (files.length) {
|
|
190
|
-
body.push('
|
|
262
|
+
body.push('### 改过的文件');
|
|
191
263
|
for (const f of files.slice(0, 40)) {
|
|
192
264
|
body.push(`- \`${f}\` ×${digest.filesEdited.get(f)}`);
|
|
193
265
|
}
|
|
@@ -195,7 +267,7 @@ export function renderPage({ shape, digest, ids, who }) {
|
|
|
195
267
|
body.push('');
|
|
196
268
|
}
|
|
197
269
|
body.push(
|
|
198
|
-
'
|
|
270
|
+
'### 用户原话',
|
|
199
271
|
'<!-- 从会话文件提取的原始诉求,按发生顺序。写上面几段时以这里为依据 -->',
|
|
200
272
|
);
|
|
201
273
|
for (const u of digest.users.slice(0, 20)) {
|
|
@@ -204,8 +276,8 @@ export function renderPage({ shape, digest, ids, who }) {
|
|
|
204
276
|
if (digest.users.length > 20) body.push(`- …另有 ${digest.users.length - 20} 句`);
|
|
205
277
|
body.push(
|
|
206
278
|
'',
|
|
207
|
-
'
|
|
208
|
-
`- ${atTag(who)} —
|
|
279
|
+
'### 关联连接',
|
|
280
|
+
`- ${atTag(who)} — 本场沉淀者`,
|
|
209
281
|
'(把本场会话改到的知识页挂在这里,双向可回溯)',
|
|
210
282
|
'',
|
|
211
283
|
);
|
|
@@ -295,40 +367,46 @@ export async function cmdIngest({ dir, file, force = false, mark = false, last =
|
|
|
295
367
|
const sessOut = brainPath(root, 'sessions');
|
|
296
368
|
await fs.mkdir(sessOut, { recursive: true });
|
|
297
369
|
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
const
|
|
370
|
+
// 落页位置 = 当天的 `log-<日期>.md`(2026-11-08 用户定「一天一文件」)。
|
|
371
|
+
// 会话 → 段的凭据是段标题 `## 会话 <id8>`:拿会话文件能在当天页里搜到它。
|
|
372
|
+
const short = ids.short || 'unknown';
|
|
373
|
+
const slug = `log-${ids.day || today()}`;
|
|
301
374
|
const dst = join(sessOut, `${slug}.md`);
|
|
302
375
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
376
|
+
// 幂等判据从「文件存在吗」改成「这一天页里已有这场会话的段吗」——
|
|
377
|
+
// 一天一文件后,同一天多场会话挂在同一个文件里,文件粒度不区分场次。
|
|
378
|
+
// 读-改-写必须走 editFile 锁:abs todo archive 也在写同一个文件,
|
|
379
|
+
// 裸 readFile+rename 会让两边互相覆盖(archive.js 的同一坑,已修)。
|
|
380
|
+
let existed = false;
|
|
381
|
+
await editFile(dst, (cur) => {
|
|
382
|
+
const has = String(cur || '').split('\n').some((l) => l.trim() === digestSectionOf(short));
|
|
383
|
+
if (has && !force) { existed = true; return SKIP; }
|
|
384
|
+
const section = renderDigestSection({ shape, digest, ids, who });
|
|
385
|
+
let text;
|
|
386
|
+
if (cur == null) text = renderDayPage({ ids, who, section });
|
|
387
|
+
else if (has) text = replaceDigestSection(cur, section, short); // --force:只重建这一段
|
|
388
|
+
else text = insertDigestSection(ensureDigestFrontmatter(cur), section);
|
|
389
|
+
return { text };
|
|
390
|
+
});
|
|
391
|
+
if (existed) {
|
|
392
|
+
// 提示写得直白(2026-11-05 实测的收益要保住):开发时自己就误用 --force
|
|
393
|
+
// 覆盖掉填好的段。会丢的是「三段结论 + 关联连接」—— 重建回来的只是空骨架 + 素材。
|
|
394
|
+
return [
|
|
395
|
+
`• 这一天页已有本场会话段,不覆盖 → .brain/sessions/${slug}.md(## 会话 ${short})`,
|
|
396
|
+
' --force 会把这一段重建回空骨架:三段结论与关联连接全部丢失,只剩素材。',
|
|
397
|
+
' 只想要素材、保留结论 → 直接读那一段;确定要重填再用 --force。',
|
|
398
|
+
].join('\n');
|
|
399
|
+
}
|
|
316
400
|
|
|
317
|
-
const body = renderPage({ shape, digest, ids, who });
|
|
318
401
|
// 骨架段里的 <!-- 提示 --> 占位**故意保留**:它告诉填页的人每段该写什么。
|
|
319
402
|
// 它不会干扰「填没填」的判定 —— load 的 digestFilled 与 lint 的 hasTail
|
|
320
403
|
// 都把占位注释当空内容(2026-11-05 审查:原注释声称「去掉注释」但代码没做,
|
|
321
|
-
//
|
|
322
|
-
// tmp + rename 原子落盘(并发读不会读到半写文件)
|
|
323
|
-
const tmp = join(sessOut, `.${slug}.md.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
|
|
324
|
-
await fs.writeFile(tmp, body, 'utf8');
|
|
325
|
-
await fs.rename(tmp, dst);
|
|
326
|
-
|
|
404
|
+
// 注释与实现不符已改正)。写盘由 editFile 锁内完成,并发读不会读到半写文件。
|
|
327
405
|
await registerInIndex(root, 'Sessions', slug, `会话:${headlineOf(digest) || ids.day}`);
|
|
328
406
|
|
|
329
407
|
// 不写 log.md(2026-11-05,沿用 abs concept 的既有判据):
|
|
330
|
-
//
|
|
331
|
-
//
|
|
408
|
+
// 「并入了一场会话摘要」是**命令的副作用**,不是工作成果 —— 零信息量,
|
|
409
|
+
// 而「这一天页存在」已由 registerInIndex 落在 index 的 Sessions 区。
|
|
332
410
|
// 更要紧的是:--auto 在每次关会话时跑,一天开 10 次就刷 10 行流水,
|
|
333
411
|
// 靠事后清理治不了(abs concept 当初正是因为「建 10 页 = 10 行噪声」才删掉这次调用的)。
|
|
334
412
|
|
|
@@ -366,11 +444,11 @@ export async function cmdIngest({ dir, file, force = false, mark = false, last =
|
|
|
366
444
|
|
|
367
445
|
const sh = SHAPES[shape];
|
|
368
446
|
return [
|
|
369
|
-
`✓
|
|
447
|
+
`✓ 会话摘要 → .brain/sessions/${slug}.md(${digestSectionOf(short)})`,
|
|
370
448
|
` ${ids.day} · ${sh.label}(判据: ${sh.when})`,
|
|
371
449
|
` 用户 ${digest.users.length} 句 · 工具 ${digest.toolCalls} 次 · 改过 ${digest.filesEdited.size} 个文件`,
|
|
372
450
|
` 挂钩: session=${ids.id} file=${hit}`,
|
|
373
|
-
'
|
|
451
|
+
' 骨架已给好,三段内容要读会话后填;填完段内写实、别再留 <!-- 提示 -->。',
|
|
374
452
|
].join('\n') + markNote;
|
|
375
453
|
}
|
|
376
454
|
|
package/src/brain/layers.js
CHANGED
|
@@ -131,7 +131,7 @@ export function layerConflict(body, dirName) {
|
|
|
131
131
|
export const DIGEST_SECTIONS = ['出了什么问题', '怎么解决的', '验证什么命令',
|
|
132
132
|
'做了什么操作', '结果怎样', '下次注意事项', '谈成了什么', '没结论的地方'];
|
|
133
133
|
|
|
134
|
-
/**
|
|
134
|
+
/** 会话摘要的骨架段里有没有任何一段写了真内容。
|
|
135
135
|
*
|
|
136
136
|
* 为何要这个判据(2026-11-05 实测):`ingest --auto` 每次关会话都落一张空骨架,
|
|
137
137
|
* 而空骨架的「用户原话」段有高词频、正文又短 —— bm25 天然占优。
|
|
@@ -141,15 +141,21 @@ export const DIGEST_SECTIONS = ['出了什么问题', '怎么解决的', '验证
|
|
|
141
141
|
*
|
|
142
142
|
* 判据:骨架段内除占位注释与空行外还有没有真内容。
|
|
143
143
|
* 不看 status —— 人可能填完忘了改 active,用 status 会漏判。
|
|
144
|
-
* 只看骨架段,不数「用户原话/改过的文件」—— 那两段是素材,不是填写成果。
|
|
144
|
+
* 只看骨架段,不数「用户原话/改过的文件」—— 那两段是素材,不是填写成果。
|
|
145
|
+
*
|
|
146
|
+
* 段标题层级不写死(2026-11-08):摘要现在并进当天的 `log-<日期>.md`,
|
|
147
|
+
* 骨架段降为 `###`(`## 会话 <id8>` 是容器)。同一段名叫 `##` 或 `###` 都算,
|
|
148
|
+
* 段的结束判据改成「遇到同级或更高级的标题」。 */
|
|
145
149
|
export function digestFilled(body) {
|
|
146
150
|
const lines = String(body || '').split('\n');
|
|
147
151
|
for (let i = 0; i < lines.length; i++) {
|
|
148
|
-
const h = lines[i].match(
|
|
149
|
-
if (!h || !DIGEST_SECTIONS.includes(h[
|
|
152
|
+
const h = lines[i].match(/^(#{2,4})\s+(.+?)\s*$/);
|
|
153
|
+
if (!h || !DIGEST_SECTIONS.includes(h[2].trim())) continue;
|
|
154
|
+
const lvl = h[1].length;
|
|
150
155
|
for (let j = i + 1; j < lines.length; j++) {
|
|
151
156
|
const t = lines[j].trim();
|
|
152
|
-
|
|
157
|
+
const hh = t.match(/^(#+)\s/);
|
|
158
|
+
if (hh && hh[1].length <= lvl) break; // 同级/更高级标题 = 本段结束
|
|
153
159
|
if (!t) continue;
|
|
154
160
|
if (t.startsWith('<!--') || t === '-->') continue; // 占位注释不算填
|
|
155
161
|
return true;
|