fix(memory): 记忆层启动接线/并发/落盘一致性整备

按设计方案整顿记忆系统,收敛一批"单测照不出、只在长跑生产里暴露"的缺陷:

- 启动接线:initMemoryStack 残留 `defer distiller.Stop()`,规则蒸馏
  10min 心跳启动即死。改为由调用点 cleanup 停机,并补 Stopped() 探针 +
  TestInitMemoryStackKeepsDistillerRunning / TestStartKeepsLoopRunningUntilStop。
- L0 相关性上下文:SetDenseSpace 注入稠密空间时回填已有事件的稠密向量,
  否则旧事件走稀疏余弦、新事件走稠密余弦,同一次 Prune 里两种尺度混排。
- 文档检索:QueryScored 访问计数从读锁内写移出(-race 竞争),更新后置脏,
  优雅关停可落盘、FindColdDocs 冷度判据跨重启不再失真。
- 蒸馏管线:只 flush 未落盘记录(persisted 标记)、蒸馏成功后从 raw 文件
  删除对应行、原子写文件,修重启重复蒸馏导致 mention_count 膨胀。
- 图库:全量 Recall 加实体上限(内部整备路径,防大图整表进内存);
  ClearSentenceID 补写锁;Commit/upsertEntity 计数语义注释澄清。
- 索引器:recalled 去重集加 FIFO 上限,防长跑进程自动注入越来越沉默。
- 文档/媒体注释修正;README 记忆层流程对齐跨模态召回。

验证:go build ./...、go vet、go test -race
./internal/memory/... ./internal/agent/core/... ./cmd/homed/... 全绿。
This commit is contained in:
JianFeeeee
2026-09-15 06:32:36 +08:00
parent 3a780384d0
commit 6afe361804
12 changed files with 413 additions and 61 deletions

View File

@ -1,7 +1,9 @@
package document
import (
"fmt"
"os"
"sync"
"testing"
"time"
@ -652,3 +654,55 @@ func TestStopFlushesDirtyDocs(t *testing.T) {
t.Fatalf("Stop 未落盘: %+v", got)
}
}
// TestQueryScoredConcurrentAndDirty 锁死两点:
// 1. QueryScored 更新访问计数必须离开读锁(旧实现在 RLock 下写,-race 会报);
// 2. 更新后必须置脏,否则优雅关停也不落盘、FindColdDocs 的冷度判据跨重启失真。
func TestQueryScoredConcurrentAndDirty(t *testing.T) {
dir := t.TempDir()
s := NewStore(dir, memory.TokenizeWords)
if err := s.Start(); err != nil {
t.Fatal(err)
}
defer s.Stop()
for i := 0; i < 8; i++ {
// 文档必须互相区分:若 8 篇文本完全相同,查询词出现在每一篇里,
// IDF = log(N/df) = 0,词向量全零、检索恒为空,测试就成了假阴性。
summary, content := "无关的天气与散步记录", "今天适合出门"
if i < 3 {
summary, content = "并发检索目标", "并发内容"
}
if err := s.Insert(&Doc{ID: fmt.Sprintf("d%d", i), Summary: summary, Content: content}); err != nil {
t.Fatal(err)
}
}
// 清掉 Insert 置的脏位,验证 QueryScored 自己会置脏。
s.mu.Lock()
s.dirty = false
s.mu.Unlock()
var wg sync.WaitGroup
for g := 0; g < 8; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for k := 0; k < 30; k++ {
s.QueryScored("并发检索目标", 5)
s.FindColdDocs(72*time.Hour, 2)
}
}()
}
wg.Wait()
s.mu.RLock()
dirty := s.dirty
access := s.docs["d0"].AccessCount
s.mu.RUnlock()
if access <= 1 {
t.Errorf("AccessCount 未随检索累加: %d", access)
}
if !dirty {
t.Error("QueryScored 更新访问计数后未置脏")
}
}