Files
OmniRoute/src/lib/db/memoryVec.ts
diegosouzapw b9f93a5c07 feat(memory): add migration 073, memoryVec CRUD module, and localDb re-export (plan 21 F2)
- 073_memory_vec.sql: creates memory_vec_meta singleton table (active_dim,
  embedding_signature, last_reset_at, vec_loaded) and adds needs_reindex column
  to memories table with a partial index; idempotent via CREATE IF NOT EXISTS +
  INSERT OR IGNORE + migration runner's duplicate-column-name guard
- src/lib/db/memoryVec.ts: implements 6 CRUD functions per §3.8 contract
  (getMemoryVecMeta, setMemoryVecMeta, markMemoryNeedsReindex,
  markAllMemoriesNeedReindex, getMemoryReindexQueue, countMemoryReindexPending)
- src/lib/localDb.ts: adds re-export block for the 6 functions (Hard Rule #2)
- .env.example: documents 7 new MEMORY_* env vars per §3.9
- tests/unit/memory-vec-meta.test.ts: 7 tests (meta get/set, migration idempotency)
- tests/unit/memory-needs-reindex.test.ts: 12 tests (mark/unmark, markAll, queue)
2026-05-27 23:16:05 -03:00

139 lines
4.2 KiB
TypeScript

/**
* db/memoryVec.ts — CRUD for memory vector metadata and reindex state.
*
* Plan 21 — Memory Engine Redesign.
* Raw SQL is allowed here (Hard Rule #5 — src/lib/db/ domain module).
*
* This module manages:
* - `memory_vec_meta`: singleton row tracking the active embedding dim/signature/reset
* - `memories.needs_reindex`: flag for lazy backfill of missing/stale vectors
*/
import { getDbInstance } from "./core";
// ──────────────── Types ────────────────
export interface MemoryVecMeta {
activeDim: number | null;
embeddingSignature: string | null;
lastResetAt: string | null;
vecLoaded: boolean;
}
// ──────────────── memory_vec_meta ────────────────
/**
* Get the singleton memory_vec_meta row.
* Returns defaults if the row is absent (e.g. migration not yet applied on
* an in-memory test DB that ran without the migration file).
*/
export function getMemoryVecMeta(): MemoryVecMeta {
const db = getDbInstance();
const row = db
.prepare(
"SELECT active_dim, embedding_signature, last_reset_at, vec_loaded FROM memory_vec_meta WHERE id = 1"
)
.get() as
| {
active_dim: number | null;
embedding_signature: string | null;
last_reset_at: string | null;
vec_loaded: number;
}
| undefined;
if (!row) {
return {
activeDim: null,
embeddingSignature: null,
lastResetAt: null,
vecLoaded: false,
};
}
return {
activeDim: row.active_dim,
embeddingSignature: row.embedding_signature,
lastResetAt: row.last_reset_at,
vecLoaded: row.vec_loaded === 1,
};
}
/**
* Update one or more fields in the singleton memory_vec_meta row.
* Uses INSERT OR REPLACE to handle the case where the row is missing
* (e.g. called before or during migration on a test DB).
*/
export function setMemoryVecMeta(meta: Partial<MemoryVecMeta>): void {
const db = getDbInstance();
// Read current values first so we can merge (partial update pattern).
const current = getMemoryVecMeta();
const activeDim = "activeDim" in meta ? meta.activeDim ?? null : current.activeDim;
const embeddingSignature =
"embeddingSignature" in meta
? meta.embeddingSignature ?? null
: current.embeddingSignature;
const lastResetAt =
"lastResetAt" in meta ? meta.lastResetAt ?? null : current.lastResetAt;
const vecLoaded =
"vecLoaded" in meta ? (meta.vecLoaded ? 1 : 0) : current.vecLoaded ? 1 : 0;
db.prepare(
`INSERT OR REPLACE INTO memory_vec_meta
(id, active_dim, embedding_signature, last_reset_at, vec_loaded)
VALUES (1, ?, ?, ?, ?)`
).run(activeDim, embeddingSignature, lastResetAt, vecLoaded);
}
// ──────────────── memories.needs_reindex ────────────────
/**
* Mark a single memory as needing reindex (or clear the flag).
*/
export function markMemoryNeedsReindex(id: string, needs: boolean): void {
const db = getDbInstance();
db.prepare("UPDATE memories SET needs_reindex = ? WHERE id = ?").run(needs ? 1 : 0, id);
}
/**
* Mark ALL memories as needing reindex.
* Returns the number of rows affected.
*/
export function markAllMemoriesNeedReindex(): number {
const db = getDbInstance();
const result = db.prepare("UPDATE memories SET needs_reindex = 1").run();
return result.changes;
}
/**
* Get a batch of memories that need reindex, ordered by creation date ascending.
* Returns id, content, and key for each memory so the vector can be regenerated.
*/
export function getMemoryReindexQueue(
limit: number
): Array<{ id: string; content: string; key: string }> {
const db = getDbInstance();
return db
.prepare(
`SELECT id, content, COALESCE(key, '') AS key
FROM memories
WHERE needs_reindex = 1
ORDER BY created_at ASC
LIMIT ?`
)
.all(limit) as Array<{ id: string; content: string; key: string }>;
}
/**
* Count how many memories currently have needs_reindex = 1.
*/
export function countMemoryReindexPending(): number {
const db = getDbInstance();
const row = db
.prepare("SELECT COUNT(*) AS cnt FROM memories WHERE needs_reindex = 1")
.get() as { cnt: number };
return row.cnt;
}