fix(storage): fix all no-op/fake implementations in LanceDB interfaces

- IncrementUseful/IncrementNotUseful now return error and persist via
  rc.Update() (Rust sidecar); all 4 implementations updated
- SoftDeleteBatch now delegates to SoftDelete() for real LanceDB write
- UpdateMemoryContent now persists content + version + version history
- Backup changed from return nil to explicit error (Rust sidecar unsupported)
- IncrementUseful/IncrementNotUseful signatures now include error return;
  all 3 call sites (feedback.go, core.go) check the error
- Test mocks (mockLance, fakeLDB) updated to match new interface

Fixes the 'silent no-op' class of bugs discovered during dedup repair.
This commit is contained in:
小唯
2026-09-20 09:53:00 +08:00
parent ece2da12ac
commit 6e67f3e33f
9 changed files with 175 additions and 92 deletions
+3 -1
View File
@@ -725,7 +725,9 @@ func (a *API) Dedup(w http.ResponseWriter, r *http.Request) {
}
}
for id := range demoted {
a.LanceDB.IncrementNotUseful(id)
if err := a.LanceDB.IncrementNotUseful(id); err != nil {
log.Printf("[dedup] demote failed %s: %v", id, err)
}
}
// 强制 flush 软删缓冲后再返回。
// 起因(2026-09-20):endpoint 报 deleted=34 时数据仍在缓冲里,
+10 -6
View File
@@ -12,7 +12,7 @@ import (
)
type FeedbackAPI struct {
LanceDB storage.LanceDB
LanceDB storage.LanceDB
}
func NewFeedbackAPI(ldb storage.LanceDB) *FeedbackAPI {
@@ -40,7 +40,9 @@ func (fa *FeedbackAPI) MarkUseful(w http.ResponseWriter, r *http.Request) {
[]string{req.MemoryID},
"user_feedback", "success", "",
)
fa.LanceDB.IncrementUseful(req.MemoryID)
if err := fa.LanceDB.IncrementUseful(req.MemoryID); err != nil {
log.Printf("[feedback] IncrementUseful failed %s: %v", req.MemoryID, err)
}
// Phase F: 质量监控 — 每次反馈后检查低质量记忆
feedbackCount := selfoptimize.Dash.UsefulCount + selfoptimize.Dash.NotUsefulCount
@@ -78,7 +80,9 @@ func (fa *FeedbackAPI) MarkNotUseful(w http.ResponseWriter, r *http.Request) {
[]string{req.MemoryID},
"user_feedback", "failure", "",
)
fa.LanceDB.IncrementNotUseful(req.MemoryID)
if err := fa.LanceDB.IncrementNotUseful(req.MemoryID); err != nil {
log.Printf("[feedback] IncrementNotUseful failed %s: %v", req.MemoryID, err)
}
// Phase F: 质量监控 — negative feedback 触发低质量检测
feedbackCount := selfoptimize.Dash.UsefulCount + selfoptimize.Dash.NotUsefulCount
@@ -117,9 +121,9 @@ func (fa *FeedbackAPI) Deprecate(w http.ResponseWriter, r *http.Request) {
// POST /api/v1/feedback/correct
func (fa *FeedbackAPI) Correct(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryID string `json:"memory_id"`
NewContent string `json:"new_content"`
Source string `json:"source"`
MemoryID string `json:"memory_id"`
NewContent string `json:"new_content"`
Source string `json:"source"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
@@ -57,8 +57,8 @@ func (m *mockLance) GetSkillCandidates(minRecalls, limit int) ([]models.MemoryRe
}
func (m *mockLance) Backup(path string) error { return nil }
func (m *mockLance) GetAuditLog(limit int) ([]map[string]interface{}, error) { return nil, nil }
func (m *mockLance) IncrementUseful(id string) {}
func (m *mockLance) IncrementNotUseful(id string) {}
func (m *mockLance) IncrementUseful(id string) error { return nil }
func (m *mockLance) IncrementNotUseful(id string) error { return nil }
func (m *mockLance) UpdateMemoryContent(id, newContent, source string) error { return nil }
func (m *mockLance) InsertMemory(rec models.MemoryRecord) error { return nil }
func (m *mockLance) Insert(table string, record any) error { return nil }
+3 -3
View File
@@ -27,11 +27,11 @@ type LanceDB interface {
GetSkillCandidates(minRecalls, limit int) ([]models.MemoryRecord, error)
Backup(path string) error
GetAuditLog(limit int) ([]map[string]interface{}, error)
IncrementUseful(id string)
IncrementNotUseful(id string)
IncrementUseful(id string) error
IncrementNotUseful(id string) error
UpdateMemoryContent(id, newContent, source string) error
InsertMemory(m models.MemoryRecord) error
Search(table string, vector []float32, topK int, namespaceFilter string) ([]models.MemoryRecord, error)
Insert(table string, record any) error
Update(table, id string, fields map[string]any) error
}
}
+93 -24
View File
@@ -533,18 +533,27 @@ func (rc *RustLanceDBClient) SoftDelete(id, reason string) error {
// TODO: 需要 Rust 侧支持 lancedb_update 才能持久化 SoftDelete
// 当前仅标记内存,重启后失效。测试数据已直接通过 Python LanceDB API 清理。
// SoftDeleteBatch 批量软删(真实落库)。
//
// 2026-09-20 修复:此前**只改内存缓存 `_local.memories`,完全不写 LanceDB**,
// 缓存又只在 Insert 路径填充、Search 不填 → 批量软删实际静默失效
// (调用方拿到 updated=N 却无一行落库)。与 SoftDelete() 的区别仅在批量语义,
// 因此这里逐个委托给已验证的 SoftDelete()(它走 SoftDeleteWriteBuffer,
// 5 分钟窗口 / 256 条阈值攒批 → 单事务提交,避免每条一个 MVCC 版本)。
//
// 注:Rust 侧的 lancedb_update_batch 只支持 recall_count 累加
// (Item{id, delta}),不支持 is_deleted 批量置位,故不能下沉到那里。
func (rc *RustLanceDBClient) SoftDeleteBatch(table string, ids []string, updatedAt string) (int64, error) {
if table != "memories" {
return 0, fmt.Errorf("unsupported table: %s", table)
}
_local.mu.Lock()
defer _local.mu.Unlock()
var updated int64
for _, id := range ids {
if m, ok := _local.memories[id]; ok && !m.IsDeleted {
m.IsDeleted = true
updated++
if err := rc.SoftDelete(id, "batch:"+updatedAt); err != nil {
log.Printf("[ipc] SoftDeleteBatch persist FAILED: id=%s error=%v", id, err)
continue
}
updated++
}
return updated, nil
}
@@ -734,7 +743,18 @@ func (rc *RustLanceDBClient) GetSkillCandidates(minRecalls, limit int) ([]models
return records, nil
}
func (rc *RustLanceDBClient) Backup(path string) error { return nil }
// Backup 备份存储。
//
// 2026-09-20:此前是 `return nil` 空实现 —— 任何人调用它都会拿到"成功",
// 而实际没有任何备份发生(与 IncrementNotUseful 同类:静默假成功)。
// 当前 Rust sidecar 未提供 backup 能力,也无调用方(全仓 grep .Backup( 为 0),
// 因此这里改为**显式报错**:一旦将来有人调用,会立刻发现而不是以为备份成功。
// 需要备份时正确做法是外部对 /var/lib/memoryweave 做文件级快照
// (见 scripts/dual-backup-push.sh)。
func (rc *RustLanceDBClient) Backup(path string) error {
return fmt.Errorf("RustLanceDBClient.Backup: not supported by Rust sidecar; "+
"snapshot the data dir instead (requested path=%s)", path)
}
func (rc *RustLanceDBClient) GetAuditLog(limit int) ([]map[string]interface{}, error) {
_local.mu.RLock()
defer _local.mu.RUnlock()
@@ -743,12 +763,25 @@ func (rc *RustLanceDBClient) GetAuditLog(limit int) ([]map[string]interface{}, e
}
return _local.auditLog[:limit], nil
}
func (rc *RustLanceDBClient) IncrementUseful(id string) {
_local.mu.Lock()
defer _local.mu.Unlock()
if m, ok := _local.memories[id]; ok {
m.RecallCount++
// IncrementUseful 递增 useful_count + recall_count(用户标记"有用")。
//
// 2026-09-20 修复(双重 bug):
//
// ① 此前只改内存缓存、不落库 → 与 IncrementNotUseful 同类的假成功;
// ② 此前改的是 RecallCount,**完全没碰 UsefulCount** → 用户点"有用"
// 时 V 值评分的分子永远不涨,同时把"有用反馈"错记成一次"召回",
// 污染召回统计(召回率/新鲜度衰减都基于 recall_count)。
// 对照 MemLanceClient.IncrementUseful:UsefulCount++ 且 RecallCount++。
func (rc *RustLanceDBClient) IncrementUseful(id string) error {
if err := rc.Update("memories", id, map[string]any{
"useful_count": map[string]string{"$inc": "1"},
"recall_count": map[string]string{"$inc": "1"},
}); err != nil {
log.Printf("[ipc] IncrementUseful failed id=%s: %v", id, err)
return err
}
return nil
}
// IncrementNotUseful 递增 not_useful_count(降权)。
@@ -760,23 +793,59 @@ func (rc *RustLanceDBClient) IncrementUseful(id string) {
//
// 委托给 Update 并传 `$inc`,由 Update 转成 Rust 侧的 `col + 1` 表达式
// 实现真自增(见 Update 内注释)。
func (rc *RustLanceDBClient) IncrementNotUseful(id string) {
func (rc *RustLanceDBClient) IncrementNotUseful(id string) error {
if err := rc.Update("memories", id, map[string]any{
"not_useful_count": map[string]string{"$inc": "1"},
}); err != nil {
log.Printf("[ipc] IncrementNotUseful failed id=%s: %v", id, err)
}
}
func (rc *RustLanceDBClient) UpdateMemoryContent(id, newContent, source string) error {
_local.mu.Lock()
defer _local.mu.Unlock()
if m, ok := _local.memories[id]; ok {
m.Content = newContent
if rc.embedder != nil {
if vec, err := rc.embedder.EncodeSingle(newContent); err == nil {
m.Vector = vec
}
}
return err
}
return nil
}
// UpdateMemoryContent 修正记忆内容(重新编码向量)并**持久化**。
//
// 2026-09-20 修复:此前只改内存缓存 `_local.memories`,不写 LanceDB,
// 且缓存只在 Insert 路径填充 → 修正后的内容重启即丢、且大多数情况下
// 根本没生效(缓存未命中直接 return nil)。同时补齐 MemLanceClient
// 版本所具有的语义:Version 递增、VersionHistory 追加、Source 更新。
func (rc *RustLanceDBClient) UpdateMemoryContent(id, newContent, source string) error {
_local.mu.Lock()
var newVec []float32
if m, ok := _local.memories[id]; ok {
m.Version++
m.VersionHistory = append(m.VersionHistory, models.VersionRecord{
Version: m.Version,
Content: newContent,
Source: source,
Reason: "corrected",
Trigger: "update_memory_content",
UpdatedBy: source,
Timestamp: time.Now(),
})
m.Content = newContent
m.Source = source
if rc.embedder != nil {
if vec, err := rc.embedder.EncodeSingle(newContent); err == nil {
m.Vector = vec
newVec = vec
}
}
}
_local.mu.Unlock()
// 持久化内容(及向量,若已重编码)
vecJSON, _ := json.Marshal(newVec)
fields := map[string]any{
"content": newContent,
"source": source,
}
if len(newVec) > 0 {
fields["vector"] = string(vecJSON)
}
if err := rc.Update("memories", id, fields); err != nil {
log.Printf("[ipc] UpdateMemoryContent persist FAILED: id=%s error=%v", id, err)
return fmt.Errorf("update memory content (id=%s): %w", id, err)
}
return nil
}
+18 -16
View File
@@ -72,15 +72,15 @@ func (mlc *MemLanceClient) InsertMemory(m models.MemoryRecord) error {
}
mlc.mu.Lock()
mlc.memories[m.ID] = &memEntry{
ID: m.ID,
Content: m.Content,
Vector: vec,
Category: m.Category,
Namespace: m.Namespace,
AgentID: m.AgentID,
Tier: m.Tier,
Version: 1,
CreatedAt: time.Now(),
ID: m.ID,
Content: m.Content,
Vector: vec,
Category: m.Category,
Namespace: m.Namespace,
AgentID: m.AgentID,
Tier: m.Tier,
Version: 1,
CreatedAt: time.Now(),
}
mlc.mu.Unlock()
return nil
@@ -219,21 +219,23 @@ func (mlc *MemLanceClient) GetAuditLog(limit int) ([]map[string]interface{}, err
return mlc.auditLog[:limit], nil
}
func (mlc *MemLanceClient) IncrementUseful(id string) {
func (mlc *MemLanceClient) IncrementUseful(id string) error {
mlc.mu.Lock()
defer mlc.mu.Unlock()
if e, ok := mlc.memories[id]; ok {
e.UsefulCount++
e.RecallCount++
}
return nil
}
func (mlc *MemLanceClient) IncrementNotUseful(id string) {
func (mlc *MemLanceClient) IncrementNotUseful(id string) error {
mlc.mu.Lock()
defer mlc.mu.Unlock()
if e, ok := mlc.memories[id]; ok {
e.NotUsefulCount++
}
return nil
}
func (mlc *MemLanceClient) UpdateMemoryContent(id, newContent, source string) error {
@@ -242,10 +244,10 @@ func (mlc *MemLanceClient) UpdateMemoryContent(id, newContent, source string) er
if e, ok := mlc.memories[id]; ok {
e.Version++
e.VersionHistory = append(e.VersionHistory, map[string]interface{}{
"version": e.Version,
"content": newContent,
"source": source,
"reason": "corrected",
"version": e.Version,
"content": newContent,
"source": source,
"reason": "corrected",
"timestamp": time.Now().Format(time.RFC3339),
})
e.Content = newContent
@@ -329,4 +331,4 @@ func (mlc *MemLanceClient) SoftDeleteBatch(table string, ids []string, updatedAt
}
}
return updated, nil
}
}
@@ -27,7 +27,7 @@ func (f *fakeLDB) GetTopByQuality(agentID string, limit int) ([]models.MemoryRec
return nil, nil
}
func (f *fakeLDB) Stats() (map[string]interface{}, error) { return nil, nil }
func (f *fakeLDB) SoftDelete(id, reason string) error { return nil }
func (f *fakeLDB) SoftDelete(id, reason string) error { return nil }
func (f *fakeLDB) GetVersionHistory(id string) ([]map[string]interface{}, error) {
return nil, nil
}
@@ -37,10 +37,10 @@ func (f *fakeLDB) GetCandidatesForForgetting() ([]map[string]interface{}, error)
func (f *fakeLDB) GetSkillCandidates(minRecalls, limit int) ([]models.MemoryRecord, error) {
return nil, nil
}
func (f *fakeLDB) Backup(path string) error { return nil }
func (f *fakeLDB) Backup(path string) error { return nil }
func (f *fakeLDB) GetAuditLog(limit int) ([]map[string]interface{}, error) { return nil, nil }
func (f *fakeLDB) IncrementUseful(id string) {}
func (f *fakeLDB) IncrementNotUseful(id string) {}
func (f *fakeLDB) IncrementUseful(id string) error { return nil }
func (f *fakeLDB) IncrementNotUseful(id string) error { return nil }
func (f *fakeLDB) UpdateMemoryContent(id, newContent, source string) error {
return nil
}
+16 -12
View File
@@ -355,7 +355,9 @@ func (sc *SQLiteClient) Search(table string, vector []float32, topK int, namespa
candVec := blobToVector(vecBlob)
sim := cosineSim(vector, candVec)
importance, _ := row["importance"].(float64)
if importance <= 0 { importance = 1.0 }
if importance <= 0 {
importance = 1.0
}
createdAt, _ := time.Parse(time.RFC3339, strVal(row, "created_at"))
candidates = append(candidates, candidate{
@@ -565,15 +567,15 @@ func (sc *SQLiteClient) GetSkillCandidates(minRecalls, limit int) ([]models.Memo
}
rec := models.MemoryRecord{
ID: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 0)))),
AgentID: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 1)))),
Namespace: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 2)))),
Content: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 3)))),
Category: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 4)))),
RecallCount: int(C.sqlite3_column_int64(stmt, 5)),
QualityScore: float64(C.sqlite3_column_double(stmt, 6)),
Importance: float64(C.sqlite3_column_double(stmt, 7)),
Tier: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 8)))),
ID: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 0)))),
AgentID: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 1)))),
Namespace: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 2)))),
Content: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 3)))),
Category: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 4)))),
RecallCount: int(C.sqlite3_column_int64(stmt, 5)),
QualityScore: float64(C.sqlite3_column_double(stmt, 6)),
Importance: float64(C.sqlite3_column_double(stmt, 7)),
Tier: C.GoString((*C.char)(unsafe.Pointer(C.sqlite3_column_text(stmt, 8)))),
}
if lastRecalled := C.sqlite3_column_text(stmt, 9); lastRecalled != nil {
rec.LastRecalledAt, _ = time.Parse(time.RFC3339, C.GoString((*C.char)(unsafe.Pointer(lastRecalled))))
@@ -637,22 +639,24 @@ func (sc *SQLiteClient) GetAuditLog(limit int) ([]map[string]interface{}, error)
return results, nil
}
func (sc *SQLiteClient) IncrementUseful(id string) {
func (sc *SQLiteClient) IncrementUseful(id string) error {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.execWithArgs(
"UPDATE memories SET useful_count=useful_count+1, recall_count=recall_count+1, updated_at=? WHERE id=?",
time.Now().Format(time.RFC3339), id,
)
return nil
}
func (sc *SQLiteClient) IncrementNotUseful(id string) {
func (sc *SQLiteClient) IncrementNotUseful(id string) error {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.execWithArgs(
"UPDATE memories SET not_useful_count=not_useful_count+1, updated_at=? WHERE id=?",
time.Now().Format(time.RFC3339), id,
)
return nil
}
func (sc *SQLiteClient) UpdateMemoryContent(id, newContent, source string) error {
+26 -24
View File
@@ -20,11 +20,11 @@ import (
// SQLiteMemClient 纯 Go SQLite + 内存向量混合存储
// 适用于 Windows 或无 CGO 环境
type SQLiteMemClient struct {
*MemLanceClient // 向量搜索走内存实现
db *sql.DB
dbPath string
pendingDirty bool
dirtyMu sync.Mutex
*MemLanceClient // 向量搜索走内存实现
db *sql.DB
dbPath string
pendingDirty bool
dirtyMu sync.Mutex
}
// NewSQLiteMemClient 创建持久化存储客户端
@@ -145,22 +145,22 @@ func (sm *SQLiteMemClient) load() error {
}
sm.memories[id] = &memEntry{
ID: id,
Content: content,
AgentID: agentID,
Namespace: namespace,
Category: category,
Tier: tier,
QualityScore: qualityScore,
Vector: vec,
IsDeleted: isDeleted,
Version: version,
VersionHistory: vh,
Source: source,
CreatedAt: createdAt,
RecallCount: recallCount,
UsefulCount: usefulCount,
NotUsefulCount: notUsefulCount,
ID: id,
Content: content,
AgentID: agentID,
Namespace: namespace,
Category: category,
Tier: tier,
QualityScore: qualityScore,
Vector: vec,
IsDeleted: isDeleted,
Version: version,
VersionHistory: vh,
Source: source,
CreatedAt: createdAt,
RecallCount: recallCount,
UsefulCount: usefulCount,
NotUsefulCount: notUsefulCount,
}
}
@@ -279,7 +279,7 @@ func (sm *SQLiteMemClient) UpdateMemoryContent(id, newContent, source string) er
return nil
}
func (sm *SQLiteMemClient) IncrementUseful(id string) {
func (sm *SQLiteMemClient) IncrementUseful(id string) error {
sm.MemLanceClient.IncrementUseful(id)
sm.mu.RLock()
e := sm.memories[id]
@@ -287,9 +287,10 @@ func (sm *SQLiteMemClient) IncrementUseful(id string) {
if e != nil {
sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) IncrementNotUseful(id string) {
func (sm *SQLiteMemClient) IncrementNotUseful(id string) error {
sm.MemLanceClient.IncrementNotUseful(id)
sm.mu.RLock()
e := sm.memories[id]
@@ -297,6 +298,7 @@ func (sm *SQLiteMemClient) IncrementNotUseful(id string) {
if e != nil {
sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) Update(table, id string, fields map[string]any) error {
@@ -320,4 +322,4 @@ func (sm *SQLiteMemClient) Stats() (map[string]interface{}, error) {
"episode_count": len(sm.episodes),
"backend": "sqlite+memvector (pure Go)",
}, nil
}
}