Skip to content

feat(memory): memory engine redesign — sqlite-vec + hybrid RRF + Studio UI (plan 21) - #2873

Merged
diegosouzapw merged 29 commits into
release/v3.8.8from
refactor/pages-v3-21-memory-engine-redesign
May 30, 2026
Merged

diegosouzapw merged 29 commits into
release/v3.8.8from
refactor/pages-v3-21-memory-engine-redesign

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Implementa o plano 21 (Memory Engine Redesign) — refaz a tela /dashboard/memory como Studio com 3 abas, conserta toda a dívida técnica do subsistema, e entrega busca semântica REAL embarcada via sqlite-vec + hybrid RRF (k=60) com camada de embedding multi-source (provider remoto · static potion local · transformers.js MiniLM opt-in), rerank opcional OFF por default, e Qdrant como tier 2 opt-in.

Entregas principais

  • UI Studio (/dashboard/memory) — 3 abas (Memórias / Playground / Engine) + Memory Concept Card explicativo.
  • Memory Engine Status — cadeia visual keyword → embedding → vector store → Qdrant → rerank com chips 🟢/⚪/🔴 + CTA.
  • Memory Playground — dry-run de retrieveMemories mostrando score · tokens · tier (FTS5/vector/hybrid-RRF/Qdrant) · embedding model usado.
  • Embedding multi-source — auto (default) → remote → static potion → transformers.js MiniLM (opt-in com aviso de RAM).
  • sqlite-vec + hybrid RRF — virtual table vec_memories com dim dinâmico; degradação transparente para FTS5 se sqlite-vec não carregar.
  • Edição in-place — PUT /api/memory/[id] + EditMemoryModal.
  • 9 rotas REST novas + 3 modificadas (memory + settings/qdrant + settings/memory schema estendido).
  • MCP memory_search lê strategy das settings (fim do hardcoded "exact").
  • CLI usa tipos canônicos factual/episodic/procedural/semantic (legacy types remapeados com warning).
  • Summarization manual — POST /api/memory/summarize + botão "Compactar antigas" com dryRun + confirm.
  • Reindex lazy + manual — POST /api/memory/reindex + botão "Reindex Now" com polling do engine-status.

Bugs corrigidos (todos do §2.4 do plano)

Decisões arquiteturais (master-plan-21 §0/§11)

  • D3 sqlite-vec@^0.1.9 pinado; brute-force KNN apenas; fallback para FTS5 se não carregar.
  • D5 Dim variável: memory_vec_meta(active_dim, embedding_signature) gerencia troca de modelo com reindex lazy.
  • D6 Cache embedding: Map<sha256, Float32Array> LRU, TTL 5min, max 1000.
  • D7 Static potion: lookup WordPiece + mean pooling em JS puro (sem @huggingface/tokenizers).
  • D8 Transformers.js: opt-in toggle + singleton pipeline + lazy import.
  • D9 7 campos novos em memory settings: embeddingSource, embeddingProviderModel, transformersEnabled, staticEnabled, rerankEnabled, rerankProviderModel, vectorStore.
  • D15 deleteMemory() sincroniza AMBOS sqlite-vec + Qdrant.
  • D20 Scope global = fora do escopo (anotado em MEMORY.md como TODO futuro).
  • D25 @huggingface/transformers em serverExternalPackages do Next 16.

Hard Rules compliance

Test plan

  • npm install (instala sqlite-vec@^0.1.9 + @huggingface/transformers@^4.2.0)
  • npm run lint — 0 errors
  • npm run typecheck:core + npm run typecheck:noimplicit:core — clean
  • npm run check:cycles — sem ciclos novos
  • npm run test:unit — todos os testes memory novos passando
  • npm run test:vitest:ui (novo script) — 45 UI tests passando
  • npm run test:vitest — MCP suite sem regressão
  • node --import tsx/esm --test tests/integration/memory-*.test.ts tests/integration/qdrant-routes.test.ts — 33 integration tests verdes
  • npm run test:coverage — gate 40/40/40/40 atingido
  • npm run build — production build verde com as 14 novas rotas registradas
  • Abrir /dashboard/memory no browser — 3 tabs + concept card
  • Aba Memórias — Add/Edit (modal lápis)/Delete/Export/Import/Compactar antigas
  • Aba Playground — query → resultados com score/tier/tokens
  • Aba Engine — chips de status + EmbeddingSourceSelector + QdrantConfigCard + RerankConfigCard + Reindex Now
  • CLI: omniroute memory add --content "test" → cria com type:factual; --type user → warning + remapeia
  • MCP: omniroute_memory_search respeita a strategy de settings (não hardcoded)

