diff --git a/go/internal/api/routes/admin.go b/go/internal/api/routes/admin.go index a52f6be..0181e95 100644 --- a/go/internal/api/routes/admin.go +++ b/go/internal/api/routes/admin.go @@ -72,6 +72,22 @@ func (aa *AdminAPI) Forget(w http.ResponseWriter, r *http.Request) { tier := strVal(mem["tier"]) content := strVal(mem["content"]) + // 🔒 2026-09-07 修复(误删事故): episodes(原始对话)不参与遗忘 + if cat := strVal(mem["category"]); cat == "episodes" { + continue + } + // 🔒 长内容保护: >200 字记忆不参与 auto 遗忘(有实质信息) + if len([]rune(content)) > 200 { + continue + } + // 2026-09-07 方案A 碎片快速道(与 server.go decay trigger 一致): + // <30 字且 >20 天未访问 → 直接遗忘(碎片 recall_count 可能虚高, 绕过保命) + if len([]rune(content)) < 30 && time.Since(lastAccess).Hours()/24 > 20 { + aa.LanceDB.SoftDelete(strVal(mem["id"]), "auto_forget_fragment") + forgotten++ + continue + } + // E4.3 修复: 从 content 提取实体(而非读 namespace),取图谱最大度 degree := extractTopEntityDegree(content, aa.GraphStore) diff --git a/go/internal/api/server.go b/go/internal/api/server.go index 622ba46..c5f3466 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -1262,6 +1262,19 @@ func NewServer() http.Handler { } for _, m := range memories { decayScanned++ + // 🔒 2026-09-07 修复(误删事故): episodes(原始对话)不参与遗忘—— + // 它们是蒸馏原料/审计历史, 不是可遗忘的记忆。曾因未过滤 category + // 导致 67 条 8月对话被 auto_forget 清掉(已恢复)。仅 distilled/general 等记忆可遗忘。 + if cat, ok := m["category"].(string); ok && cat == "episodes" { + continue + } + // 🔒 长内容保护: distilled >200 字(有实质信息)不参与 auto 遗忘 + // (曾误删 405 字 CNB 修复经验; 长记忆应由蒸馏/整合管理, 非时间遗忘) + if cat, _ := m["category"].(string); cat != "episodes" { + if cs, ok := m["content"].(string); ok && len([]rune(cs)) > 200 { + continue + } + } // 解析 last_recalled_at lastAccessed := time.Now().Add(-30 * 24 * time.Hour) // 默认30天前 if t, ok := m["last_recalled_at"].(string); ok && t != "" { @@ -1269,6 +1282,20 @@ func NewServer() http.Handler { lastAccessed = parsed } } + // 2026-09-07 方案A 碎片快速道: <30 字且 >20 天未访问 → 直接遗忘 + // (绕过 ShouldForget 的 recallCount 保命——碎片 recall_count 可能虚高)。 + // 碎片无记忆价值(审计: 30.9% <20字 + 43% <50字 = 蒸馏无门槛产物)。 + contentStr := "" + if cs, ok := m["content"].(string); ok { + contentStr = cs + } + if len([]rune(contentStr)) < 30 && time.Since(lastAccessed).Hours()/24 > 20 { + if id, ok := m["id"].(string); ok { + _ = ldb.SoftDelete(id, "auto_forget_fragment") + decayForgotten++ + } + continue + } recallCount := 0 if rc, ok := m["recall_count"].(int); ok { recallCount = rc diff --git a/go/internal/governance/governance.go b/go/internal/governance/governance.go index b18ef0c..58acac7 100644 --- a/go/internal/governance/governance.go +++ b/go/internal/governance/governance.go @@ -220,6 +220,8 @@ type Forgetter struct { } func NewForgetter() *Forgetter { + // 2026-09-07 方案A: 0.015→0.03 曾致误删(36天库龄下 0-recall 长记忆全被清, 405字CNB经验被删), + // 回滚 0.015。碎片清除由碎片快速道(server.go <30字&>20天)负责, 普通记忆 53 天老化合理。 return &Forgetter{agentType: "default", decayRate: 0.015} } @@ -259,7 +261,10 @@ func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier s score = 0.1 } // recallCount > 0 减缓衰减 - score += float64(recallCount) * 0.05 + // 2026-09-07 方案A: 0.05→0.005。原 0.05/次 + 无 cap → recall_count 457-540 时 +25 分, + // 永不遗忘 (日志大量 recall_count 500+ = 每次搜索命中都 ++, 虚高保命)。 + // 现 0.005/次, cap 30 次后贡献 ≤0.15 分 ≈ 5 天保护, 合理。 + score += float64(recallCount) * 0.005 // 图谱节点度 > 5 时,每超过 1 度 + 0.03 保留分(E4.3: 图谱推理参与遗忘决策) if len(graphDegree) > 0 && graphDegree[0] > 5 { score += float64(graphDegree[0]-5) * 0.03