-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathmemory-adoption.html
More file actions
754 lines (672 loc) · 55.1 KB
/
Copy pathmemory-adoption.html
File metadata and controls
754 lines (672 loc) · 55.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>对标之后抄什么:四条做法的适配方案</title>
<style>
:root{
--bg:#16150f; --pan:#1d1c14; --bd:#2e2c20; --tx:#bdb9a6; --acc:#d4843a;
--cb:#12110b; --dim:#8a8675;
--c0:#4f8ef7; --c1:#5aad4e; --c2:#d4843a; --c3:#9d6fe0; --c4:#2db3d5;
--no:#e0445a;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--tx);
font:14px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:40px 24px 96px}
.wrap{max-width:1180px;margin:0 auto}
h1{font-size:23px;color:#f0eee6;margin:0 0 6px}
.sub{color:var(--tx);margin:0 0 18px;max-width:78ch}
.sub a{color:var(--acc)}
h2{font-size:16px;color:var(--acc);margin:34px 0 8px;scroll-margin-top:20px}
h2 .num{color:var(--dim);font-size:12px;margin-right:8px;letter-spacing:.16em}
h2 .verd{font-size:10px;letter-spacing:.18em;border:1px solid;border-radius:99px;
padding:2px 8px;margin-left:9px;vertical-align:2px;font-weight:400}
.vno{color:var(--no);border-color:var(--no)}
.vmod{color:var(--c2);border-color:var(--c2)}
.vyes{color:var(--c1);border-color:var(--c1)}
.lead{color:var(--dim);font-size:13px;margin:0 0 12px;max-width:82ch}
.card{background:var(--pan);border:1px solid var(--bd);border-radius:12px;padding:8px;margin-bottom:16px}
.card.scroll{overflow-x:auto}
.card.scroll svg{min-width:900px}
svg{display:block;width:100%;height:auto;background:var(--cb);border-radius:8px}
.legend{font-size:12.5px;color:var(--dim);padding:8px 10px;line-height:1.8}
.legend b{color:#f0eee6}
code{background:var(--cb);color:var(--acc);padding:1px 5px;border-radius:4px;font:12px Menlo,monospace}
text{font-family:-apple-system,"Segoe UI",sans-serif}
.toc{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 26px}
.toc a{font-size:12px;color:var(--tx);text-decoration:none;border:1px solid var(--bd);
background:var(--pan);border-radius:99px;padding:4px 12px}
.toc a:hover{border-color:var(--acc);color:#f0eee6}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px}
.st{background:var(--pan);border:1px solid var(--bd);border-top-width:3px;border-radius:12px;padding:13px 15px}
.st .k{font-size:10.5px;letter-spacing:.2em;color:var(--dim);margin-bottom:5px}
.st h3{margin:0 0 5px;font-size:15px;font-weight:700;color:#f0eee6}
.st p{margin:0;font-size:12.5px;color:#a5a190;line-height:1.65}
.st a{color:var(--acc)}
@media(max-width:820px){.grid3{grid-template-columns:1fr}}
.tbl{overflow-x:auto;background:var(--pan);border:1px solid var(--bd);border-radius:12px;margin-bottom:16px}
table{width:100%;border-collapse:collapse;font-size:13px;min-width:820px}
th{text-align:left;padding:10px 14px;color:var(--dim);font-weight:600;font-size:11px;
letter-spacing:.14em;border-bottom:1px solid var(--bd);white-space:nowrap}
td{padding:9px 14px;border-bottom:1px solid #26241a;vertical-align:top;line-height:1.6}
tr:last-child td{border-bottom:none}
td.y{color:var(--c1)} td.n{color:var(--no)} td.q{color:var(--dim)} td.h{color:var(--c2)}
td b{color:#f0eee6;font-weight:600}
td .src{display:block;font:11px Menlo,monospace;color:#6b6a63;margin-top:3px;word-break:break-all}
tr.us td{background:#141a12}
tr.us td:first-child{box-shadow:inset 3px 0 0 var(--c1)}
tr.plan td{background:#1a1622}
tr.plan td:first-child{box-shadow:inset 3px 0 0 var(--c3)}
.note{border-left:3px solid var(--bd);padding:2px 0 2px 13px;color:var(--dim);
font-size:12.5px;margin:0 0 16px;max-width:86ch;line-height:1.8}
.note b{color:#f0eee6}
.note a{color:var(--acc)}
.note.k{border-left-color:var(--acc)}
.note.p{border-left-color:var(--c3)}
.note.no{border-left-color:var(--no)}
.note code{font-size:11.5px}
</style>
</head>
<body>
<div class="wrap">
<h1>对标之后抄什么:四条做法的适配方案</h1>
<p class="sub">
<a href="memory-comparison.html">八个框架横向对比</a>挑出了几条别人做得比我们好的地方。
这一页只回答接下来的问题:放进我们的结构里,每一条该怎么落,或者为什么不落。
权威正文在<a href="overview.html">overview.md</a>,机制图在
<a href="memory-architecture.html">记忆子系统:机制图解</a>。
</p>
<div class="grid3">
<div class="st" style="border-top-color:var(--c1)">
<div class="k">第一层</div>
<h3>我们现在</h3>
<p>对着 <code>openprogram/memory/</code> 里跑着的代码核实过,行号照录。带实测的地方写出数字。</p>
</div>
<div class="st" style="border-top-color:var(--c0)">
<div class="k">第二层</div>
<h3>人家怎么做</h3>
<p>被抄的那家的原始形状,以及它为什么长成那样。形状不一定适用,动机通常适用。</p>
</div>
<div class="st" style="border-top-color:var(--c3)">
<div class="k">第三层</div>
<h3>我们打算</h3>
<p>放进我们结构后的方案,或者不放的理由。每条一个判决,没有"值得研究"这种结论。</p>
</div>
</div>
<div class="toc">
<a href="#board">00 四条判决</a>
<a href="#recall">01 检索不建索引</a>
<a href="#core">02 常驻块怎么裁</a>
<a href="#branch">03 离开的分支</a>
<a href="#cursor">04 游标兜底</a>
<a href="#land">05 改动落在哪</a>
</div>
<!-- ============================ 00 ============================ -->
<h2 id="board"><span class="num">00</span>四条判决</h2>
<p class="lead">先给结论。三条改造后采纳或直接采纳,一条否掉,理由都不是"不好",是我们的结构里那件事已经有别的东西在做,或者代价落在了错误的位置上。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 322" role="img" aria-label="四条借鉴做法的判决">
<text x="18" y="24" fill="#8a8675" font-size="11" letter-spacing="1.6">借来的做法</text>
<text x="470" y="24" fill="#8a8675" font-size="11" letter-spacing="1.6">出处</text>
<text x="620" y="24" fill="#8a8675" font-size="11" letter-spacing="1.6">判决</text>
<text x="790" y="24" fill="#8a8675" font-size="11" letter-spacing="1.6">一句话理由</text>
<line x1="18" y1="34" x2="1122" y2="34" stroke="#2e2c20"/>
<!-- lane 1 -->
<rect x="18" y="46" width="1104" height="58" rx="9" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="46" width="3" height="58" fill="#e0445a"/>
<text x="36" y="70" fill="#f0eee6" font-size="13" font-weight="600">检索不建索引:文件抬头写自述,模型从清单里挑五个</text>
<text x="36" y="90" fill="#8a8675" font-size="11.5">每轮检索多一次小模型调用,换掉索引</text>
<text x="470" y="78" fill="#4f8ef7" font-size="12">claude-code</text>
<rect x="620" y="63" width="72" height="24" rx="12" fill="none" stroke="#e0445a"/>
<text x="656" y="79" fill="#e0445a" font-size="12" text-anchor="middle">否掉</text>
<text x="790" y="72" fill="#bdb9a6" font-size="11.5">那次调用买的是"没有索引也能语义匹配",</text>
<text x="790" y="90" fill="#bdb9a6" font-size="11.5">我们有索引,粒度还比文件细</text>
<!-- lane 2 -->
<rect x="18" y="112" width="1104" height="58" rx="9" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="112" width="3" height="58" fill="#d4843a"/>
<text x="36" y="136" fill="#f0eee6" font-size="13" font-weight="600">常驻块按归属裁:只丢自动升上来的,人写的一律不动</text>
<text x="36" y="156" fill="#8a8675" font-size="11.5">并把丢掉的清单返回给调用方</text>
<text x="470" y="144" fill="#4f8ef7" font-size="12">openclaw</text>
<rect x="620" y="129" width="132" height="24" rx="12" fill="none" stroke="#5aad4e"/>
<text x="686" y="145" fill="#5aad4e" font-size="12" text-anchor="middle">改造后采纳 · 已落地</text>
<text x="790" y="138" fill="#bdb9a6" font-size="11.5">归属之所以要紧是因为丢等于销毁。</text>
<text x="790" y="156" fill="#bdb9a6" font-size="11.5">让 core.md 变成派生视图,丢就不再销毁</text>
<!-- lane 3 -->
<rect x="18" y="178" width="1104" height="58" rx="9" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="178" width="3" height="58" fill="#d4843a"/>
<text x="36" y="202" fill="#f0eee6" font-size="13" font-weight="600">离开一条分支时给它生成摘要,接在树上</text>
<text x="36" y="222" fill="#8a8675" font-size="11.5">被放弃的分支里说过的话不至于凭空消失</text>
<text x="470" y="210" fill="#4f8ef7" font-size="12">pi-mono</text>
<rect x="620" y="195" width="112" height="24" rx="12" fill="none" stroke="#d4843a"/>
<text x="676" y="211" fill="#d4843a" font-size="12" text-anchor="middle">改造后采纳</text>
<text x="790" y="204" fill="#bdb9a6" font-size="11.5">目的照收,摘要节点不要。会话边界枚举全部</text>
<text x="790" y="222" fill="#bdb9a6" font-size="11.5">分支即可,走到有标记的前缀就停</text>
<!-- lane 4 -->
<rect x="18" y="244" width="1104" height="58" rx="9" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="244" width="3" height="58" fill="#5aad4e"/>
<text x="36" y="268" fill="#f0eee6" font-size="13" font-weight="600">游标找不到时,把全部消息重新当成没写过</text>
<text x="36" y="288" fill="#8a8675" font-size="11.5">返回零条会让抽取彻底停摆,重做一遍的代价有限</text>
<text x="470" y="276" fill="#4f8ef7" font-size="12">claude-code</text>
<rect x="620" y="261" width="72" height="24" rx="12" fill="none" stroke="#5aad4e"/>
<text x="656" y="277" fill="#5aad4e" font-size="12" text-anchor="middle">采纳</text>
<text x="790" y="270" fill="#bdb9a6" font-size="11.5">迁移那条已覆盖一半,另一半没覆盖:</text>
<text x="790" y="288" fill="#bdb9a6" font-size="11.5">读不出来的账本现在会抛,然后无限重试</text>
</svg>
</div>
<!-- ============================ 01 ============================ -->
<h2 id="recall"><span class="num">01</span>检索不建索引<span class="verd vno">否掉</span></h2>
<p class="lead">claude-code的做法:每个记忆文件抬头写一句自述,检索时把这些描述列成清单,花一次便宜的小模型调用挑出五个文件名,全程不建索引。我们的检索是每轮都跑的BM25,跑在用户消息和模型调用之间。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 372" role="img" aria-label="检索三层对比">
<!-- headers -->
<rect x="14" y="14" width="356" height="24" rx="6" fill="#141a12"/>
<text x="26" y="31" fill="#5aad4e" font-size="11.5" letter-spacing="1.4">第一层 · 我们现在</text>
<rect x="392" y="14" width="356" height="24" rx="6" fill="#101722"/>
<text x="404" y="31" fill="#4f8ef7" font-size="11.5" letter-spacing="1.4">第二层 · claude-code</text>
<rect x="770" y="14" width="356" height="24" rx="6" fill="#211316"/>
<text x="782" y="31" fill="#e0445a" font-size="11.5" letter-spacing="1.4">第三层 · 我们打算</text>
<!-- col 1 -->
<rect x="14" y="52" width="356" height="34" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="74" fill="#f0eee6" font-size="12">用户发来一句话</text>
<path d="M192 86 L192 104" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="14" y="106" width="356" height="52" rx="7" fill="#1d1c14" stroke="#5aad4e"/>
<text x="30" y="126" fill="#f0eee6" font-size="12">provider.search() 每轮一次</text>
<text x="30" y="145" fill="#8a8675" font-size="11">retrieval/inspect.search,top_k=5,内部封顶10</text>
<path d="M192 158 L192 176" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="14" y="178" width="356" height="70" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="198" fill="#f0eee6" font-size="12">BM25索引,persist=False</text>
<text x="30" y="217" fill="#8a8675" font-size="11">每轮把 topics/ 和 sources/ 全量重新哈希、重新解析</text>
<text x="30" y="235" fill="#e0445a" font-size="11">耗时与工作区体积成正比,见下图</text>
<path d="M192 248 L192 266" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="14" y="268" width="356" height="52" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="288" fill="#f0eee6" font-size="12">五个段落,带块ID和脚注</text>
<text x="30" y="307" fill="#8a8675" font-size="11">直接围在 memory-context 里进提示词,零次模型调用</text>
<rect x="14" y="330" width="356" height="30" rx="7" fill="#141a12" stroke="#2e2c20"/>
<text x="30" y="350" fill="#5aad4e" font-size="11.5">同一轮里的额外模型往返:0</text>
<!-- col 2 -->
<rect x="392" y="52" width="356" height="34" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="408" y="74" fill="#f0eee6" font-size="12">每个记忆文件抬头一句自述</text>
<path d="M570 86 L570 104" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="392" y="106" width="356" height="52" rx="7" fill="#1d1c14" stroke="#4f8ef7"/>
<text x="408" y="126" fill="#f0eee6" font-size="12">把全部自述列成一张清单</text>
<text x="408" y="145" fill="#8a8675" font-size="11">没有倒排表,没有向量,没有缓存要维护</text>
<path d="M570 158 L570 176" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="392" y="178" width="356" height="70" rx="7" fill="#1d1c14" stroke="#4f8ef7"/>
<text x="408" y="198" fill="#f0eee6" font-size="12">一次便宜的小模型调用</text>
<text x="408" y="217" fill="#8a8675" font-size="11">判据比"相关就选"细:拿不准就不选,宁可返回空</text>
<text x="408" y="235" fill="#4f8ef7" font-size="11">findRelevantMemories.ts:18-24</text>
<path d="M570 248 L570 266" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a1)"/>
<rect x="392" y="268" width="356" height="52" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="408" y="288" fill="#f0eee6" font-size="12">五个文件名</text>
<text x="408" y="307" fill="#8a8675" font-size="11">agent 拿到名字,自己再 Read 一遍</text>
<rect x="392" y="330" width="356" height="30" rx="7" fill="#101722" stroke="#2e2c20"/>
<text x="408" y="350" fill="#4f8ef7" font-size="11.5">同一轮里的额外模型往返:1,串在用户等待里</text>
<!-- col 3 -->
<rect x="770" y="52" width="356" height="88" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="786" y="74" fill="#e0445a" font-size="12" font-weight="600">不引入这次调用</text>
<text x="786" y="94" fill="#bdb9a6" font-size="11">它换来的是"没有索引也能按意思找"。</text>
<text x="786" y="112" fill="#bdb9a6" font-size="11">我们已经有索引:BM25现成,语义那一路有</text>
<text x="786" y="130" fill="#bdb9a6" font-size="11">retrieval/embedding.py,都不占模型往返。</text>
<rect x="770" y="150" width="356" height="88" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="786" y="172" fill="#e0445a" font-size="12" font-weight="600">不引入文件自述</text>
<text x="786" y="192" fill="#bdb9a6" font-size="11">我们的返回单位是段落,不是文件。文件名</text>
<text x="786" y="210" fill="#bdb9a6" font-size="11">已经是自述:一个主题一个文件是写入契约,</text>
<text x="786" y="228" fill="#bdb9a6" font-size="11">topics/people/dave.md 不需要再解释一遍。</text>
<rect x="770" y="248" width="356" height="112" rx="7" fill="#1d1c14" stroke="#5aad4e"/>
<text x="786" y="270" fill="#5aad4e" font-size="12" font-weight="600">只留下它的一条判据</text>
<text x="786" y="290" fill="#bdb9a6" font-size="11">"拿不准就不选,宁可返回空"。我们现在的</text>
<text x="786" y="308" fill="#bdb9a6" font-size="11">下限是 lexical<=0 且 adjustment<=0.02 才丢,</text>
<text x="786" y="326" fill="#bdb9a6" font-size="11">等于"共享一个词就算命中",所以和记忆无关的</text>
<text x="786" y="344" fill="#bdb9a6" font-size="11">一轮照样会被塞五段。分数下限值得单独加。</text>
<defs>
<marker id="a1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#4a4736"/>
</marker>
</defs>
</svg>
<div class="legend">
<b>实测</b>:在这份工作区里给 <code>topics/</code> 插一段没有脚注的介绍性段落,严格解析直接拒绝
(<code>TopicFormatError: memory block ID required</code>),blockquote 同样被拒,只有HTML注释能过。
也就是说"抬头写一句自述"落到我们的格式里得先改解析契约,而换来的信息路径名已经给了。
</div>
</div>
<div class="card scroll">
<svg viewBox="0 0 1140 208" role="img" aria-label="每轮检索耗时随工作区体积增长">
<text x="18" y="26" fill="#f0eee6" font-size="12.5" font-weight="600">每轮检索的实测耗时(本机,合成工作区,三次取平均)</text>
<text x="18" y="46" fill="#8a8675" font-size="11">这一栏是"多一次模型调用"要挤进去的地方。它现在已经不宽裕,而且宽裕不起来的原因是本地解析,不是排序。</text>
<!-- axis -->
<line x1="196" y1="62" x2="196" y2="188" stroke="#2e2c20"/>
<line x1="196" y1="188" x2="1110" y2="188" stroke="#2e2c20"/>
<!-- bars: scale 1400ms -> 900px -->
<rect x="196" y="72" width="42" height="20" rx="3" fill="#5aad4e"/>
<text x="186" y="87" fill="#bdb9a6" font-size="11.5" text-anchor="end">0.50 MB 冷</text>
<text x="248" y="87" fill="#5aad4e" font-size="11.5">66 ms</text>
<rect x="196" y="100" width="277" height="20" rx="3" fill="#d4843a"/>
<text x="186" y="115" fill="#bdb9a6" font-size="11.5" text-anchor="end">3.34 MB 冷</text>
<text x="483" y="115" fill="#d4843a" font-size="11.5">431 ms</text>
<rect x="196" y="128" width="631" height="20" rx="3" fill="#e0445a"/>
<text x="186" y="143" fill="#bdb9a6" font-size="11.5" text-anchor="end">9.80 MB 冷</text>
<text x="837" y="143" fill="#e0445a" font-size="11.5">1292 ms</text>
<rect x="196" y="156" width="479" height="20" rx="3" fill="#8a8675"/>
<text x="186" y="171" fill="#bdb9a6" font-size="11.5" text-anchor="end">9.80 MB 热缓存</text>
<text x="685" y="171" fill="#8a8675" font-size="11.5">981 ms(persist=True,缓存命中仍要重建评分语料)</text>
<text x="196" y="202" fill="#6b6a63" font-size="10">0</text>
<text x="518" y="202" fill="#6b6a63" font-size="10">500 ms</text>
<text x="840" y="202" fill="#6b6a63" font-size="10">1000 ms</text>
</svg>
<div class="legend">
合成工作区:主题文件每个12个块,来源档案每个60条消息,按真实格式生成。
耗时随工作区总字节线性增长,因为 <code>inspect.search</code> 走的是 <code>persist=False</code>,
每轮把两棵目录树全量重新哈希和解析。<b>结论的方向</b>:每轮检索的预算问题是本地的,
本地问题能修;模型往返修不掉,只能不加。
</div>
</div>
<p class="note no"><b>否掉,不是因为它不好。</b>
claude-code没有索引,这条路是它绕开索引的办法,同一份对比里也正是它把实现规模压到最小的原因。
我们已经付过建索引的代价,再花一次模型往返去买同一件事,买的是重复的东西,付的是每一轮的等待。
真要提升检索质量,先做的两件事都在本地:一是让每轮检索不再全量重解析,二是给结果加一个真正的分数下限。</p>
<!-- ============================ 02 ============================ -->
<h2 id="core"><span class="num">02</span>常驻块写满了怎么裁<span class="verd vyes">改造后采纳 · 已落地</span></h2>
<p class="lead">openclaw裁 MEMORY.md 的时候只丢自己自动升上去的段落,人手写的一字不动,并把丢掉的日期列表返回给调用方。要抄这条得先答一个前置问题:<b>core.md 在新结构里到底是什么</b>。答完之后会发现,归属这件事本身可以不用再区分。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 300" role="img" aria-label="core.md 改之前的写入路径">
<rect x="14" y="14" width="200" height="24" rx="6" fill="#141a12"/>
<text x="26" y="31" fill="#5aad4e" font-size="11.5" letter-spacing="1.4">第一层 · 我们改之前</text>
<rect x="14" y="52" width="200" height="62" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="74" fill="#f0eee6" font-size="12">写入 agent</text>
<text x="30" y="93" fill="#8a8675" font-size="11">提示词:稳定信息</text>
<text x="30" y="108" fill="#8a8675" font-size="11">才更新 core.md</text>
<path d="M214 83 L252 83" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a2)"/>
<rect x="256" y="52" width="200" height="62" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="272" y="74" fill="#f0eee6" font-size="12">core.md 追加一段</text>
<text x="272" y="93" fill="#8a8675" font-size="11">带块ID,带证据脚注</text>
<text x="272" y="108" fill="#8a8675" font-size="11">和主题文件同一套契约</text>
<path d="M456 83 L494 83" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a2)"/>
<rect x="498" y="52" width="220" height="62" rx="7" fill="#1d1c14" stroke="#d4843a"/>
<text x="514" y="74" fill="#d4843a" font-size="12">2000 token 闸</text>
<text x="514" y="93" fill="#8a8675" font-size="11">block_views.py:27-36</text>
<text x="514" y="108" fill="#8a8675" font-size="11">超了就 raise</text>
<path d="M718 83 L756 83" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a2)"/>
<rect x="760" y="52" width="230" height="62" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="776" y="74" fill="#e0445a" font-size="12">整笔事务被拒</text>
<text x="776" y="93" fill="#8a8675" font-size="11">这一轮的主题文件改动</text>
<text x="776" y="108" fill="#8a8675" font-size="11">一起回滚,工作区逐字节不变</text>
<path d="M875 114 L875 140" stroke="#e0445a" stroke-width="1.5" marker-end="url(#a3)"/>
<rect x="640" y="142" width="470" height="46" rx="7" fill="#211316" stroke="#e0445a"/>
<text x="656" y="163" fill="#f0eee6" font-size="12">修复话术:core.md is full. Leave it alone and put this in a Topic file.</text>
<text x="656" y="180" fill="#8a8675" font-size="11">agent.py:96-99</text>
<path d="M640 165 L560 165" stroke="#e0445a" stroke-width="1.5" marker-end="url(#a3)"/>
<rect x="180" y="142" width="376" height="46" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="196" y="163" fill="#e0445a" font-size="12" font-weight="600">从此以后每一条稳定事实都被劝走</text>
<text x="196" y="180" fill="#8a8675" font-size="11">core.md 停在它第一次装满时的样子,不再变化</text>
<rect x="14" y="208" width="1096" height="76" rx="7" fill="#12110b" stroke="#2e2c20"/>
<text x="30" y="230" fill="#f0eee6" font-size="12" font-weight="600">没有任何东西在维护它</text>
<text x="30" y="250" fill="#bdb9a6" font-size="11.5">夜间重整只处理 topics/:organize_topics 把 topics/**.md 列进提示词,core.md 不在列表里(management/api.py:83-94)。</text>
<text x="30" y="269" fill="#bdb9a6" font-size="11.5">重构之前有一条确定性重建流程负责它,做法是"core.md 是某个指定页的镜像";重构时删掉了,没有补回来。</text>
</svg>
</div>
<div class="card scroll">
<svg viewBox="0 0 1140 296" role="img" aria-label="openclaw 的裁法与我们的裁法">
<rect x="14" y="14" width="540" height="24" rx="6" fill="#101722"/>
<text x="26" y="31" fill="#4f8ef7" font-size="11.5" letter-spacing="1.4">第二层 · openclaw:一个文件,丢就是销毁</text>
<rect x="586" y="14" width="540" height="24" rx="6" fill="#1a1622"/>
<text x="598" y="31" fill="#9d6fe0" font-size="11.5" letter-spacing="1.4">第三层 · 我们现在:正本加派生视图,丢不销毁</text>
<!-- openclaw -->
<rect x="14" y="50" width="540" height="146" rx="8" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="72" fill="#f0eee6" font-size="12">MEMORY.md 就是全部,10000 字符上限</text>
<rect x="30" y="84" width="240" height="22" rx="4" fill="#2a2a1c" stroke="#5aad4e"/>
<text x="42" y="99" fill="#5aad4e" font-size="11">人手写的段落 · 一律保留</text>
<rect x="30" y="110" width="240" height="22" rx="4" fill="#2a2118" stroke="#d4843a"/>
<text x="42" y="125" fill="#d4843a" font-size="11">自动升上来的 · 按日期从老到新丢</text>
<rect x="30" y="136" width="240" height="22" rx="4" fill="#211316" stroke="#e0445a"/>
<text x="42" y="151" fill="#e0445a" font-size="11">被丢掉的 · 世界上不再有第二份</text>
<text x="30" y="180" fill="#8a8675" font-size="11">正因为丢等于销毁,才必须先分清归属。返回丢掉的日期列表是它的留痕。</text>
<path d="M290 95 L340 95" stroke="#4a4736" stroke-width="1.2" marker-end="url(#a2)"/>
<path d="M290 121 L340 121" stroke="#4a4736" stroke-width="1.2" marker-end="url(#a2)"/>
<rect x="344" y="82" width="192" height="52" rx="6" fill="#12110b" stroke="#2e2c20"/>
<text x="360" y="103" fill="#bdb9a6" font-size="11">memory-budget.ts:25,100-164</text>
<text x="360" y="121" fill="#bdb9a6" font-size="11">按归属,不按时间</text>
<!-- ours -->
<rect x="586" y="50" width="250" height="146" rx="8" fill="#1d1c14" stroke="#5aad4e"/>
<text x="602" y="72" fill="#5aad4e" font-size="12" font-weight="600">topics/core.md 正本</text>
<text x="602" y="90" fill="#8a8675" font-size="11">一个主题文件,和别的主题</text>
<text x="602" y="106" fill="#8a8675" font-size="11">文件同一套契约,不设上限</text>
<rect x="602" y="116" width="218" height="18" rx="3" fill="#2a2a1c"/>
<text x="612" y="129" fill="#bdb9a6" font-size="10.5">段落 ^a1 · 常驻</text>
<rect x="602" y="138" width="218" height="18" rx="3" fill="#2a2a1c"/>
<text x="612" y="151" fill="#bdb9a6" font-size="10.5">段落 ^b2 · 常驻</text>
<rect x="602" y="160" width="218" height="18" rx="3" fill="#241f14"/>
<text x="612" y="173" fill="#8a8675" font-size="10.5">段落 ^c3 · 超出预算,留在这里</text>
<path d="M840 118 L878 118" stroke="#9d6fe0" stroke-width="1.5" marker-end="url(#a4)"/>
<text x="842" y="110" fill="#9d6fe0" font-size="10">渲染</text>
<rect x="882" y="50" width="244" height="146" rx="8" fill="#1d1c14" stroke="#9d6fe0"/>
<text x="898" y="72" fill="#9d6fe0" font-size="12" font-weight="600">core.md 派生</text>
<text x="898" y="90" fill="#8a8675" font-size="11">每次写入成功后重建,和</text>
<text x="898" y="106" fill="#8a8675" font-size="11">timeline/ 那些放在一起</text>
<rect x="898" y="116" width="212" height="18" rx="3" fill="#2a2a1c"/>
<text x="908" y="129" fill="#bdb9a6" font-size="10.5">段落 ^a1</text>
<rect x="898" y="138" width="212" height="18" rx="3" fill="#2a2a1c"/>
<text x="908" y="151" fill="#bdb9a6" font-size="10.5">段落 ^b2</text>
<text x="898" y="176" fill="#5aad4e" font-size="10.5">装到 2000 token 为止,装不下的不报错</text>
<rect x="586" y="206" width="540" height="76" rx="8" fill="#12110b" stroke="#9d6fe0"/>
<text x="602" y="228" fill="#f0eee6" font-size="12" font-weight="600">归属问题就此消失</text>
<text x="602" y="248" fill="#bdb9a6" font-size="11.5">被挤出去的段落还在正本里,检索照样找得到,脚注照样指着原话。</text>
<text x="602" y="267" fill="#bdb9a6" font-size="11.5">既然丢不销毁,就不需要先判断这段是谁写的才敢丢。</text>
<rect x="14" y="206" width="540" height="76" rx="8" fill="#12110b" stroke="#2e2c20"/>
<text x="30" y="228" fill="#f0eee6" font-size="12" font-weight="600">谁想让某段永远在,就把它排在前面</text>
<text x="30" y="248" fill="#bdb9a6" font-size="11.5">渲染按正本里的文件顺序装,装不下的是尾巴。人手编辑正本、夜间重整调整顺序,</text>
<text x="30" y="267" fill="#bdb9a6" font-size="11.5">都是已经存在的动作,不需要为常驻块单独发明一种标记。</text>
<defs>
<marker id="a2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#4a4736"/>
</marker>
<marker id="a3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#e0445a"/>
</marker>
<marker id="a4" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#9d6fe0"/>
</marker>
</defs>
</svg>
</div>
<p class="note k"><b>前置问题的答案</b>:core.md 是派生视图,不是独立内容。
它是 <code>topics/core.md</code> 这一个主题文件的渲染结果,和 <code>timeline/</code>、
<code>recent_events.jsonl</code>、<code>relations.json</code> 同一类,手工编辑它不起作用,
编辑要落在正本上。写入 agent 的提示词随之从"更新 core.md"变成"要每次都看见的稳定事实写进
<code>topics/core.md</code>",它于是只需要认识一种文件。老实现"core.md 是某个指定页的镜像"这句话仍然成立,
只是指定页从 wiki 页变成了主题文件。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 210" role="img" aria-label="块ID契约决定了什么形状才裁得动">
<text x="18" y="26" fill="#f0eee6" font-size="12.5" font-weight="600">为什么必须先变成派生视图,才谈得上裁</text>
<text x="18" y="46" fill="#8a8675" font-size="11">工作区有一条硬契约:编辑前存在过的块ID,编辑后必须还找得到,否则整笔拒绝。</text>
<text x="18" y="63" fill="#6b6a63" font-size="10.5">workspace.py:147-162 取基线时把 core.md 的块ID并进集合;topic_normalization.py:296-322 比对后判 "block ID must not be removed"。</text>
<!-- now -->
<rect x="18" y="82" width="540" height="110" rx="8" fill="#1d1c14" stroke="#e0445a"/>
<text x="34" y="104" fill="#e0445a" font-size="12" font-weight="600">现在:core.md 里可能有独苗</text>
<rect x="34" y="116" width="230" height="26" rx="4" fill="#12110b" stroke="#2e2c20"/>
<text x="46" y="133" fill="#bdb9a6" font-size="11">core.md 段落 ^c3</text>
<rect x="300" y="116" width="240" height="26" rx="4" fill="#12110b" stroke="#2e2c20"/>
<text x="312" y="133" fill="#6b6a63" font-size="11">topics/ 里没有同一个ID</text>
<path d="M264 129 L296 129" stroke="#e0445a" stroke-width="1.2" stroke-dasharray="3 3"/>
<text x="34" y="164" fill="#bdb9a6" font-size="11">裁掉它,^c3 从整个工作区消失,事务判定"块ID不得移除",</text>
<text x="34" y="182" fill="#bdb9a6" font-size="11">这一笔连同别的改动一起被拒。裁剪在今天是不合法的操作。</text>
<!-- planned -->
<rect x="586" y="82" width="540" height="110" rx="8" fill="#1d1c14" stroke="#5aad4e"/>
<text x="602" y="104" fill="#5aad4e" font-size="12" font-weight="600">派生之后:core.md 的ID永远是子集</text>
<rect x="602" y="116" width="230" height="26" rx="4" fill="#12110b" stroke="#2e2c20"/>
<text x="614" y="133" fill="#bdb9a6" font-size="11">core.md 段落 ^c3</text>
<rect x="868" y="116" width="240" height="26" rx="4" fill="#12110b" stroke="#5aad4e"/>
<text x="880" y="133" fill="#5aad4e" font-size="11">topics/core.md 里的 ^c3</text>
<path d="M832 129 L864 129" stroke="#5aad4e" stroke-width="1.2" marker-end="url(#a5)"/>
<text x="602" y="164" fill="#bdb9a6" font-size="11">裁掉渲染结果里的 ^c3,ID在正本里活着,契约满足。</text>
<text x="602" y="182" fill="#bdb9a6" font-size="11">顺带两处特判可以删掉:取基线和校验都不必再单独看 core.md。</text>
<defs>
<marker id="a5" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#5aad4e"/>
</marker>
</defs>
</svg>
</div>
<div class="tbl">
<table>
<tr>
<th>格子</th><th>我们改之前</th><th>openclaw</th><th>我们现在</th>
</tr>
<tr>
<td><b>谁写</b></td>
<td>写入 agent 直接编辑 core.md</td>
<td>自动升级流程加人手编辑,同一个文件</td>
<td class="h">谁都不写它。写 <code>topics/core.md</code>,Runtime 渲染</td>
</tr>
<tr>
<td><b>闸门</b></td>
<td>2000 token,超了抛异常<span class="src">management/config.py:7</span></td>
<td>10000 字符</td>
<td class="h">2000 token 变成渲染预算,不再是异常</td>
</tr>
<tr>
<td><b>满了怎么办</b></td>
<td class="n">整笔事务拒绝,修复话术让写入方绕开它,core.md 从此冻结</td>
<td>丢自动段落,人写的不动</td>
<td class="h">按正本文件顺序装到预算为止,装不下的留在正本</td>
</tr>
<tr>
<td><b>丢了会不会没</b></td>
<td class="q">不适用,从来没丢过</td>
<td class="n">会,所以必须先分归属</td>
<td class="y">不会,正本是全量,检索照旧命中</td>
</tr>
<tr>
<td><b>什么时候跑</b></td>
<td class="n">没有任何时候。夜间重整只列 topics/</td>
<td>写入时按预算裁</td>
<td class="h">每次写入事务安装成功后,跟着别的派生视图一起重建</td>
</tr>
<tr>
<td><b>说不说</b></td>
<td class="n">reorganize 返回 <code>{"status":"ok","topics":N}</code>,没有 core 的位置</td>
<td class="y">返回丢掉的日期列表</td>
<td class="y">返回渲染进去的token数和被挤出去的块ID</td>
</tr>
</table>
</div>
<p class="note"><b>顺便补上"这次到底动没动"</b>。夜间重整是模型判断,
而模型判断的失败模式是安静地什么都不做,并且在它自己的判据下它是对的:
同一套重整提示词,一个52万字符的单主题对话最后挤成一个34.4k字符的文件,重整跑了很多次一次都没拆它,
因为判据写的是"覆盖了两个主题"而不是大小。所以 <code>reorganize</code> 的返回要同时带上这一趟真正改过的文件列表,
空列表就是"这次什么都没动",让它可见。判据本身另说,但看不见的时候连该不该改判据都无从谈起。</p>
<!-- ============================ 03 ============================ -->
<h2 id="branch"><span class="num">03</span>离开的分支<span class="verd vmod">改造后采纳 · 已落地</span></h2>
<p class="lead">pi-mono在用户离开一条分支时给它生成一份摘要接在树上。这正对着 overview.md 承认的缺口:两条写入路径都只要 head 底下那条分支,用户中途放弃的分支再也没有人走回去。目的照收,摘要节点不要。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 380" role="img" aria-label="分支写入的三层">
<rect x="14" y="14" width="356" height="24" rx="6" fill="#141a12"/>
<text x="26" y="31" fill="#5aad4e" font-size="11.5" letter-spacing="1.4">第一层 · 我们现在</text>
<rect x="392" y="14" width="356" height="24" rx="6" fill="#101722"/>
<text x="404" y="31" fill="#4f8ef7" font-size="11.5" letter-spacing="1.4">第二层 · pi-mono</text>
<rect x="770" y="14" width="356" height="24" rx="6" fill="#1a1622"/>
<text x="782" y="31" fill="#9d6fe0" font-size="11.5" letter-spacing="1.4">第三层 · 我们打算</text>
<!-- col1 dag -->
<rect x="14" y="50" width="356" height="230" rx="8" fill="#1d1c14" stroke="#2e2c20"/>
<circle cx="80" cy="86" r="9" fill="#5aad4e"/><text x="98" y="90" fill="#bdb9a6" font-size="11">起点</text>
<line x1="80" y1="95" x2="80" y2="121" stroke="#4a4736" stroke-width="1.5"/>
<circle cx="80" cy="130" r="9" fill="#5aad4e"/><text x="98" y="134" fill="#bdb9a6" font-size="11">共享前缀</text>
<line x1="80" y1="139" x2="80" y2="165" stroke="#4a4736" stroke-width="1.5"/>
<line x1="80" y1="139" x2="200" y2="165" stroke="#4a4736" stroke-width="1.5" stroke-dasharray="4 3"/>
<circle cx="80" cy="174" r="9" fill="#5aad4e"/>
<line x1="80" y1="183" x2="80" y2="209" stroke="#4a4736" stroke-width="1.5"/>
<circle cx="80" cy="218" r="10" fill="#5aad4e" stroke="#f0eee6" stroke-width="2"/>
<text x="98" y="222" fill="#f0eee6" font-size="11">head,会被写</text>
<circle cx="208" cy="174" r="9" fill="#3a382a"/>
<line x1="208" y1="183" x2="208" y2="209" stroke="#3a382a" stroke-width="1.5"/>
<circle cx="208" cy="218" r="9" fill="#3a382a"/>
<text x="226" y="222" fill="#6b6a63" font-size="11">再没人走回来</text>
<text x="30" y="256" fill="#8a8675" font-size="11">两条写入路径都问 get_branch(session_id),</text>
<text x="30" y="272" fill="#8a8675" font-size="11">拿到的是 head 那一条,别的分支不在视野里。</text>
<!-- col2 -->
<rect x="392" y="50" width="356" height="230" rx="8" fill="#1d1c14" stroke="#4f8ef7"/>
<circle cx="458" cy="86" r="9" fill="#4f8ef7"/>
<line x1="458" y1="95" x2="458" y2="121" stroke="#4a4736" stroke-width="1.5"/>
<circle cx="458" cy="130" r="9" fill="#4f8ef7"/>
<line x1="458" y1="139" x2="458" y2="165" stroke="#4a4736" stroke-width="1.5"/>
<line x1="458" y1="139" x2="578" y2="165" stroke="#4a4736" stroke-width="1.5" stroke-dasharray="4 3"/>
<circle cx="458" cy="174" r="10" fill="#4f8ef7" stroke="#f0eee6" stroke-width="2"/>
<circle cx="586" cy="174" r="9" fill="#3a382a"/>
<line x1="586" y1="183" x2="586" y2="200" stroke="#3a382a" stroke-width="1.5"/>
<rect x="536" y="200" width="100" height="30" rx="6" fill="#101722" stroke="#4f8ef7"/>
<text x="586" y="220" fill="#4f8ef7" font-size="11" text-anchor="middle">分支摘要</text>
<text x="408" y="256" fill="#8a8675" font-size="11">它没有长期记忆层,摘要是被放弃的话唯一的去处。</text>
<text x="408" y="272" fill="#8a8675" font-size="11">branch-summarization.ts:200-262</text>
<!-- col3 -->
<rect x="770" y="50" width="356" height="230" rx="8" fill="#1d1c14" stroke="#9d6fe0"/>
<text x="786" y="74" fill="#9d6fe0" font-size="12" font-weight="600">会话边界枚举全部分支</text>
<rect x="786" y="86" width="324" height="26" rx="5" fill="#12110b" stroke="#2e2c20"/>
<text x="798" y="103" fill="#bdb9a6" font-size="11">list_branches(session_id) 给出每个分支尖端</text>
<rect x="786" y="118" width="324" height="26" rx="5" fill="#12110b" stroke="#2e2c20"/>
<text x="798" y="135" fill="#bdb9a6" font-size="11">对每个尖端走一次 get_branch(session_id, tip)</text>
<rect x="786" y="150" width="324" height="26" rx="5" fill="#12110b" stroke="#5aad4e"/>
<text x="798" y="167" fill="#5aad4e" font-size="11">已写消息id的集合自动扣掉共享前缀</text>
<rect x="786" y="182" width="324" height="26" rx="5" fill="#12110b" stroke="#5aad4e"/>
<text x="798" y="199" fill="#5aad4e" font-size="11">get_branch 已经过滤掉 rewound 的节点</text>
<rect x="786" y="214" width="324" height="26" rx="5" fill="#12110b" stroke="#d4843a"/>
<text x="798" y="231" fill="#d4843a" font-size="11">非head分支欠够一批才写,头底下那条照旧强制</text>
<text x="786" y="262" fill="#8a8675" font-size="11">不新增节点类型,不新增事件,不多一次模型调用。</text>
<rect x="14" y="292" width="1112" height="76" rx="8" fill="#12110b" stroke="#2e2c20"/>
<text x="30" y="314" fill="#f0eee6" font-size="12" font-weight="600">"故意放弃"不需要我们判断,会话存储已经记了</text>
<text x="30" y="334" fill="#bdb9a6" font-size="11.5">用户点回退,_rewind.py 给整段打上 rewound,节点留在图里但退出对话路径;get_branch 直接把它们滤掉(session_store.py:1030)。</text>
<text x="30" y="353" fill="#bdb9a6" font-size="11.5">重试产生的兄弟分支只有一两轮,够不上一批,自然不写。真正走了一段又折返的分支才够得上,而那正是缺口所指的那种。</text>
</svg>
</div>
<p class="note p"><b>为什么不做摘要节点。</b>
pi-mono的会话JSONL树只为回放和分支服务,跨会话只剩 AGENTS.md,被放弃的分支不摘要就真的没了。
我们有一整层按主题存放的记忆,一条分支上说过的事实该去 <code>topics/people/dave.md</code>,
不该去挂在DAG上的一个摘要节点里,那里没有脚注、没有块ID、检索也进不去。
我们已经有一种挂在DAG上的摘要(<code>context/summary</code> 的压缩摘要),它服务的是上下文窗口,不是记忆,
再加一种只会让"这段话到底在哪"多一个答案。</p>
<p class="note no"><b>而且"离开一条分支"在我们这里不是一个能观察到的事件。</b>
事件注册表里没有任何 head 移动或者分支切换的事件;head 的写入散在五处
(<code>set_head</code>、<code>append_message</code> 的链式前进、<code>SessionNodeWriter.append</code>、
<code>update_session</code>、<code>_rewind.py</code> 直接改索引),它们唯一共同经过的地方是
<code>SessionMemoryIndex.set_head</code>,而那里把旧值直接丢掉了。
照抄 pi-mono 就得先在这条链路上开一个新事件、并让它记住上一个 head 是谁,
为的是触发一件在会话结束时做同样划算的事。会话边界那个入口已经在了:
<code>session_watcher.py:103</code> 每次只问了 head 那一条分支。</p>
<p class="note"><b>一个要一起改的判据。</b>
<code>should_incremental_write</code> 现在有两条:欠够 token 阈值,或者欠着东西且最后一条消息已经过去一小时。
被放弃的分支最后一条消息一定是旧的,第二条会让一轮的重试兄弟也被写进去。
所以非head分支只走第一条,把 <code>idle_after</code> 关掉。这是这条方案唯一需要新增的判断。</p>
<!-- ============================ 04 ============================ -->
<h2 id="cursor"><span class="num">04</span>游标兜底<span class="verd vyes">采纳 · 已落地</span></h2>
<p class="lead">claude-code在游标指的那条消息找不到时,选择把全部消息重新当成没写过,注释写明返回零条会让抽取在这个会话剩下的时间里彻底停摆。我们的写入游标方案里"从原话档案播种"是同一个取舍。问题是它只覆盖了迁移那一次,日常运行里还有一条同形状的路没有堵。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 320" role="img" aria-label="游标失效时的两条路">
<rect x="14" y="14" width="356" height="24" rx="6" fill="#101722"/>
<text x="26" y="31" fill="#4f8ef7" font-size="11.5" letter-spacing="1.4">第二层 · claude-code 堵的那条</text>
<rect x="392" y="14" width="356" height="24" rx="6" fill="#141a12"/>
<text x="404" y="31" fill="#5aad4e" font-size="11.5" letter-spacing="1.4">第一层 · 我们现在同形状的那条</text>
<rect x="770" y="14" width="356" height="24" rx="6" fill="#1a1622"/>
<text x="782" y="31" fill="#9d6fe0" font-size="11.5" letter-spacing="1.4">第三层 · 一条规则</text>
<rect x="14" y="50" width="356" height="42" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="30" y="76" fill="#f0eee6" font-size="12">游标是一条消息UUID,向后扫</text>
<path d="M192 92 L192 108" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a6)"/>
<rect x="14" y="110" width="356" height="42" rx="7" fill="#1d1c14" stroke="#d4843a"/>
<text x="30" y="136" fill="#d4843a" font-size="12">那条消息在会话里找不到了</text>
<path d="M192 152 L192 168" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a6)"/>
<rect x="14" y="170" width="356" height="60" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="30" y="192" fill="#e0445a" font-size="12">照直做的话:扫出零条</text>
<text x="30" y="212" fill="#8a8675" font-size="11">这个会话剩下的时间里再也抽不出东西</text>
<path d="M192 230 L192 246" stroke="#5aad4e" stroke-width="1.5" marker-end="url(#a5)"/>
<rect x="14" y="248" width="356" height="56" rx="7" fill="#141a12" stroke="#5aad4e"/>
<text x="30" y="270" fill="#5aad4e" font-size="12">它选:全部当成没写过</text>
<text x="30" y="290" fill="#8a8675" font-size="11">重写一遍代价有限,安静停摆代价没有上限</text>
<rect x="392" y="50" width="356" height="42" rx="7" fill="#1d1c14" stroke="#2e2c20"/>
<text x="408" y="76" fill="#f0eee6" font-size="12">游标状态在 runtime.json 里</text>
<path d="M570 92 L570 108" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a6)"/>
<rect x="392" y="110" width="356" height="42" rx="7" fill="#1d1c14" stroke="#d4843a"/>
<text x="408" y="136" fill="#d4843a" font-size="12">文件读不出来或者不是合法JSON</text>
<path d="M570 152 L570 168" stroke="#4a4736" stroke-width="1.5" marker-end="url(#a6)"/>
<rect x="392" y="170" width="356" height="60" rx="7" fill="#1d1c14" stroke="#e0445a"/>
<text x="408" y="192" fill="#e0445a" font-size="12">RuntimeStateStore.load 直接抛</text>
<text x="408" y="212" fill="#8a8675" font-size="11">state.py:51-55,json.loads 没有兜底</text>
<path d="M570 230 L570 246" stroke="#e0445a" stroke-width="1.5" marker-end="url(#a3)"/>
<rect x="392" y="248" width="356" height="56" rx="7" fill="#211316" stroke="#e0445a"/>
<text x="408" y="270" fill="#e0445a" font-size="12">变成 WriteFailure(retryable=True)</text>
<text x="408" y="290" fill="#8a8675" font-size="11">看门狗不标记会话,每一轮回来重试,永远写不进去</text>
<rect x="770" y="50" width="356" height="140" rx="8" fill="#1d1c14" stroke="#9d6fe0"/>
<text x="786" y="74" fill="#9d6fe0" font-size="12" font-weight="600">读不出来的游标等于空集合</text>
<text x="786" y="96" fill="#bdb9a6" font-size="11.5">不是异常。一个读不出来的游标说的是</text>
<text x="786" y="114" fill="#bdb9a6" font-size="11.5">"我不知道哪些写过了",而集合形状下</text>
<text x="786" y="132" fill="#bdb9a6" font-size="11.5">"不知道"的安全解释只有一个:都没写过。</text>
<text x="786" y="156" fill="#8a8675" font-size="11">重写一批的代价是有限的,而且事务本来就会</text>
<text x="786" y="174" fill="#8a8675" font-size="11">合并说了同一件事的段落。</text>
<rect x="770" y="200" width="356" height="104" rx="8" fill="#12110b" stroke="#2e2c20"/>
<text x="786" y="222" fill="#f0eee6" font-size="12" font-weight="600">另一半已经被结构管住了</text>
<text x="786" y="242" fill="#bdb9a6" font-size="11">游标文件在工作区里面,不在旁边:</text>
<text x="786" y="260" fill="#bdb9a6" font-size="11">memory/.scriptorium/cursors/。备份、git提交、</text>
<text x="786" y="278" fill="#bdb9a6" font-size="11">回滚都带着它一起走,不会出现"内容退回上周、</text>
<text x="786" y="296" fill="#bdb9a6" font-size="11">游标停在今天"这种把整周都标成已写的情形。</text>
<defs>
<marker id="a6" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#4a4736"/>
</marker>
</defs>
</svg>
</div>
<p class="note"><b>和迁移那条的分工。</b>
overview.md 的「从位置游标迁移」处理的是一次性的:老状态没法反推出账,于是从原话档案播种,
宁可多写一批也不全丢。那是同一个取舍,但它只在升级那一刻发生。
这里说的是日常运行时账本本身出问题,和迁移无关,两条都要有,方向一致。</p>
<p class="note k"><b>账本换了形状之后,这一条落在哪。</b>
写入游标最后取的是<b>在节点上打「已写」标记</b>,不是记忆自己存一份 ID 集合,
所以没有一个会读不出来的游标文件了。这一条的方向仍然成立,只是换了对象:
账本读不出来时按「都没写过」处理,而不是抛异常让这个会话永远写不进去。
节点标记这边对应的是标记丢了就重做一遍,以及 <code>RuntimeStateStore.load</code>
对着一份解析不了的 <code>runtime.json</code> 应当读成空计数器而不是抛。
两种做法的并排对比在 <a href="written-marker.html">written-marker.md</a> 第三层。</p>
<!-- ============================ 05 ============================ -->
<h2 id="land"><span class="num">05</span>改动落在哪</h2>
<p class="lead">三条采纳的方案,落到文件上是这些位置。否掉的那条不落任何文件。</p>
<div class="card scroll">
<svg viewBox="0 0 1140 440" role="img" aria-label="三条方案的改动落点">
<text x="18" y="26" fill="#8a8675" font-size="11" letter-spacing="1.6">方案</text>
<text x="250" y="26" fill="#8a8675" font-size="11" letter-spacing="1.6">落点</text>
<text x="700" y="26" fill="#8a8675" font-size="11" letter-spacing="1.6">这个位置现在做什么</text>
<line x1="18" y1="36" x2="1122" y2="36" stroke="#2e2c20"/>
<rect x="18" y="46" width="1104" height="222" rx="8" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="46" width="3" height="222" fill="#d4843a"/>
<text x="36" y="70" fill="#5aad4e" font-size="12.5" font-weight="600">core.md 变派生视图 · 已落地</text>
<text x="36" y="90" fill="#8a8675" font-size="11">02节</text>
<text x="36" y="118" fill="#8a8675" font-size="10.5">上面五行改行为,</text>
<text x="36" y="134" fill="#8a8675" font-size="10.5">下面五行是它不再</text>
<text x="36" y="150" fill="#8a8675" font-size="10.5">被当成正本之后,</text>
<text x="36" y="166" fill="#8a8675" font-size="10.5">顺带要拆掉的特判</text>
<text x="250" y="70" fill="#d4843a" font-size="11.5">runtime/derived_views.py</text>
<text x="700" y="70" fill="#bdb9a6" font-size="11.5">重建 timeline、recent_events、relations,core.md 加进来</text>
<text x="250" y="90" fill="#d4843a" font-size="11.5">management/block_views.py:24-36</text>
<text x="700" y="90" fill="#bdb9a6" font-size="11.5">_synchronize一超2000 token就raise,改成渲染预算并报出挤掉的块ID</text>
<text x="250" y="110" fill="#d4843a" font-size="11.5">management/agent.py:96-99</text>
<text x="700" y="110" fill="#bdb9a6" font-size="11.5">修复话术"core.md 满了,别动它",删掉</text>
<text x="250" y="130" fill="#d4843a" font-size="11.5">prompts/write.py:15</text>
<text x="700" y="130" fill="#bdb9a6" font-size="11.5">让写入方直接编辑 core.md,改成写 topics/core.md</text>
<text x="250" y="150" fill="#d4843a" font-size="11.5">prompts/system.py:7,15</text>
<text x="700" y="150" fill="#bdb9a6" font-size="11.5">同一句话在系统提示词里又说了两遍,一起改</text>
<text x="250" y="176" fill="#8a8675" font-size="11.5">management/transaction.py:27,169-190</text>
<text x="700" y="176" fill="#bdb9a6" font-size="11.5">WRITABLE_FILES把core.md列为可手改,派生之后它不再可写</text>
<text x="250" y="196" fill="#8a8675" font-size="11.5">management/patching.py:1-5</text>
<text x="700" y="196" fill="#bdb9a6" font-size="11.5">补丁的可写面同样写着topics/**和core.md</text>
<text x="250" y="216" fill="#8a8675" font-size="11.5">workspace.py 与 transaction.py 的四处基线</text>
<text x="700" y="216" fill="#bdb9a6" font-size="11.5">取基线时把core.md的块ID并进校验集合,四处都可以去掉</text>
<text x="250" y="236" fill="#8a8675" font-size="11.5">topic_normalization.py:100,222,250,311</text>
<text x="700" y="236" fill="#bdb9a6" font-size="11.5">分配块ID时把core.md追加进文件列表,当成第五个主题文件</text>
<text x="250" y="256" fill="#8a8675" font-size="11.5">store.py:95</text>
<text x="700" y="256" fill="#bdb9a6" font-size="11.5">ensure()建工作区时塞一个空的# Core,派生之后由渲染负责</text>
<rect x="18" y="278" width="1104" height="90" rx="8" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="278" width="3" height="90" fill="#9d6fe0"/>
<text x="36" y="302" fill="#f0eee6" font-size="12.5" font-weight="600">会话边界写全部分支</text>
<text x="36" y="322" fill="#8a8675" font-size="11">03节</text>
<text x="250" y="302" fill="#9d6fe0" font-size="11.5">memory/writing.py:231-291</text>
<text x="700" y="302" fill="#bdb9a6" font-size="11.5">_branch只取head那条,强制模式循环写到没有欠账</text>
<text x="250" y="322" fill="#9d6fe0" font-size="11.5">runtime/state.py:111-121</text>
<text x="700" y="322" fill="#bdb9a6" font-size="11.5">should_incremental_write的闲置分支要能关掉</text>
<text x="250" y="342" fill="#9d6fe0" font-size="11.5">memory/session_watcher.py:103</text>
<text x="700" y="342" fill="#bdb9a6" font-size="11.5">会话闲置时db.get_branch(sid)只拿head那条,这是方案的入口</text>
<rect x="18" y="378" width="1104" height="40" rx="8" fill="#1d1c14" stroke="#2e2c20"/>
<rect x="18" y="378" width="3" height="40" fill="#5aad4e"/>
<text x="36" y="403" fill="#f0eee6" font-size="12.5" font-weight="600">游标读不出来等于空集合</text>
<text x="250" y="403" fill="#5aad4e" font-size="11.5">runtime/state.py:51-55</text>
<text x="700" y="403" fill="#bdb9a6" font-size="11.5">load里的json.loads没有兜底,读不出来就抛</text>
</svg>
</div>
<p class="note"><b>节点marker已经替换位置游标。</b>
实现位于SessionDB节点metadata、<code>runtime/online.py</code>和<code>writing.py</code>的
<code>_records</code>边界,legacy迁移从严格解析的Source前缀恢复可验证节点。
会话边界现在先处理当前head,再枚举其他活分支;共享前缀由节点marker去重。
完整定义见 <a href="written-marker.html">written-marker.md</a>。</p>
<p class="note k"><b>实现状态(2026-08-11)</b>:01节的无索引模型选择按延迟实测拒绝;
02节<code>topics/core.md</code>正本与确定性<code>core.md</code>派生视图已实现;03节会话边界枚举
活分支已实现;04节损坏状态按空状态恢复并配合严格legacy迁移与节点marker实现。
当前仍使用轮后阈值和idle watcher,没有由head变化事件直接通知writer。</p>
</div>
</body>
</html>