diff --git a/go/internal/api/routes/consolidation_pipe.go b/go/internal/api/routes/consolidation_pipe.go index 4efecd5..4d6ecc1 100644 --- a/go/internal/api/routes/consolidation_pipe.go +++ b/go/internal/api/routes/consolidation_pipe.go @@ -50,7 +50,14 @@ func (cp *ConsolidationPipeline) Run() (*ConsolidationReport, error) { // mode: "cluster_only" | "full" | "prune_only" // full 模式触发 LLM 质量回溯 + 衰减校准(仅低频调度使用) func (cp *ConsolidationPipeline) RunWithMode(mode string) (*ConsolidationReport, error) { - // ─── 尝试 Rust sidecar ────────────────────────────── + // ─── 前置检查:timestamp 合理性(防止 epoch-0 数据污染聚类结果)──────── + if mode == "full" || mode == "cluster_only" { + if suspicious, total := cp.checkTimestampSanity(); suspicious > 0 { + log.Printf("[WARN] timestamp 检查: %d/%d 条记忆时间戳可疑(可能是 epoch-0),结果仅供参考", suspicious, total) + } + } + + // ─── 尝试 Rust sidecar ─────────────────────────────────────── var report *ConsolidationReport if cp.dataDir != "" && cp.sqlitePath != "" { // 用指定模式调用 Rust sidecar @@ -64,6 +71,13 @@ func (cp *ConsolidationPipeline) RunWithMode(mode string) (*ConsolidationReport, Patterns: []string{fmt.Sprintf("decay_rates=%v", rustReport.DecayRates)}, GraphPruned: 0, } + + // ─── 后置检查:聚类数量下限 ───────────────────────────── + if rustReport.Clusters <= 1 { + log.Printf("[ERROR] consolidate 生成 clusters=%d(疑似 epoch-0 数据或聚类失效),结果可能无效", rustReport.Clusters) + report.Patterns = append(report.Patterns, fmt.Sprintf("⚠️ clusters=%d 可能异常", rustReport.Clusters)) + } + if rustReport.Quality != nil { report.Patterns = append(report.Patterns, fmt.Sprintf("quality_score=%.2f low_info=%d hallucinations=%d", @@ -270,6 +284,56 @@ func (cp *ConsolidationPipeline) updateGraph() (int, error) { return before - after, nil } +// checkTimestampSanity 抽样检查记忆时间戳合理性(5% sampling,不阻塞) +// 返回 (可疑数量, 抽样总数)。epoch-0(< 2024-01-01)视为可疑。 +func (cp *ConsolidationPipeline) checkTimestampSanity() (suspicious, total int) { + const epochThreshold int64 = 1704067200 // 2024-01-01 00:00:00 UTC + + memories, err := cp.ldb.GetCandidatesForForgetting() + if err != nil || len(memories) == 0 { + return 0, 0 + } + + // 全量不超过 100 条 + sampleSize := len(memories) + if sampleSize > 100 { + sampleSize = 100 + } + step := len(memories) / sampleSize + if step < 1 { + step = 1 + } + + for i := 0; i < len(memories); i += step { + total++ + ts := memoryInt64Val(memories[i], "created_at") + if ts > 0 && ts < epochThreshold { + suspicious++ + } + } + return suspicious, total +} + +// memoryInt64Val 安全取 int64(处理 string/int/float) +func memoryInt64Val(m map[string]interface{}, key string) int64 { + v, ok := m[key] + if !ok { + return 0 + } + switch val := v.(type) { + case int64: + return val + case float64: + return int64(val) + case int: + return int64(val) + case string: + t, _ := time.Parse(time.RFC3339, val) + return t.Unix() + } + return 0 +} + // ─── 报告 ───────────────────────────────────────────── type ConsolidationReport struct { diff --git a/go/internal/api/server.go b/go/internal/api/server.go index a436d5e..6852c25 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -87,6 +87,10 @@ func NewServer() http.Handler { ldb = storage.NewMemLanceClient(emb) log.Printf("[zhiyid] 存储后端: 内存(零依赖)") } + + // 启动时数据目录一致性检查(防止路径混乱导致读取废弃数据) + runStartupChecks(backend) + // G7.2: 注册 LDB getter(供 skill crystallize 使用) routes.RegisterLDBGetter(func() storage.LanceDB { return ldb }) @@ -1109,3 +1113,39 @@ func AgentTypeDecayOrDefault(agentType string) float64 { } return 0.015 } + +// runStartupChecks 启动时数据目录一致性检查 +// 检测废弃路径(如 /home/muc/data)并警告,防止数据源混乱 +func runStartupChecks(backend string) { + canonicalDataDir := "/var/lib/memoryweave" + deprecatedPaths := []string{ + "/home/muc/data", + "/home/muc/.local/share/memoryweave", + } + + for _, dep := range deprecatedPaths { + if _, err := os.Stat(dep); err == nil { + log.Printf("[WARN] 检测到废弃数据目录 %s,仍有数据残留(当前 canonical: %s)", dep, canonicalDataDir) + } + } + + // LanceDB 后端:检查 Rust sidecar socket 是否可达 + if backend == "lancedb" { + sockPath := os.Getenv("LANCEDB_SOCKET") + if sockPath == "" { + sockPath = "/tmp/zhiyi-ipc.sock" + } + if _, err := os.Stat(sockPath); os.IsNotExist(err) { + log.Printf("[WARN] LanceDB socket 不存在 (%s),Rust sidecar 可能未运行", sockPath) + } else { + log.Printf("[startup] LanceDB socket 就绪: %s", sockPath) + } + } + + // canonical 目录存在性检查 + if _, err := os.Stat(canonicalDataDir); os.IsNotExist(err) { + log.Printf("[WARN] canonical 数据目录不存在: %s(首次部署?)", canonicalDataDir) + } else { + log.Printf("[startup] 数据目录正常: %s", canonicalDataDir) + } +} diff --git a/go/internal/governance/graph_expander.go b/go/internal/governance/graph_expander.go index 14c2a0d..fc74b5e 100644 --- a/go/internal/governance/graph_expander.go +++ b/go/internal/governance/graph_expander.go @@ -26,10 +26,10 @@ func (g *InMemoryGraph) ExpandFromResults(results []models.RecallResult, namespa continue } for _, p := range paths { - target, _ := p["target"].(string) - source, _ := p["source"].(string) + to, _ := p["to"].(string) + from, _ := p["from"].(string) - for _, id := range []string{target, source} { + for _, id := range []string{to, from} { if id != "" && !seen[id] { seen[id] = true expanded = append(expanded, models.RecallResult{