Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

553 lines
21 KiB
Go

package vector
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
"go.kenn.io/agentsview/internal/db"
)
// refreshWatermarkKey is the metadata-table key holding the RFC3339
// ended_at high-water mark of the most recent Refresh scan, used to
// restrict the next incremental (full=false) scan to newer sessions.
const refreshWatermarkKey = "refresh_watermark"
// scopeIncludeAutomatedKey is the metadata-table key holding the
// include-automated scope ("true"/"false") the mirror was last refreshed
// under. Build compares it against the requested scope on every call: the
// scope is part of the mirror's identity, not the embedding fingerprint, so
// a change forces a full reconciliation scan rather than silently leaving
// now-out-of-scope rows (and their vectors) behind or missing newly-in-scope
// sessions an incremental scan's watermark would skip.
const scopeIncludeAutomatedKey = "scope_include_automated"
// maxSQLVars caps bind variables per IN (...) clause to stay within
// SQLite's default SQLITE_MAX_VARIABLE_NUMBER (999), mirroring
// internal/db's constant of the same purpose: a pathological refresh (a
// large eviction batch) or a deep semantic overfetch can otherwise push a
// single-shot query over SQLite's limit.
const maxSQLVars = 500
// chunkKeys invokes fn once per maxSQLVars-sized slice of keys, for callers
// binding one key per IN (...) placeholder.
func chunkKeys(keys []string, fn func(chunk []string) error) error {
for start := 0; start < len(keys); start += maxSQLVars {
end := min(start+maxSQLVars, len(keys))
if err := fn(keys[start:end]); err != nil {
return err
}
}
return nil
}
// inPlaceholders returns a "(?,?,...)" string and []any args for a slice of
// string keys, for use inside an IN (...) clause.
func inPlaceholders(keys []string) (string, []any) {
args := make([]any, len(keys))
for i, k := range keys {
args[i] = k
}
return "(" + strings.TrimSuffix(strings.Repeat("?,", len(keys)), ",") + ")", args
}
// UnitSource is the slice of the archive the mirror needs (implemented by
// *db.DB): the stream of embedding-unit documents — individual user
// messages and runs of contiguous assistant messages.
type UnitSource interface {
ScanEmbeddableUnits(ctx context.Context, since string, includeAutomated bool,
fn func(db.EmbeddableUnit) error) (string, error)
}
// RefreshStats summarizes one Refresh call: Upserted counts mirror rows
// inserted or changed (new identity or content_hash changed; this includes
// a doc_key reinserted after a same-scan slot eviction, see Refresh),
// Unchanged counts rows rescanned with an identical content_hash (e.g. an
// ordinal-only shift with no eviction involved), and Deleted counts mirror
// rows genuinely removed — a slot-evicted doc_key not reinserted anywhere
// else in the same scan, or, in full mode, an identity not seen in the scan
// at all.
type RefreshStats struct {
Upserted int
Deleted int
Unchanged int
}
// DocKey computes the mirror's document identity for a unit: a source_uuid
// (the unit's first member's, for a run) keeps the key stable across
// ordinal-shifting rewrites (compaction, resync) and across later run
// members appending, splitting off, or changing; its absence falls back to
// a session+ordinal key. kind selects the prefix scheme: "run" units use
// "r:" (uuid) / "ro:" (ordinal fallback), "user" units use "u:" / "o:".
//
// The messages schema permits more than one message in a session to share a
// non-empty source_uuid, so occurrence disambiguates them: it is the 1-based
// count of how many times (sessionID, sourceUUID) has been seen so far in
// scan order, shared across unit kinds. The first occurrence keeps the
// plain "<prefix><session>:<uuid>" key; later occurrences append
// "#<occurrence>". Since the scan is ordered by (session_id, ordinal) of
// each unit's first member, occurrence assignment is deterministic and
// stable across resyncs. occurrence is ignored when sourceUUID is empty.
//
// sessionID and sourceUUID are percent-escaped (escapeDocKeyComponent)
// before joining so the ":" and "#" delimiters, and any literal "%", inside
// either component cannot be confused with the key's own structure — e.g.
// source_uuid "dup#2" at its first occurrence would otherwise collide with
// source_uuid "dup" at its second occurrence.
func DocKey(kind, sessionID, sourceUUID string, ordinal, occurrence int) string {
uuidPrefix, ordinalPrefix := "u:", "o:"
if kind == "run" {
uuidPrefix, ordinalPrefix = "r:", "ro:"
}
session := escapeDocKeyComponent(sessionID)
if sourceUUID != "" {
uuid := escapeDocKeyComponent(sourceUUID)
if occurrence > 1 {
return uuidPrefix + session + ":" + uuid + "#" + strconv.Itoa(occurrence)
}
return uuidPrefix + session + ":" + uuid
}
return ordinalPrefix + session + ":" + strconv.Itoa(ordinal)
}
// escapeDocKeyComponent percent-encodes the characters DocKey uses as
// delimiters — ':', '#', and '%' itself — so a session_id or source_uuid
// containing them cannot be mistaken for key structure, keeping DocKey
// injective.
func escapeDocKeyComponent(s string) string {
if !strings.ContainsAny(s, "%:#") {
return s
}
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
switch r {
case '%', ':', '#':
fmt.Fprintf(&b, "%%%02X", r)
default:
b.WriteRune(r)
}
}
return b.String()
}
// contentHash returns the mirror's content_hash for content: kit's
// sqlitevec store uses it as the revision column, so any change here
// invalidates the embedding stamp and marks the document pending.
func contentHash(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
}
// Refresh reconciles the mirror against src. full=true scans the entire
// archive (since="") and additionally deletes mirror rows (and their
// vectors, via store.DeleteVectors) whose identity was not seen in the
// scan; full=false scans only sessions newer than the stored watermark
// (metadata-table key "refresh_watermark") and only upserts, leaving that
// reconciliation to a subsequent full refresh. includeAutomated
// is passed through to src.ScanEmbeddableUnits: false excludes automated
// sessions from the scan entirely, so their mirror rows are absent (and, in
// full mode, reconciled away) rather than merely unembedded. Either mode
// also resolves same-scan slot evictions (see evictSlotOccupant) once the
// scan completes: a UUID-keyed doc_key evicted from a (session_id, ordinal)
// slot it no longer occupies is deleted via store.DeleteVectors only if it
// was not reinserted elsewhere in the same scan, so a row that merely
// shifted (or was displaced in a shift cascade) keeps its embeddings. The
// watermark is advanced to the scan's max ended_at afterwards.
func (ix *Index) Refresh(
ctx context.Context, src UnitSource, full, includeAutomated bool,
) (RefreshStats, error) {
if err := ix.requireWritable(); err != nil {
return RefreshStats{}, err
}
since := ""
if !full {
watermark, err := ix.refreshWatermark(ctx)
if err != nil {
return RefreshStats{}, err
}
since = watermark
}
var stats RefreshStats
seen := make(map[string]struct{})
occurrences := make(map[string]int)
evicted := make(map[string]struct{})
sentinel, err := ix.parkingFloor(ctx)
if err != nil {
return RefreshStats{}, err
}
maxEnded, err := src.ScanEmbeddableUnits(ctx, since, includeAutomated, func(u db.EmbeddableUnit) error {
occurrence := 1
if u.SourceUUID != "" {
occKey := u.SessionID + "\x00" + u.SourceUUID
occurrences[occKey]++
occurrence = occurrences[occKey]
}
key := DocKey(u.Kind, u.SessionID, u.SourceUUID, u.Ordinal, occurrence)
unchanged, evictedKeys, err := ix.upsertMirrorRow(ctx, key, u, &sentinel)
if err != nil {
return fmt.Errorf("upserting mirror row %s: %w", key, err)
}
for _, k := range evictedKeys {
evicted[k] = struct{}{}
}
if unchanged {
stats.Unchanged++
} else {
stats.Upserted++
}
seen[key] = struct{}{}
return nil
})
if err != nil {
return RefreshStats{}, fmt.Errorf("scanning embeddable units: %w", err)
}
// finalizeEvictions must run before full-mode reconcileDeletions: an
// evicted key that never reappears anywhere in the scan is absent from
// seen too, so reconcileDeletions would otherwise also treat its (still
// present, sentinel-parked) row as a vanished identity and delete it a
// second time. Resolving evictions first means the row is gone by the
// time reconcileDeletions scans the mirror, so it is never counted
// there. finalizeEvictions also guards against re-deleting an
// already-absent key on its own (see its doc comment), so this ordering
// and that guard together make Refresh's accounting robust regardless
// of which pass would otherwise see the row first.
finalized, err := ix.finalizeEvictions(ctx, evicted)
if err != nil {
return RefreshStats{}, err
}
stats.Deleted += finalized
if full {
deleted, err := ix.reconcileDeletions(ctx, seen)
if err != nil {
return RefreshStats{}, err
}
stats.Deleted += deleted
}
if maxEnded != "" {
if err := ix.setRefreshWatermark(ctx, maxEnded); err != nil {
return RefreshStats{}, err
}
}
return stats, nil
}
// upsertMirrorRow evicts any row occupying the same (session_id, ordinal)
// slot under a different doc_key, then upserts key's row from u. It returns
// whether the row's content_hash was unchanged (a no-op update, e.g. an
// ordinal-only shift) and the doc_key(s) the slot eviction displaced (0 or
// 1), for the caller to reconcile once the whole scan completes. sentinel
// is a per-Refresh-call counter evictSlotOccupant uses to park a displaced
// row at a unique negative ordinal; see evictSlotOccupant.
func (ix *Index) upsertMirrorRow(
ctx context.Context, key string, u db.EmbeddableUnit, sentinel *int,
) (unchanged bool, evicted []string, err error) {
evicted, err = ix.evictSlotOccupant(ctx, key, u.SessionID, u.Ordinal, sentinel)
if err != nil {
return false, nil, err
}
var existingHash sql.NullString
err = ix.db.QueryRowContext(ctx,
`SELECT content_hash FROM `+ix.spec.DocsTable+` WHERE doc_key = ?`, key,
).Scan(&existingHash)
if err != nil && err != sql.ErrNoRows {
return false, evicted, fmt.Errorf("reading existing content hash: %w", err)
}
hash := contentHash(u.Content)
unchanged = existingHash.Valid && existingHash.String == hash
offsets, err := marshalOffsets(u.Offsets)
if err != nil {
return false, evicted, err
}
if _, err := ix.db.ExecContext(ctx, `
INSERT INTO `+ix.spec.DocsTable+` (doc_key, session_id, source_uuid, ordinal, ordinal_end,
subordinate, offsets, content, content_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(doc_key) DO UPDATE SET
session_id = excluded.session_id,
ordinal = excluded.ordinal,
ordinal_end = excluded.ordinal_end,
subordinate = excluded.subordinate,
offsets = excluded.offsets,
content = excluded.content,
content_hash = excluded.content_hash`,
key, u.SessionID, u.SourceUUID, u.Ordinal, u.OrdinalEnd,
u.Subordinate, offsets, u.Content, hash,
); err != nil {
return false, evicted, fmt.Errorf("upserting row: %w", err)
}
return unchanged, evicted, nil
}
// marshalOffsets encodes a unit's member offsets for the mirror's offsets
// column. A nil slice (every user doc; see db.EmbeddableUnit.Offsets) is
// stored as the schema's canonical empty array "[]" rather than
// encoding/json's "null" for a nil slice, matching the column's DEFAULT and
// sparing readers a null case.
func marshalOffsets(offsets []db.UnitOffset) (string, error) {
if offsets == nil {
return "[]", nil
}
encoded, err := json.Marshal(offsets)
if err != nil {
return "", fmt.Errorf("marshaling unit offsets: %w", err)
}
return string(encoded), nil
}
// evictSlotOccupant parks the mirror row of any doc_key occupying
// (sessionID, ordinal) under a key other than key at a unique negative
// ordinal, guarding the mirror's unique index before an upsert lands on
// that slot without deleting the row outright. Message ordinals are always
// >= 0, so a negative ordinal can never collide with a real one or with
// another parked row: sentinel is a counter shared across one Refresh
// scan, decremented per eviction to keep every parked ordinal distinct.
//
// The row is left in place, not deleted, so that if the same doc_key is
// reinserted later in the same scan (a stable UUID-keyed identity that
// merely shifted position in a cascade), upsertMirrorRow's ON CONFLICT(doc_key)
// path updates it in place and never touches the embed_gen column kit's
// SaveVectors stamped it with — a fresh INSERT would reset embed_gen to
// NULL and silently uncover the document. Whether an evicted key is
// genuinely gone or was reinserted is not decidable until the whole scan
// finishes, so store.DeleteVectors and the row's actual removal are
// deferred to Refresh's post-scan finalizeEvictions pass.
// parkingFloor returns the starting value for Refresh's parking sentinel:
// 0 when the mirror holds no parked rows, otherwise the most negative
// parked ordinal already present. Parking writes are individual autocommit
// updates, so a Refresh interrupted between evictSlotOccupant and
// finalizeEvictions leaves rows parked at negative ordinals; a later run
// restarting its sentinel at 0 could then park a row in the same session at
// an already-taken negative ordinal and fail the unique (session_id,
// ordinal) index — deterministically on every retry, wedging refreshes.
// Seeding below the leftover floor keeps every parked ordinal unique across
// runs; the leftovers themselves self-heal (reinserted rows overwrite their
// ordinal, full-mode reconciliation deletes vanished ones).
func (ix *Index) parkingFloor(ctx context.Context) (int, error) {
var floor sql.NullInt64
if err := ix.db.QueryRowContext(ctx,
`SELECT MIN(ordinal) FROM `+ix.spec.DocsTable+` WHERE ordinal < 0`,
).Scan(&floor); err != nil {
return 0, fmt.Errorf("reading parked-ordinal floor: %w", err)
}
if !floor.Valid {
return 0, nil
}
return int(floor.Int64), nil
}
func (ix *Index) evictSlotOccupant(
ctx context.Context, key, sessionID string, ordinal int, sentinel *int,
) ([]string, error) {
rows, err := ix.db.QueryContext(ctx,
`SELECT doc_key FROM `+ix.spec.DocsTable+`
WHERE session_id = ? AND ordinal = ? AND doc_key != ?`,
sessionID, ordinal, key)
if err != nil {
return nil, fmt.Errorf("finding slot occupant: %w", err)
}
var evictKeys []string
for rows.Next() {
var k string
if err := rows.Scan(&k); err != nil {
rows.Close()
return nil, fmt.Errorf("scanning slot occupant: %w", err)
}
evictKeys = append(evictKeys, k)
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, fmt.Errorf("iterating slot occupants: %w", err)
}
rows.Close()
for _, k := range evictKeys {
*sentinel--
if _, err := ix.db.ExecContext(ctx,
`UPDATE `+ix.spec.DocsTable+` SET ordinal = ? WHERE doc_key = ?`, *sentinel, k,
); err != nil {
return nil, fmt.Errorf("evicting slot occupant %s: %w", k, err)
}
}
return evictKeys, nil
}
// finalizeEvictions resolves every doc_key evictSlotOccupant displaced
// during one Refresh scan: a key whose ordinal is still negative (the
// sentinel evictSlotOccupant parked it at) once the scan is done was never
// reinserted, so it is genuinely gone — its vectors and stamps are deleted
// via store.DeleteVectors, and its mirror row is finally removed. kit's
// store keeps orphaned vectors occupying KNN LIMIT slots even though
// QueryGeneration filters them from hits, so this cleanup matters even
// though the row itself is inert. A key whose ordinal was overwritten back
// to a real (non-negative) value was reinserted under its own doc_key later
// in the same scan — it merely shifted position and keeps its row and
// embeddings untouched.
//
// A key already absent from the mirror entirely (ok is false below) is
// skipped rather than deleted again: Refresh runs finalizeEvictions before
// full-mode reconcileDeletions specifically so this case shouldn't arise
// within one call, but the guard makes the accounting correct regardless of
// call order — an evicted key that never reappears in the scan is also
// absent from seen, so without this guard reconcileDeletions would delete
// the row once and finalizeEvictions would count deleting it again.
func (ix *Index) finalizeEvictions(ctx context.Context, evicted map[string]struct{}) (int, error) {
if len(evicted) == 0 {
return 0, nil
}
keys := make([]string, 0, len(evicted))
for k := range evicted {
keys = append(keys, k)
}
ordinals, err := ix.currentOrdinals(ctx, keys)
if err != nil {
return 0, err
}
var deleted int
for _, key := range keys {
ordinal, ok := ordinals[key]
if !ok {
continue // already removed from the mirror; nothing left to do
}
if ordinal >= 0 {
continue // reinserted under its own doc_key later in the same scan
}
if err := ix.store.DeleteVectors(ctx, key); err != nil {
return deleted, fmt.Errorf("deleting evicted vectors for %s: %w", key, err)
}
if _, err := ix.db.ExecContext(ctx,
`DELETE FROM `+ix.spec.DocsTable+` WHERE doc_key = ?`, key,
); err != nil {
return deleted, fmt.Errorf("deleting evicted mirror row %s: %w", key, err)
}
deleted++
}
return deleted, nil
}
// currentOrdinals returns the current ordinal of each of keys that is still
// present in the mirror; a key absent from the result was somehow
// already removed from the mirror. keys is queried in maxSQLVars-sized
// chunks: a large eviction batch in a single Refresh scan can otherwise
// exceed SQLite's bind-variable limit.
func (ix *Index) currentOrdinals(ctx context.Context, keys []string) (map[string]int, error) {
ordinals := make(map[string]int, len(keys))
err := chunkKeys(keys, func(chunk []string) error {
placeholders, args := inPlaceholders(chunk)
rows, err := ix.db.QueryContext(ctx,
`SELECT doc_key, ordinal FROM `+ix.spec.DocsTable+` WHERE doc_key IN `+placeholders, args...)
if err != nil {
return fmt.Errorf("checking evicted doc_key ordinals: %w", err)
}
for rows.Next() {
var k string
var ordinal int
if err := rows.Scan(&k, &ordinal); err != nil {
rows.Close()
return fmt.Errorf("scanning evicted doc_key ordinal: %w", err)
}
ordinals[k] = ordinal
}
if err := rows.Err(); err != nil {
rows.Close()
return fmt.Errorf("checking evicted doc_key ordinals: %w", err)
}
return rows.Close()
})
if err != nil {
return nil, err
}
return ordinals, nil
}
// reconcileDeletions deletes every mirror row (and its vectors) whose
// doc_key was not seen in a full scan.
func (ix *Index) reconcileDeletions(
ctx context.Context, seen map[string]struct{},
) (int, error) {
rows, err := ix.db.QueryContext(ctx, `SELECT doc_key FROM `+ix.spec.DocsTable)
if err != nil {
return 0, fmt.Errorf("listing mirror doc_keys: %w", err)
}
var vanished []string
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
rows.Close()
return 0, fmt.Errorf("scanning mirror doc_key: %w", err)
}
if _, ok := seen[key]; !ok {
vanished = append(vanished, key)
}
}
if err := rows.Err(); err != nil {
rows.Close()
return 0, fmt.Errorf("iterating mirror doc_keys: %w", err)
}
rows.Close()
for _, key := range vanished {
if err := ix.store.DeleteVectors(ctx, key); err != nil {
return 0, fmt.Errorf("deleting vectors for %s: %w", key, err)
}
if _, err := ix.db.ExecContext(ctx,
`DELETE FROM `+ix.spec.DocsTable+` WHERE doc_key = ?`, key,
); err != nil {
return 0, fmt.Errorf("deleting mirror row %s: %w", key, err)
}
}
return len(vanished), nil
}
// refreshWatermark reads the stored refresh watermark, returning "" when
// none has been recorded yet.
func (ix *Index) refreshWatermark(ctx context.Context) (string, error) {
value, _, err := ix.metaGet(ctx, refreshWatermarkKey)
return value, err
}
// setRefreshWatermark advances the stored refresh watermark to value.
func (ix *Index) setRefreshWatermark(ctx context.Context, value string) error {
return ix.metaSet(ctx, refreshWatermarkKey, value)
}
// storedIncludeAutomatedScope reads the include-automated scope the mirror
// was last refreshed under. ok is false when no scope has ever been stored
// (the mirror's first build), in which case value is meaningless.
func (ix *Index) storedIncludeAutomatedScope(ctx context.Context) (value, ok bool, err error) {
raw, ok, err := ix.metaGet(ctx, scopeIncludeAutomatedKey)
if err != nil {
return false, false, fmt.Errorf("reading stored include-automated scope: %w", err)
}
if !ok {
return false, false, nil
}
parsed, err := strconv.ParseBool(raw)
if err != nil {
return false, false, fmt.Errorf("parsing stored include-automated scope %q: %w", raw, err)
}
return parsed, true, nil
}
// setIncludeAutomatedScope records value as the include-automated scope the
// mirror was most recently refreshed under.
func (ix *Index) setIncludeAutomatedScope(ctx context.Context, value bool) error {
return ix.metaSet(ctx, scopeIncludeAutomatedKey, strconv.FormatBool(value))
}