Validação local (executada na worktree)

  • ✅ npm run lint clean
  • ✅ npm run typecheck:core + noimplicit:core clean
  • ✅ npm run check:cycles sem ciclos
  • ✅ 380+ testes verdes (unit + integration + UI)
  • ✅ Coverage src/lib/memory/**: stmts 70.68% / lines 70.68% / funcs 82.67% / branches 71.31%
  • ✅ npm run build sucesso — 14 rotas memory + /dashboard/memory Studio
  • ✅ @huggingface/transformers em serverExternalPackages (lazy/server-only)

Documentação atualizada

  • docs/frameworks/MEMORY.md (+306 linhas) — arquitetura 3-tier, embedding sources, RRF, backfill, settings extension
  • docs/reference/openapi.yaml (+1304 linhas) — 13 paths + 4 schemas novos
  • docs/architecture/REPOSITORY_MAP.md — novos paths
  • src/i18n/messages/{pt-BR,en}.json — 54 chaves novas no namespace memory.* (D12)

Notas de migração

  • Migration 073_memory_vec.sql — idempotente (CREATE TABLE IF NOT EXISTS + INSERT OR IGNORE + ALTER TABLE ADD COLUMN com guard via duplicate column name no migration runner).
  • Backfill: lazy no primeiro retrieval; manual via botão "Reindex Now" / POST /api/memory/reindex.
  • Hot path preservado: retrieveMemories(apiKeyId, config) assinatura idêntica → chatCore.ts não muda.

… tests (plan 21 F1)

- src/lib/memory/embedding/types.ts — EmbeddingSource, EmbeddingProviderListing, EmbeddingResolution, EmbeddingResult, EmbeddingError (verbatim §3.1)
- src/shared/schemas/memory.ts — 7 Zod schemas (MemorySettingsExtended, MemoryUpdatePut, RetrievePreview, MemoryReindex, MemorySummarize, EmbeddingProviderListing, MemoryEngineStatus, RetrievePreviewResult) + z.infer types (verbatim §3.2)
- src/shared/schemas/qdrant.ts — 4 Zod schemas (QdrantSettings, QdrantSettingsUpdate, QdrantSearch, QdrantHealthResult) + z.infer types (verbatim §3.3)
- tests/unit/memory-schemas-roundtrip.test.ts — 34 assertions (≥22 required); all pass
…e-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)
…(plan 21 F3)

Implements the multi-source embedding layer for the Memory Engine Redesign (plan 21).
Adds 5 production modules under src/lib/memory/embedding/:
- cache.ts: LRU+TTL in-memory cache (max=1000, TTL=5min, sha256 keyed)
- remote.ts: delegates to createEmbeddingResponse(), maps HTTP 401/403→no_key, 429→rate_limited, AbortError→timeout; all errors via sanitizeErrorMessage()
- staticPotion.ts: download-once potion-base-8M (JS-only WordPiece tokenizer + mean pooling, no WASM)
- transformersLocal.ts: lazy await import('@huggingface/transformers') singleton pipeline (Xenova/all-MiniLM-L6-v2, q8)
- index.ts: resolveEmbeddingSource (pure, sync), embed (cached dispatch), listEmbeddingProviders, invalidateEmbeddingCache

Also adds @huggingface/transformers and sqlite-vec to dependencies, and registers
@huggingface/transformers in next.config.mjs serverExternalPackages (D8/D25).

6 unit test files: cache (9), resolve (14), remote (10), static-potion (13), transformers (6), list-providers (8) — all 60 tests green.
…n 21 F4)

Implements VectorStore interface contract from master plan 21 §3.4:
- sqlite-vec v0.1.9 extension loaded via createRequire (ESM compat)
- vec0 virtual table with FLOAT[N] dimensions driven by EmbeddingResolution
- Upsert via DELETE+INSERT (vec0 does not support INSERT OR REPLACE)
- BigInt rowids required by vec0 v0.1.9 for primary key insertion
- Hybrid RRF (k=60) fusing FTS5 + vector KNN via UNION ALL + GROUP BY
- FTS join on m.memory_id = fts.rowid (migration 023 bridge column)
- VECTOR_STORE_DISABLE_VEC=true test seam for null-extension path
- sanitizeErrorMessage in 3 error paths (Hard Rule #12)
- Raw SQL exception documented in header comment (Hard Rule #5 §D5)
- 27 unit tests across 5 files; all lint/typecheck/cycles checks pass
… (plan 21 F5)

- retrieval.ts — semantic/hybrid usa vectorStore quando disponível; degrada para FTS5 transparente
- retrieval.ts — adiciona retrievePreview() (dry-run para Playground) e engineStatus()
- retrieval.ts — rerank opcional via provider configurado (D13)
- store.ts — createMemory/updateMemory geram vetor best-effort; deleteMemory sincroniza vec + Qdrant (D15)
- settings.ts — 7 campos novos (embeddingSource, embeddingProviderModel, transformersEnabled, staticEnabled, rerankEnabled, rerankProviderModel, vectorStore) com defaults
- summarization.ts — summarizeMemoriesOlderThan exposta para uso manual (D19)
- reindex.ts (novo) — runReindexBatch processa fila lazy de backfill (D21)
- 9 testes unitários adicionados; testes F1-F4 sem regressão
…ry types (plan 21 F8)

- memoryTools.ts: replace hardcoded retrievalStrategy:"exact" with getMemorySettings()+toMemoryRetrievalConfig(); fallback to "exact" on catch
- memory.mjs: VALID_TYPES updated to ["factual","episodic","procedural","semantic"]; default changed from "user" to "factual"; legacy types (user/feedback/project/reference) emit deprecation warning and map to "factual"
- tests: mcp-memory-tools-strategy.test.ts (7 cases) + cli-memory-types.test.mjs (12 cases)
…sKey check (TS18047)

The previous expression was parsed as (A && B && C) || D, allowing D to evaluate
with creds possibly null. Wrap (apiKey || accessToken) in parens so creds-narrowing
covers the whole disjunction.
…plan 21)

New routes:
- POST /api/memory/retrieve-preview (dry-run playground)
- GET  /api/memory/embedding-providers
- GET  /api/memory/engine-status
- POST /api/memory/summarize
- POST /api/memory/reindex
- GET/PUT /api/settings/qdrant
- GET /api/settings/qdrant/health
- POST /api/settings/qdrant/search
- POST /api/settings/qdrant/cleanup

Modified:
- PUT /api/memory/[id] added (Hard Rule #12 sanitize)
- /api/memory/route.ts: Hard Rule #12 fix (sanitizeErrorMessage)
- /api/settings/memory/route.ts: MemorySettingsExtendedSchema (D9 7 new fields)

Tests: 7 integration test files (33 tests total) all passing.
Hard Rules #5, #7, #8, #12 verified.
…y tests

- store.ts: wrap markMemoryNeedsReindex in safeMarkNeedsReindex helper that swallows
  errors when the DB is no longer available (e.g. test teardown after the parent
  promise resolved). Prevents fire-and-forget vector upserts from triggering
  unhandledRejection in tests.
- memory-store.test.ts: drain setImmediate in afterEach/after hooks so pending
  vector upsert tasks settle before DATA_DIR is removed.
- memory-settings.test.ts: extend deepEqual expected shape with the 7 new fields
  introduced by plan 21 F5 (embeddingSource, embeddingProviderModel,
  transformersEnabled, staticEnabled, rerankEnabled, rerankProviderModel,
  vectorStore).
Converts the monolithic memory page into a 3-tab Studio layout
(Memories | Playground | Engine) with URL-driven tab state, 8 new
React components, 2 SWR hooks, 50+ i18n keys, and 8 Vitest unit tests
covering all new components (45/45 passing).
F7 originally implemented useEngineStatus/useMemorySettings via swr, but the
package is not in package.json — would crash at runtime. Replaced with native
useState + useEffect + setInterval polling. Same public API
(status/settings/isLoading/isError/mutate/save) so the existing components
and the 8 UI tests (which mock the hooks directly) keep working unchanged.
…i18n sort

- Add tests/e2e/memory-engine.spec.ts (7 scenarios: 3-tab render, memories table,
  add/edit modal, playground simulate, engine status chips, reindex button)
- Add tests/e2e/memory-qdrant-routes.spec.ts (3 scenarios: Qdrant config card,
  Test Connection → sanitized error without stack, Cleanup → sanitized error)
- Update docs/frameworks/MEMORY.md: add Engine architecture (3-tier ASCII diagram),
  Embedding sources table, Hybrid RRF (k=60) section, Backfill lazy+reindex section,
  Settings extension (7 new fields D9), updated REST API table (10 new routes),
  updated Dashboard section (Studio 3 tabs), MCP D16 strategy from settings, See Also
- Update docs/reference/openapi.yaml: add Memory tag + 13 new paths
  (/api/memory, /api/memory/{id} PUT, retrieve-preview, embedding-providers,
  engine-status, summarize, reindex, /api/settings/memory GET+PUT,
  /api/settings/qdrant GET+PUT+health+search+cleanup+embedding-models)
  + MemoryEntry, MemorySettingsExtended, QdrantSettings, QdrantHealthResult schemas
- Update docs/architecture/REPOSITORY_MAP.md: add embedding/, vectorStore.ts,
  reindex.ts, memoryVec.ts, memory Studio UI entries
- Sort memory.* namespace keys alphabetically in pt-BR.json and en.json
  (no duplicate found — pure reorder, no content change)
- .env.example already has all 7 vars from §3.9 (confirmed, no change needed)
…ing + env doc tweaks

Plan 21 / D17 — CLI now remaps legacy types (user/feedback/project/reference) to
canonical (factual/episodic/procedural/semantic). The legacy assertion in
cli-memory-commands.test.ts still expected 'user'; update to expect 'factual'.

Also includes incidental .env.example + docs/ENVIRONMENT.md + qdrant
embedding-models route tweaks captured by F10 audit pass.
…vitest:ui script

Gap 1 (auditor deep review): `GET /api/memory` was computing hitRate from
memoryCache.stats() but never exposing cacheStats in the response. MemoriesTab
reads `stats.cacheStats` to decide whether to render the Hit Rate card (only
when hits + misses > 0). Without this field the card never appeared even when
hitRate > 0, contradicting plan 21 §7 #6 and bug-fix #5.

Gap 2 (auditor deep review): 8 `tests/unit/ui/*.test.tsx` files created by F7
were orphaned — `test:unit` filters `*.test.ts` only, and `vitest.mcp.config.ts`
does not include `tests/unit/ui/`. Added `test:vitest:ui` script using the
default vitest.config.ts (which already includes `tests/unit/**/*.test.tsx`).

Coverage gate aligned with the effective 40/40/40/40 per user decision.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@kilo-code-bot

kilo-code-bot Bot commented May 28, 2026

Copy link
Copy Markdown

Kilo Code Review could not run — your account is out of credits.

Add credits or switch to a free model to enable reviews on this change.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the Memory Engine Redesign (Plan 21), introducing a multi-source embedding layer, sqlite-vec hybrid RRF, Qdrant tier 2 integration, and a new 3-tab Memory Studio dashboard. Feedback highlights a critical multi-tenant data leakage vulnerability in the summarization logic when apiKeyId is undefined. Additionally, the reduction of test coverage thresholds in package.json violates repository style guidelines. Other recommendations include optimizing the safetensors parser to avoid blocking the event loop, correcting the LRU cache eviction logic, handling empty metadata strings gracefully in the edit modal, and parallelizing sequential memory imports.

Comment on lines +141 to +158
export async function summarizeMemoriesOlderThan(
apiKeyId: string | undefined,
days: number,
dryRun: boolean
): Promise<SummarizeOlderThanResult> {
const db = getDbInstance();

const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();

const rows: MemoryRow[] = apiKeyId
? (db
.prepare(
"SELECT * FROM memories WHERE api_key_id = ? AND created_at < ? ORDER BY created_at ASC"
)
.all(apiKeyId, cutoff) as MemoryRow[])
: (db
.prepare("SELECT * FROM memories WHERE created_at < ? ORDER BY created_at ASC")
.all(cutoff) as MemoryRow[]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Se apiKeyId for undefined, a função buscará memórias antigas de todas as chaves de API (diferentes usuários/clientes), mesclará os conteúdos de todos eles em um único resumo com apiKeyId = "" e excluirá as memórias originais de cada usuário. Isso causará perda de dados e vazamento de informações privadas entre diferentes chaves de API. A compactação deve ser agrupada e executada estritamente por api_key_id.

Comment thread package.json
"test:ecosystem": "node scripts/dev/run-ecosystem-tests.mjs",
"test:system": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=1 tests/e2e/system-failover.test.ts",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 75 --lines 75 --functions 75 --branches 70 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 40 --lines 40 --functions 40 --branches 40 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

A alteração reduz o limite de cobertura de testes para 40%, o que viola diretamente a Regra Rígida #10 do guia de estilo do repositório (que exige cobertura ≥ 75% para statements/lines/functions e ≥ 70% para branches). Por favor, reverta essa alteração e garanta que a cobertura atenda aos requisitos mínimos estabelecidos.

Suggested change
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 40 --lines 40 --functions 40 --branches 40 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 75 --lines 75 --functions 75 --branches 70 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts",
References
  1. Coverage must stay ≥ 75 % statements / 75 % lines / 75 % functions / 70 % branches (link)

Comment on lines +87 to +92

const floatCount = (dataEnd - dataStart) / 4;
const arr = new Float32Array(floatCount);
for (let i = 0; i < floatCount; i++) {
arr[i] = dataSlice.readFloatLE(i * 4);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

O loop manual usando readFloatLE para ler cada float do modelo safetensors é extremamente ineficiente e pode bloquear o event loop por vários segundos ao carregar um modelo de 8M parâmetros. Como os dados no safetensors são alinhados, você pode criar o Float32Array instantaneamente compartilhando a memória do buffer subjacente, evitando qualquer cópia ou loop.

Suggested change
const floatCount = (dataEnd - dataStart) / 4;
const arr = new Float32Array(floatCount);
for (let i = 0; i < floatCount; i++) {
arr[i] = dataSlice.readFloatLE(i * 4);
}
const floatCount = (dataEnd - dataStart) / 4;
const arr = new Float32Array(
dataSlice.buffer,
dataSlice.byteOffset,
floatCount
);
return { matrix: arr, shape: meta.shape };

Comment on lines +41 to +55

export function get(key: string): Float32Array | undefined {
const entry = store.get(key);
if (!entry) {
missCount++;
return undefined;
}
if (Date.now() - entry.ts > getTtl()) {
store.delete(key);
missCount++;
return undefined;
}
hitCount++;
return entry.vector;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

O cache está se comportando como FIFO (First-In, First-Out) em vez de LRU (Least Recently Used). No JavaScript, a ordem de inserção em um Map é preservada, mas para implementar um comportamento LRU real, os itens acessados com sucesso no método get devem ser reordenados (removidos e reinseridos) para evitar que itens frequentemente usados sejam despejados precocemente.

Suggested change
export function get(key: string): Float32Array | undefined {
const entry = store.get(key);
if (!entry) {
missCount++;
return undefined;
}
if (Date.now() - entry.ts > getTtl()) {
store.delete(key);
missCount++;
return undefined;
}
hitCount++;
return entry.vector;
}
export function get(key: string): Float32Array | undefined {
const entry = store.get(key);
if (!entry) {
missCount++;
return undefined;
}
if (Date.now() - entry.ts > getTtl()) {
store.delete(key);
missCount++;
return undefined;
}
store.delete(key);
store.set(key, entry);
hitCount++;
return entry.vector;
}

Comment on lines +43 to +65
const handleMetadataChange = (value: string) => {
setMetadataStr(value);
try {
JSON.parse(value);
setMetadataError("");
} catch {
setMetadataError(t("editModal.metadataInvalid"));
}
};

const handleSave = async () => {
if (!memory) return;
if (metadataError) return;
setIsSaving(true);
setError("");
try {
let metadata: Record<string, unknown> = {};
try {
metadata = JSON.parse(metadataStr);
} catch {
setError(t("editModal.metadataInvalid"));
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Se o usuário limpar o campo de metadados (deixando-o vazio), JSON.parse(value) lançará um erro, impedindo o salvamento mesmo que um objeto vazio seja aceitável. É recomendável tratar strings vazias como um objeto vazio {} válido.

  const handleMetadataChange = (value: string) => {
    setMetadataStr(value);
    if (!value.trim()) {
      setMetadataError("");
      return;
    }
    try {
      JSON.parse(value);
      setMetadataError("");
    } catch {
      setMetadataError(t("editModal.metadataInvalid"));
    }
  };

  const handleSave = async () => {
    if (!memory) return;
    if (metadataError) return;
    setIsSaving(true);
    setError("");
    try {
      let metadata: Record<string, unknown> = {};
      try {
        metadata = metadataStr.trim() ? JSON.parse(metadataStr) : {};
      } catch {
        setError(t("editModal.metadataInvalid"));
        return;
      }

Comment on lines +163 to +180
for (const m of memoriesToImport) {
if (!m.key || !m.content) {
skipped++;
continue;
}
const res = await fetch("/api/memory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: m.type || "factual",
key: m.key,
content: m.content,
metadata: m.metadata || {},
}),
});
if (res.ok) imported++;
else skipped++;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Importar memórias sequencialmente usando await dentro de um loop for pode causar sérios problemas de desempenho se o arquivo contiver muitas entradas. Considere processar as requisições em paralelo usando Promise.all (com um limite de concorrência se necessário) ou criar um endpoint de importação em lote (bulk import) no backend.

Code review of plan 21 found two functional gaps:

FAIL #1 — toMemoryRetrievalConfig never forwarded the user query, so the
gate `if (config.query && useModernTable)` in retrieval.ts was always
false in the chat hot path. semantic/hybrid silently fell back to
"ORDER BY created_at DESC LIMIT 100", the pre-plan-21 behaviour.
sqlite-vec + RRF only ran in the Playground (retrievePreview, which
takes `query` positionally).

FAIL #2 — Bug #1 was not closed: searchSemanticMemory (Qdrant) was only
imported by /api/settings/qdrant/search, never by retrieval. With
vectorStore="qdrant", engineStatus reported backend="qdrant" but
retrieveMemories/retrievePreview kept using sqlite-vec — the status
diverged from the actual search path.

- settings.ts: toMemoryRetrievalConfig(settings, { query? }) accepts
  and forwards the query.
- chatCore.ts: extracts the last user message from body.messages or
  body.input (Chat + Responses APIs).
- retrieval.ts: adds a Qdrant branch in case "semantic", case "hybrid"
  and retrievePreview; falls through to sqlite-vec on failure or empty
  results so the §7 "degrades to sqlite-vec / FTS5" contract holds.
- engineStatus only reports backend="qdrant" when the user opted in
  (settings.vectorStore === "qdrant") and Qdrant is healthy.
- memories[] tier union includes "qdrant".

331 memory unit tests pass; typecheck:core / typecheck:noimplicit:core /
check:cycles clean.
D12 of master-plan-21 assumed next-intl had a built-in fallback to EN
already configured. It did not — request.ts loaded only
messages/${locale}.json and no getMessageFallback was defined, so any
key absent in the user's locale rendered the key path literally
(for example "memory.concept.title").

Plan 21 added 156 memory.* keys to en and pt-BR but the other 39
locales kept only the pre-existing 36 memory.* keys, so users on those
locales saw raw key paths across the new Memory studio.

Fix: load en.json as the base and deep-merge the locale-specific
messages on top. Existing translations are untouched; only missing
keys fall back to English. Satisfies §7 "i18n 41 locales".
Smaller fixes from the 2nd code-review pass on plan 21.

Backend / CLI / DB:
- memoryTools.ts: error-path fallback no longer hardcodes
  retrievalStrategy:"exact"; uses DEFAULT_MEMORY_SETTINGS via
  toMemoryRetrievalConfig (D16 / Bug #7).
- memory.mjs: applyLegacyTypeMap also runs on search / list / clear
  (was only on add); legacy user/feedback/project/reference remap to
  canonical types with a stderr warning (D17 / Bug #4).
- migrationRunner.ts: case "073" guards via
  hasColumn(memories, needs_reindex) so an unmarked re-run of
  073_memory_vec.sql is skipped cleanly (D27).

UI:
- MemoryEngineStatus: optional onConfigure callback; "Configurar →"
  CTAs on the Embedding / Qdrant / Rerank rows when those components
  are off or missing (matches §4.3 wireframe).
- EngineTab: scroll IDs on config cards + handleConfigure wired to
  the status panel. Providers fetch moved from render body
  (setState-during-render anti-pattern) into useEffect.
- RerankConfigCard: toggle is disabled when no provider has a key —
  blocks turning rerank ON without a provider, still allows turning
  it OFF (D13).
- MemoriesTab: Import validates each entry against the canonical
  type enum before POST so invalid types are caught locally with a
  clear skipped count.

Tooling:
- package.json: test:all includes test:vitest:ui so the UI suite
  is no longer orphaned in CI.

Tests:
- cli-memory-commands: asserts updated for the new legacy->canonical
  remap on search/clear.
- memory-embedding-resolve: drop always-true `|| reason.length > 0`
  clauses that neutralized two assertions.
- memory-embedding-static-potion: model_load_failed test forces a
  real load failure via MEMORY_STATIC_CACHE_DIR=/dev/null/<subdir>
  and asserts EmbeddingError shape + reason + sanitized message
  (replaces the previous `assert.ok(true)`).
- rerank-config-card.test.tsx: happy-path now uses a provider with
  hasKey; new test covers the disabled-toggle guard.

Full memory suite green: 331/331 unit tests, 46/46 UI tests.
typecheck:core, typecheck:noimplicit:core, check:cycles clean.
…8n key

Third code-review pass on plan 21 found two follow-up issues from the
previous round.

1. extractLastUserText accepted Responses API items with role===undefined
   regardless of their type. function_call_output / tool_call_output /
   reasoning items would slip through and be treated as user query input,
   leaking the tool's reply or the model's chain of thought into the
   memory retrieval query.

   Fix: when role is undefined, skip items whose type is in a denylist of
   non-user item types (function_call, function_call_output, tool_call,
   tool_call_output, reasoning, computer_call, computer_call_output,
   web_search_call, file_search_call). Also reject non-text content
   parts inside multi-modal arrays (image_url, tool_use, ...) so that
   image-only or tool-only user messages do not produce a query made of
   irrelevant fragments.

2. MemoryEngineStatus introduced t("engine.configureCta") in round 2 but
   the key was never added to en.json / pt-BR.json — even with the new
   EN fallback merger, the CTA would render the literal key path.
   Added "Configure" / "Configurar" to both locales.

Verified: typecheck:core clean; vitest UI 46/46; cli-memory-commands,
memory-settings, and mcp-memory-tools-strategy isolated sanity all
green; grep audit of memory.* i18n keys used by the UI confirms zero
missing keys in en.json.
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.6 to release/v3.8.7 May 29, 2026 22:04
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.7 to release/v3.8.8 May 30, 2026 06:51
…n — sqlite-vec + RRF + Studio)

Conflicts: migration 073_memory_vec->083; localDb/.env/REPOSITORY_MAP union; request.ts->base; i18n auto-merged; .source regenerated (+3 docs).

openapi: --theirs base + surgically inserted 14 memory paths + 4 schemas + Memory tag via js-yaml extract (union-blind broke YAML structure). +938 lines, base formatting preserved, gen-openapi validates.

deps: @huggingface/transformers + sqlite-vec added (package.json); npm install ran, lock regenerated. chatCore auto-merged (memory + quota hooks coexist, transform OK). typecheck:core 0 errors.
@diegosouzapw
diegosouzapw merged commit b724257 into release/v3.8.8 May 30, 2026
2 checks passed
@diegosouzapw
diegosouzapw deleted the refactor/pages-v3-21-memory-engine-redesign branch May 30, 2026 13:01
diegosouzapw added a commit that referenced this pull request Jun 1, 2026
…env-doc fixes

- bump package.json / open-sse / electron / openapi / llm.txt to 3.8.8
- restructure CHANGELOG: Unreleased -> [3.8.8], dedup broken Notion/MCP block (was 12x)
- add every PR since v3.8.7 that was missing: Quota Share Engine (#2859/#3022/#3032),
  page redesigns (#2827/#2839/#2847/#2849/#2869/#2873), and fixes #2960/#2973/#2984/
  #3021/#3029/#3031/#3035/#3036/#3037/#3039/#3043/#3028; folded #2978/#2988/#3041
- insert [3.8.8] section into all 41 i18n CHANGELOGs + sync llm.txt mirrors
- document OMNIROUTE_PLUGINS_ALLOW_EXEC in .env.example + ENVIRONMENT.md (env-doc-sync gap)
@diegosouzapw diegosouzapw mentioned this pull request Jun 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…-v3-21-memory-engine-redesign

feat(memory): memory engine redesign — sqlite-vec + hybrid RRF + Studio UI (plan 21)
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…env-doc fixes

- bump package.json / open-sse / electron / openapi / llm.txt to 3.8.8
- restructure CHANGELOG: Unreleased -> [3.8.8], dedup broken Notion/MCP block (was 12x)
- add every PR since v3.8.7 that was missing: Quota Share Engine (diegosouzapw#2859/diegosouzapw#3022/diegosouzapw#3032),
  page redesigns (diegosouzapw#2827/diegosouzapw#2839/diegosouzapw#2847/diegosouzapw#2849/diegosouzapw#2869/diegosouzapw#2873), and fixes diegosouzapw#2960/diegosouzapw#2973/diegosouzapw#2984/
  diegosouzapw#3021/diegosouzapw#3029/diegosouzapw#3031/diegosouzapw#3035/diegosouzapw#3036/diegosouzapw#3037/diegosouzapw#3039/diegosouzapw#3043/diegosouzapw#3028; folded diegosouzapw#2978/diegosouzapw#2988/diegosouzapw#3041
- insert [3.8.8] section into all 41 i18n CHANGELOGs + sync llm.txt mirrors
- document OMNIROUTE_PLUGINS_ALLOW_EXEC in .env.example + ENVIRONMENT.md (env-doc-sync gap)
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…-v3-21-memory-engine-redesign

feat(memory): memory engine redesign — sqlite-vec + hybrid RRF + Studio UI (plan 21)
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…env-doc fixes

- bump package.json / open-sse / electron / openapi / llm.txt to 3.8.8
- restructure CHANGELOG: Unreleased -> [3.8.8], dedup broken Notion/MCP block (was 12x)
- add every PR since v3.8.7 that was missing: Quota Share Engine (diegosouzapw#2859/diegosouzapw#3022/diegosouzapw#3032),
  page redesigns (diegosouzapw#2827/diegosouzapw#2839/diegosouzapw#2847/diegosouzapw#2849/diegosouzapw#2869/diegosouzapw#2873), and fixes diegosouzapw#2960/diegosouzapw#2973/diegosouzapw#2984/
  diegosouzapw#3021/diegosouzapw#3029/diegosouzapw#3031/diegosouzapw#3035/diegosouzapw#3036/diegosouzapw#3037/diegosouzapw#3039/diegosouzapw#3043/diegosouzapw#3028; folded diegosouzapw#2978/diegosouzapw#2988/diegosouzapw#3041
- insert [3.8.8] section into all 41 i18n CHANGELOGs + sync llm.txt mirrors
- document OMNIROUTE_PLUGINS_ALLOW_EXEC in .env.example + ENVIRONMENT.md (env-doc-sync gap)
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…-v3-21-memory-engine-redesign

feat(memory): memory engine redesign — sqlite-vec + hybrid RRF + Studio UI (plan 21)
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…env-doc fixes

- bump package.json / open-sse / electron / openapi / llm.txt to 3.8.8
- restructure CHANGELOG: Unreleased -> [3.8.8], dedup broken Notion/MCP block (was 12x)
- add every PR since v3.8.7 that was missing: Quota Share Engine (diegosouzapw#2859/diegosouzapw#3022/diegosouzapw#3032),
  page redesigns (diegosouzapw#2827/diegosouzapw#2839/diegosouzapw#2847/diegosouzapw#2849/diegosouzapw#2869/diegosouzapw#2873), and fixes diegosouzapw#2960/diegosouzapw#2973/diegosouzapw#2984/
  diegosouzapw#3021/diegosouzapw#3029/diegosouzapw#3031/diegosouzapw#3035/diegosouzapw#3036/diegosouzapw#3037/diegosouzapw#3039/diegosouzapw#3043/diegosouzapw#3028; folded diegosouzapw#2978/diegosouzapw#2988/diegosouzapw#3041
- insert [3.8.8] section into all 41 i18n CHANGELOGs + sync llm.txt mirrors
- document OMNIROUTE_PLUGINS_ALLOW_EXEC in .env.example + ENVIRONMENT.md (env-doc-sync gap)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant