Merge the memory and store sweep (#244)
The embedder prefix audit came back clean, which was the one finding worth escalating. Every EmbedQuery, EmbedPassage and Embed call site across internal/store, internal/memory and their cmd/mavend callers agrees. No naked Embed on a note. ReembedAll and RepairFactVectors each ran an identical select and scan over memory_vectors before diverging on what to do with the row. One allMemVectorMetas now, parameterized over a small interface so it serves backfill's transaction and factvectors' plain read alike. Two swallowed errors. AcceptProposedRoutine read RowsAffected with a discarded error where every other call in the same file checks it, so a driver error read as zero rows. MarkAcked did the same, and the branch it fed was dead, since both arms returned nil. The swallowed error and the branch went together. The agent refuted the rest of the brief. Repeated scans and swallowed errors were one instance each rather than the pattern tasks.go showed. Both packages carry per-type scan helpers already, and every magic value is already named with its reason beside it, which reads as the residue of earlier sweep waves. (V-581)
This commit is contained in:
@@ -55,17 +55,15 @@ func (s *Store) LastSent(ctx context.Context, key string) (time.Time, error) {
|
||||
// alarm (voice acknowledgment, Telegram callback, etc.).
|
||||
func (s *Store) MarkAcked(ctx context.Context, key string) error {
|
||||
now := time.Now()
|
||||
res, err := s.db.ExecContext(ctx,
|
||||
// No pending nudges is not an error — already acked or never sent — so the
|
||||
// rows-affected count is not read at all: every outcome below this line is
|
||||
// the same nil.
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
`UPDATE nudges SET outcome = 'acted', outcome_ts = ?
|
||||
WHERE rule = ? AND channel = 'telegram' AND outcome = 'pending'`,
|
||||
now.UnixMilli(), key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark acked %s: %w", key, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
// no pending nudges — already acked or never sent; not an error.
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@@ -103,30 +102,16 @@ func (s *Store) ReembedAll(ctx context.Context, currentID string, embed EmbedFun
|
||||
id, text, kind string
|
||||
}
|
||||
var vecs []vecRow
|
||||
rows, err = tx.QueryContext(ctx, `SELECT id, meta FROM memory_vectors`)
|
||||
memRows, err := allMemVectorMetas(ctx, tx)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("reembed: read memory vectors: %w", err)
|
||||
return res, fmt.Errorf("reembed: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var id, metaJSON string
|
||||
if err := rows.Scan(&id, &metaJSON); err != nil {
|
||||
rows.Close()
|
||||
return res, fmt.Errorf("reembed: memory row: %w", err)
|
||||
}
|
||||
meta := map[string]string{}
|
||||
if err := json.Unmarshal([]byte(metaJSON), &meta); err != nil {
|
||||
rows.Close()
|
||||
return res, fmt.Errorf("reembed: meta for %q: %w", id, err)
|
||||
}
|
||||
if meta["text"] == "" {
|
||||
for _, v := range memRows {
|
||||
if v.Meta["text"] == "" {
|
||||
res.NoText++
|
||||
continue
|
||||
}
|
||||
vecs = append(vecs, vecRow{id: id, text: meta["text"], kind: meta["type"]})
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return res, fmt.Errorf("reembed: memory vectors: %w", err)
|
||||
vecs = append(vecs, vecRow{id: v.ID, text: v.Meta["text"], kind: v.Meta["type"]})
|
||||
}
|
||||
|
||||
for _, v := range vecs {
|
||||
|
||||
@@ -64,9 +64,9 @@ func (s *Store) RepairFactVectors(ctx context.Context, embed EmbedFunc) (FactVec
|
||||
return res, nil
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, meta FROM memory_vectors`)
|
||||
memRows, err := allMemVectorMetas(ctx, s.db)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("repair fact vectors: read: %w", err)
|
||||
return res, fmt.Errorf("repair fact vectors: %w", err)
|
||||
}
|
||||
type factVec struct {
|
||||
id, key string
|
||||
@@ -75,33 +75,19 @@ func (s *Store) RepairFactVectors(ctx context.Context, embed EmbedFunc) (FactVec
|
||||
}
|
||||
var vecs []factVec
|
||||
newest := map[string]int64{} // key → newest ts seen for it
|
||||
for rows.Next() {
|
||||
var id, metaJSON string
|
||||
if err := rows.Scan(&id, &metaJSON); err != nil {
|
||||
rows.Close()
|
||||
return res, fmt.Errorf("repair fact vectors: row: %w", err)
|
||||
}
|
||||
meta := map[string]string{}
|
||||
if err := json.Unmarshal([]byte(metaJSON), &meta); err != nil {
|
||||
rows.Close()
|
||||
return res, fmt.Errorf("repair fact vectors: meta for %q: %w", id, err)
|
||||
}
|
||||
if meta["type"] != "fact" {
|
||||
for _, v := range memRows {
|
||||
if v.Meta["type"] != "fact" {
|
||||
continue
|
||||
}
|
||||
key, ts, ok := splitFactVectorID(id)
|
||||
key, ts, ok := splitFactVectorID(v.ID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
vecs = append(vecs, factVec{id: id, key: key, meta: meta, ts: ts})
|
||||
vecs = append(vecs, factVec{id: v.ID, key: key, meta: v.Meta, ts: ts})
|
||||
if ts > newest[key] {
|
||||
newest[key] = ts
|
||||
}
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return res, fmt.Errorf("repair fact vectors: rows: %w", err)
|
||||
}
|
||||
|
||||
for _, v := range vecs {
|
||||
drop := v.ts < newest[v.key]
|
||||
|
||||
@@ -169,6 +169,49 @@ func (m *MemoryStore) DeletePrefix(ctx context.Context, prefix string) (int64, e
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// memVectorRow is one memory_vectors row with its meta blob decoded — the
|
||||
// shape both ReembedAll (backfill.go) and RepairFactVectors (factvectors.go)
|
||||
// read the whole table as, before each decides what to do with a row on its
|
||||
// own terms (one keys off meta["text"], the other off meta["type"] and the
|
||||
// id's embedded key/timestamp). Query-then-scan was duplicated across the two
|
||||
// before this, id-for-id.
|
||||
type memVectorRow struct {
|
||||
ID string
|
||||
Meta map[string]string
|
||||
}
|
||||
|
||||
// queryContexter is the common surface *sql.DB and *sql.Tx share that
|
||||
// allMemVectorMetas needs. ReembedAll reads inside a transaction so its
|
||||
// migration is atomic; RepairFactVectors reads directly off the db handle.
|
||||
type queryContexter interface {
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
// allMemVectorMetas reads every memory_vectors row and decodes its meta blob.
|
||||
func allMemVectorMetas(ctx context.Context, q queryContexter) ([]memVectorRow, error) {
|
||||
rows, err := q.QueryContext(ctx, `SELECT id, meta FROM memory_vectors`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read memory vectors: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []memVectorRow
|
||||
for rows.Next() {
|
||||
var id, metaJSON string
|
||||
if err := rows.Scan(&id, &metaJSON); err != nil {
|
||||
return nil, fmt.Errorf("memory vector row: %w", err)
|
||||
}
|
||||
meta := map[string]string{}
|
||||
if err := json.Unmarshal([]byte(metaJSON), &meta); err != nil {
|
||||
return nil, fmt.Errorf("meta for %q: %w", id, err)
|
||||
}
|
||||
out = append(out, memVectorRow{ID: id, Meta: meta})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("memory vectors: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// escapeLike neutralises the LIKE wildcards in a literal prefix.
|
||||
func escapeLike(s string) string {
|
||||
r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
|
||||
|
||||
@@ -187,7 +187,10 @@ func (s *Store) AcceptProposedRoutine(ctx context.Context, id int64, ts time.Tim
|
||||
if err != nil {
|
||||
return fmt.Errorf("accept proposed routine: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("accept proposed routine: rows affected: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("%w: id=%d not in 'proposed' status", ErrProposedRoutineNotFound, id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user