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

@ -147,9 +147,6 @@ func initMemoryStack(dataDir string) (*memoryStack, func()) {
} else {
log.Printf("[homed] graph memory initialized")
}
if memDB != nil {
}
memIdx := memory.NewIndexer(memDB)
memIdx.Sync() // 启动时立即同步,避免前30分钟空窗
socialStore := social.New(memDB)
@ -160,8 +157,11 @@ func initMemoryStack(dataDir string) (*memoryStack, func()) {
BatchSize: 50,
})
if memDB != nil {
// 这里**故意不写 defer distiller.Stop()**:本函数在 return 时即触发
// defer,而 Stop() → cancel() 会让刚启动的 distillLoop 立刻退出,
// 规则蒸馏管线启动即死、10min 心跳从不运行(旧 main() 拆分时的残留)。
// 停机由调用点注册的 cleanup 负责(见下方返回值)。
distiller.Start()
defer distiller.Stop()
}
return &memoryStack{db: memDB, indexer: memIdx, social: socialStore, distiller: distiller},

View File

@ -0,0 +1,42 @@
package main
import (
"os"
"path/filepath"
"testing"
)
// TestInitMemoryStackKeepsDistillerRunning 锁死启动接线回归:
// initMemoryStack 必须返回一个**仍在运行**的蒸馏器。
//
// 历史 bug:main() 拆分时函数体内残留一句 `defer distiller.Stop()`,
// 函数一 return 就 cancel 掉刚启动的循环,规则蒸馏 10min 心跳从不运行。
// 该缺陷不会让任何单测变红——pipeline 的 TestDistillOnce* 直接调
// distillOnce,绕过了 Start/Stop 接线;只有在这里按「启动阶段函数」的
// 真实调用方式断言,才照得出来。
func TestInitMemoryStackKeepsDistillerRunning(t *testing.T) {
dir := t.TempDir()
// NewGraphDB 需要父目录已存在(生产由 dataDir 初始化保证)。
if err := os.MkdirAll(filepath.Join(dir, "memory"), 0755); err != nil {
t.Fatal(err)
}
st, cleanup := initMemoryStack(dir)
if st == nil || st.distiller == nil {
cleanup()
t.Fatal("initMemoryStack 未返回蒸馏器")
}
if st.db == nil {
cleanup()
t.Skip("图库未初始化,无法验证蒸馏接线")
}
if st.distiller.Stopped() {
cleanup()
t.Fatal("initMemoryStack 返回后蒸馏循环已被停掉(defer Stop 残留?)")
}
// cleanup 是唯一的停机点:先停蒸馏器、再关图库。
cleanup()
if !st.distiller.Stopped() {
t.Fatal("cleanup 之后蒸馏器应已停止")
}
}