store: voiding a fact drops its memory vectors (V-470)
Revert voided the fact row and left the vector, so recall kept serving the voided fact's utterance and the documented repair reported success on a box that stayed broken. There was no way to repair a poisoned box at all. DeletePrefix covers every vector for the key, earlier rows included: their values are superseded, and a superseded value has no business claiming a turn. It is best-effort — the audit trail is already committed, and a fact that is voided but still recallable beats a void that failed.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -332,6 +333,20 @@ func (s *Store) VoidLatestFact(ctx context.Context, key, source string, ts time.
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("void: last insert id: %w", err)
|
||||
}
|
||||
// The other half of the repair (#470). A fact reaches recall through a
|
||||
// vector keyed `fact:<key>:<unix>`, holding the utterance that wrote it.
|
||||
// Voiding the row alone left that vector answering questions, so revert
|
||||
// reported success on a box that stayed broken. Deleting every vector for
|
||||
// the key covers the earlier rows too: their values are superseded, and a
|
||||
// superseded value has no business claiming a turn.
|
||||
//
|
||||
// Best-effort by design: the audit trail is already committed, and a fact
|
||||
// that is voided but still recallable is better than a void that failed.
|
||||
if n, derr := s.VectorMemory().DeletePrefix(ctx, "fact:"+key+":"); derr != nil {
|
||||
log.Printf("store: void %q: memory vectors survive: %v", key, derr)
|
||||
} else if n > 0 {
|
||||
log.Printf("store: void %q: dropped %d memory vector(s)", key, n)
|
||||
}
|
||||
return oldID, newID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,27 @@ func (m *MemoryStore) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePrefix removes every vector whose id starts with prefix and returns
|
||||
// how many went. Same escaping as ByPrefix, so a key containing % or _ cannot
|
||||
// widen the delete.
|
||||
//
|
||||
// It exists for the repair half of a revert (#470). Voiding a fact row left
|
||||
// its vector in the index, so recall kept serving the voided fact's utterance
|
||||
// and the documented repair did not repair.
|
||||
func (m *MemoryStore) DeletePrefix(ctx context.Context, prefix string) (int64, error) {
|
||||
pattern := escapeLike(prefix) + "%"
|
||||
res, err := m.db.ExecContext(ctx,
|
||||
`DELETE FROM memory_vectors WHERE id LIKE ? ESCAPE '\'`, pattern)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("memory: delete prefix %q: %w", prefix, err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("memory: delete prefix %q: rows affected: %w", prefix, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// escapeLike neutralises the LIKE wildcards in a literal prefix.
|
||||
func escapeLike(s string) string {
|
||||
r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Stage 3 of #470: reverting a fact reported success and left the vector that
|
||||
// was answering questions, so the documented repair did not repair.
|
||||
func TestVoidLatestFactDropsMemoryVectors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
now := time.Now()
|
||||
mem := s.VectorMemory()
|
||||
|
||||
if _, err := s.WriteFact(ctx, now, KindSelf, "go_version", `"1.20"`, "tap:voice", 1.0, sql.NullInt64{}); err != nil {
|
||||
t.Fatalf("WriteFact: %v", err)
|
||||
}
|
||||
// The id shape actionFact writes: fact:<key>:<unix>.
|
||||
if err := mem.Insert(ctx, "fact:go_version:1", []float32{1, 0, 0}, map[string]string{
|
||||
"type": "fact", "text": "какая последняя версия языка Go?",
|
||||
}); err != nil {
|
||||
t.Fatalf("Insert: %v", err)
|
||||
}
|
||||
// A vector for another key must survive the void.
|
||||
if err := mem.Insert(ctx, "fact:water:1", []float32{0, 1, 0}, map[string]string{
|
||||
"type": "fact", "text": "запиши что я пил воду",
|
||||
}); err != nil {
|
||||
t.Fatalf("Insert: %v", err)
|
||||
}
|
||||
|
||||
if _, _, err := s.VoidLatestFact(ctx, "go_version", "feedback", now.Add(time.Minute)); err != nil {
|
||||
t.Fatalf("VoidLatestFact: %v", err)
|
||||
}
|
||||
|
||||
got, err := mem.ByPrefix(ctx, "fact:")
|
||||
if err != nil {
|
||||
t.Fatalf("ByPrefix: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].ID != "fact:water:1" {
|
||||
t.Fatalf("after the void the index holds %+v; want only fact:water:1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePrefixDoesNotWidenOnWildcards(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
mem := s.VectorMemory()
|
||||
|
||||
for _, id := range []string{"fact:a_b:1", "fact:axb:1"} {
|
||||
if err := mem.Insert(ctx, id, []float32{1, 0}, map[string]string{"type": "fact"}); err != nil {
|
||||
t.Fatalf("Insert %q: %v", id, err)
|
||||
}
|
||||
}
|
||||
n, err := mem.DeletePrefix(ctx, "fact:a_b:")
|
||||
if err != nil {
|
||||
t.Fatalf("DeletePrefix: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("deleted %d rows; the _ in the key must not match x", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user