51 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| System Memory | 3.8.40 | 2026-06-28 |
System Memory
Source of truth:
src/lib/memory/andsrc/app/api/memory/Last updated: 2026-06-28 — v3.8.40 (off-by-default + int8 quantization catch-up)
OmniRoute zapewnia trwałą pamięć konwersacyjną powiązaną z kluczem API (oraz opcjonalnie z identyfikatorem sesji). Wspomnienia są wyodrębniane automatycznie z odpowiedzi LLM poprzez lekkie dopasowanie wzorców regex i wstrzykiwane z powrotem do kolejnych żądań jako wiodąca wiadomość systemowa (albo pierwsza wiadomość użytkownika dla dostawców, którzy odrzucają rolę system).
Memory jest WYŁĄCZONE domyślnie (v3.8.30+).
DEFAULT_MEMORY_SETTINGS.enabledma teraz wartośćfalse(src/lib/memory/settings.ts). Włączenie pamięci wstrzykuje domaxTokens(~2k) pobranego kontekstu do każdego żądania chat, co jest rozliczane — zaskakujący koszt przy nowych instalacjach i dla klientów, które same zarządzają swoim kontekstem. Włącz jawnie w Settings → Memory (MemorySkillsTabpokazuje ostrzeżenie o koszcie tokenów, gdy pamięć jest włączona). Klient może wyłączyć pamięć dla pojedynczego żądania nagłówkiemx-omniroute-no-memory(true/1/yes) — zobacz tabelę nagłówków żądań w API_REFERENCE.md. Żądanie no-memory ustawiamemoryOwnerId = null, co wyłącza zarówno wstrzykiwanie pamięci, jak i skilli dla tego żądania (open-sse/handlers/chatCore/headers.ts::isNoMemoryRequested).
Pamięć jest zakresowana per klucz API, nie per użytkownik — każde żądanie
uwierzytelnione tym samym kluczem API dzieli ten sam pulę pamięci, z opcjonalnym
dalszym zakresowaniem przez sessionId.
Architektura
Client → /v1/chat/completions (apiKeyInfo resolved upstream)
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ resolveMemoryOwnerId(apiKeyInfo) # extracts id
→ getMemorySettings() # cached settings
→ shouldInjectMemory(body, {enabled}) # gate
→ retrieveMemories(apiKeyId, config) # SQL + FTS5 + optional vector
→ injectMemory(body, memories, provider) # system or user message
→ upstream provider call
→ on response: extractFacts(text, apiKeyId, sessionId) # non-blocking
→ setImmediate → createMemory(fact) per match
→ embed(content) + upsertVector(id, vec)
Miejsca wywołań wstrzykiwania i ekstrakcji są podpięte w
open-sse/handlers/chatCore.ts (szukaj retrieveMemories, injectMemory
oraz extractFacts).
Architektura silnika (rozstrzyganie 3-poziomowe)
Memory Engine rozstrzyga ścieżkę retrieval w runtime na podstawie dostępnej infrastruktury i ustawień. Istnieją trzy poziomy, stosowane w kolejności priorytetu:
┌─────────────────────────────────────────────────────────────┐
│ TIER 0 — Keyword (FTS5) │
│ Always available. SQLite FTS5 full-text search over │
│ content + key. Used when strategy = "exact" or as fallback. │
└──────────────────────────────────┬──────────────────────────┘
│ strategy = semantic|hybrid?
▼
┌─────────────────────────────────────────────────────────────┐
│ TIER 1 — Embedded Vector (sqlite-vec) │
│ sqlite-vec v0.1.9 loaded via db.loadExtension(). │
│ KNN brute-force over Float32 vectors. Active when: │
│ • sqlite-vec loadExtension succeeds │
│ • An embedding source is available (remote | static | │
│ transformers) that can produce a Float32Array │
│ • vec_memories table exists (created on first ready()) │
└──────────────────────────────────┬──────────────────────────┘
│ qdrant.enabled?
▼
┌─────────────────────────────────────────────────────────────┐
│ TIER 2 — Qdrant (opt-in external vector database) │
│ When enabled, replaces sqlite-vec for semantic/hybrid. │
│ Requires running Qdrant instance + configured host/port. │
└─────────────────────────────────────────────────────────────┘
Degradacja jest automatyczna i przezroczysta:
- Jeśli sqlite-vec nie załaduje się, tier 1 jest niedostępny → fallback do tier 0.
- Jeśli źródło embeddingów zwróci błąd, tier 1 spada do tier 0.
- Jeśli Qdrant jest niezdrowy, tier 2 spada do tier 1 (lub tier 0, jeśli tier 1 też jest niedostępny).
Źródła embeddingów
Warstwa embeddingów (src/lib/memory/embedding/) rozstrzyga, którego źródła użyć
na podstawie MemorySettingsExtended.embeddingSource:
| Source | Description | Key required | Cold start |
|---|---|---|---|
remote |
Używa API embeddingów skonfigurowanego dostawcy (OpenAI, Cohere itd.) | Yes | None |
static |
Lokalny embedding lookup-table przez potion-base-8M (WordPiece + mean pooling) |
No | ~200ms |
transformers |
Lokalna inferencja ONNX przez @huggingface/transformers v4, all-MiniLM-L6-v2 |
No | ~3s + ~400MB RAM |
auto |
Rozstrzyganie runtime: remote (jeśli jest klucz) → static → transformers → null | Depends | Depends |
Kolejność rozstrzygania dla auto:
- Znajdź pierwszego dostawcę w
listEmbeddingProviders()zhasKey === true→remote. - Jeśli
settings.staticEnabled === true→static. - Jeśli
settings.transformersEnabled === true→transformers. - W przeciwnym razie →
null(degradacja do wyszukiwania słów kluczowych FTS5).
Cache embeddingów (src/lib/memory/embedding/cache.ts) używa mapy LRU w pamięci
kluczowanej przez ${source}:${model}:${dim}:${sha256(text)}, ograniczonej do
MEMORY_EMBEDDING_CACHE_MAX wpisów (domyślnie 1000) z TTL
MEMORY_EMBEDDING_CACHE_TTL_MS (domyślnie 5 min). Współdzielony między wszystkimi
wywołującymi w cyklu życia procesu.
Hybrid RRF (k=60)
Gdy strategy = "hybrid" i magazyn wektorowy jest dostępny, retrieval używa
Reciprocal Rank Fusion do scalenia wyników FTS5 i wektorowych:
RRF(d) = Σ 1 / (k + rank_i(d)) where k = 60 (configurable via MEMORY_RRF_K)
i
Konkretnie:
- Uruchom wyszukiwanie FTS5 → ranking
R_fts(pozycja 1..N). - Uruchom wyszukiwanie wektorowe KNN → ranking
R_vec(pozycja 1..M). - Dla każdego unikalnego
memoryId:rrf_score = 1/(60 + fts_rank)+1/(60 + vec_rank)(0 jeśli nie ma na liście). - Sortuj po
rrf_scoreDESC, zastosuj przejście budżetu tokenów.
RRF jest znany z skuteczności bez normalizacji score'ów między
heterogenicznymi systemami retrieval. Domyślne k=60 pochodzi z oryginalnego
artykułu Cormack et al. i dobrze działa dla małych korpusów (<10k memories).
Backfill (leniwy + reindex)
Gdy model embeddingów się zmienia (wykrywane przez embedding_signature),
magazyn wektorowy jest przebudowywany, a wszystkie istniejące memories dostają
needs_reindex = 1 w tabeli memories.
Lazy backfill: Przy następnym retrieval każde memory bez wpisu wektorowego jest
embedowane i wstawiane do vec_memories przed uruchomieniem wyszukiwania. To
amortyzuje koszt backfillu na realnych żądaniach bez blokowania startu.
Explicit reindex: Zakładka Engine w /dashboard/memory udostępnia przycisk
"Reindex Now", który wywołuje POST /api/memory/reindex. Handler woła
runReindexBatch() z src/lib/memory/reindex.ts, które przetwarza do
limit oczekujących wpisów na żądanie. Postęp można odpytywać przez
GET /api/memory/engine-status (vectorStore.needsReindex).
Tabela memory_vec_meta (migracja 073_memory_vec.sql) przechowuje:
active_dim— bieżący wymiar wektora (null = jeszcze nie skalibrowany).embedding_signature—${source}:${model}:${dim}używany do wykrywania zmian.last_reset_at— znacznik czasu ostatniego pełnego resetu.vec_loaded— flaga 0/1, czy sqlite-vec załadował się pomyślnie.
Rozszerzenie ustawień
Do MemorySettingsExtended (plan 21, D9) w
src/shared/schemas/memory.ts dodano siedem nowych pól, utrwalanych przez
src/lib/db/settings.ts:
| Field | Type | Default | Description |
|---|---|---|---|
embeddingSource |
"remote" | "static" | "transformers" | "auto" |
"auto" |
Którego źródła embeddingów użyć |
embeddingProviderModel |
string | null |
null |
Provider/model w formacie provider/model |
transformersEnabled |
boolean |
false |
Opt-in dla Transformers.js (MiniLM, ~400MB) |
staticEnabled |
boolean |
false |
Opt-in dla lokalnego modelu static potion-base-8M |
rerankEnabled |
boolean |
false |
Włącz krok rerankingu (+200–500 ms/req) |
rerankProviderModel |
string | null |
null |
Provider/model rerank w formacie provider/model |
vectorStore |
"sqlite-vec" | "qdrant" | "auto" |
"auto" |
Którego backendu wektorowego użyć |
Są one wystawione przez GET /PUT /api/settings/memory (schemat MemorySettingsExtendedSchema).
TODO (D20): Scope
global(współdzielenie memories między wszystkimi kluczami API) nie jest zaimplementowany w tym wydaniu. Wymaga zmian schematu i globalnej ścieżki retrieval. Śledź osobno.
Warstwy przechowywania
Primary: SQLite (tabela memories)
Utworzona przez migrację 015_create_memories.sql:
| Column | Type | Notes |
|---|---|---|
id |
TEXT PRIMARY KEY |
UUID generowany przez crypto.randomUUID() |
api_key_id |
TEXT NOT NULL |
Właścicielski klucz API |
session_id |
TEXT |
Opcjonalny zakres per-rozmowa |
type |
TEXT NOT NULL |
Jedno z: factual, episodic, procedural, semantic |
key |
TEXT |
Stabilny klucz upsert, np. preference:i_prefer_python |
content |
TEXT NOT NULL |
Właściwy tekst faktu |
metadata |
TEXT |
Blob JSON (category, extractedAt, source, ...) |
created_at / updated_at |
TEXT |
Ciągi ISO 8601 |
expires_at |
TEXT |
Opcjonalne wygaśnięcie; NULL oznacza trwałe |
memory_id |
INTEGER UNIQUE |
Dodane przez 023_fix_memory_fts_uuid.sql do mostkowania UUID ↔ FTS5 rowids |
Indeksy: api_key_id, session_id, type, expires_at, plus unikalny
indeks memory_id.
Semantyka upsert: createMemory() szuka istniejącego wiersza z tym samym
(api_key_id, key) i aktualizuje go w miejscu, gdy znajdzie (scalając metadata
przez płytki spread). Dzięki temu tabela nie rośnie bez ograniczeń przy
powtarzanych stwierdzeniach preferencji.
Full-text Search (wirtualna tabela memory_fts)
022_add_memory_fts5.sql tworzy wirtualną tabelę FTS5 nad content i
key. 023_fix_memory_fts_uuid.sql naprawia rzeczywisty bug, w którym UUID
klucza głównego nie łączył się z integer rowid FTS5 — migracja dodaje kolumnę
memory_id, odtwarza tabelę FTS i podpina triggery
(memory_fts_ai, memory_fts_ad, memory_fts_au), które utrzymują FTS w synchronizacji przy
INSERT, DELETE i UPDATE.
Używane przez retrieval.ts dla strategii semantic i hybrid (patrz poniżej).
Kod retrieval pilnuje hasTable("memory_fts") i spada do
kolejności chronologicznej, jeśli tabela FTS nie istnieje lub zapytanie FTS rzuci wyjątek.
Opcjonalnie: Qdrant (vector store tier 2)
src/lib/memory/qdrant.ts implementuje opcjonalną integrację Qdrant jako tier 2
magazynu wektorowego. Retrieval kieruje do Qdrant tylko gdy selektor silnika
memoryVectorStore === "qdrant" — domyślne "auto" (oraz "sqlite-vec")
nigdy nie wybierają Qdrant. Przełącznik w zakładce Engine ustawia oba
qdrantEnabled i memoryVectorStore razem: włączenie czyni Qdrant magazynem
głównym, wyłączenie resetuje do "auto" (#5597 — przed tą poprawką włączenie
było martwe, bo nic nie zapisywało selektora silnika). Jeśli Qdrant jest
nieosiągalny lub nic nie zwróci, retrieval spada do sqlite-vec → FTS5.
upsertSemanticMemoryPoint()— embedujekey + contentskonfigurowanym modelem embeddingów, zapewnia istnienie kolekcji (tworzy wektory cosine-distance przy pierwszym użyciu) i upsertuje punkt z payloadem{memoryId, apiKeyId, sessionId, key, content, metadata, createdAtUnix, expiresAtUnix}.searchSemanticMemory(query, topK, scope)— embeduje zapytanie, przeszukuje kolekcję filtrowaną pokind = "omniroute_memory"oraz opcjonalnie poapiKeyId/sessionId. OgraniczatopKdo[1, 20].deleteSemanticMemoryPoint(id)— usunięcie pojedynczego punktu. Wołane przezdeleteMemory()po usunięciu wiersza SQLite (D15).cleanupSemanticMemoryPoints({retentionDays})— masowe usuwanie punktów, którychexpiresAtUnixjest w przeszłości lubcreatedAtUnixjest starszy niż próg retencji. Najpierw liczy, żeby dashboard mógł pokazać rzeczywiste liczby.checkQdrantHealth()— sonda zdrowiaGET /readyzz latencją.
UI ustawień udostępnia konfigurację Qdrant, health check, test wyszukiwania
semantycznego i cleanup w zakładce Engine /dashboard/memory. Odpowiadające
trasy pod src/app/api/settings/qdrant/ są w pełni podpięte od v3.8.6:
| Route | Method | Description |
|---|---|---|
/api/settings/qdrant |
GET / PUT |
Odczyt / aktualizacja ustawień Qdrant |
/api/settings/qdrant/health |
GET |
Sonda liveness + latencja |
/api/settings/qdrant/search |
POST |
Test wyszukiwania semantycznego |
/api/settings/qdrant/cleanup |
POST |
Usuń wygasłe / stare punkty |
/api/settings/qdrant/embedding-models |
GET |
Lista dostępnych modeli embeddingów |
Uwagi behawioralne (czego się spodziewać):
- Wybór silnika — włączenie Qdrant w zakładce Engine czyni go magazynem głównym
(ustawia
memoryVectorStore="qdrant"); wyłączenie resetuje do"auto"(#5597). - Brak back-fill — do Qdrant trafiają tylko memories utworzone/zaktualizowane po włączeniu (dual-write fire-and-forget). Istniejące memories SQLite nie są migrowane; "Reindex Now" przebudowuje tylko indeks sqlite-vec, nie Qdrant.
- Wymiar wektora jest wykrywany automatycznie z rzeczywistego embeddinga przy pierwszym użyciu — nie ma pola wymiaru do wypełnienia. Zmiana modelu embeddingów po utworzeniu kolekcji nie jest obsługiwana automatycznie: istniejąca kolekcja zostaje nietknięta, zapisy/wyszukiwania z niezgodnym wymiarem kończą się błędem i spadają do sqlite-vec. Odtwórz kolekcję (nowa nazwa albo usuń w Qdrant), aby zmienić embedder.
- Metryka odległości — zawsze Cosine (zahardkodowana przy tworzeniu kolekcji; niekonfigurowalna).
- Auth — tylko klucz API (wysyłany jako nagłówek
api-key; opcjonalny dla lokalnego Dockera bez auth). JWT/RBAC nie są używane. - Pola konfiguracji — UI udostępnia
host,port,collection,embeddingModel,apiKey.vectorSize/hnswEfConstructsą tylko env/DB, avectorSizenie jest używane przy tworzeniu kolekcji (wymiar pochodzi z embeddinga).
Kwantyzacja wektorów (int8 — opt-in, oba backendy)
Oba backendy wektorowe wspierają opcjonalną kwantyzację int8, aby zmniejszyć zużycie pamięci przechowywanych wektorów (~4× mniejsze niż Float32) przy niewielkim koszcie recall. Domyślnie wyłączone na obu — wektory pozostają pełnej precyzji, dopóki nie włączysz jawnie.
| Backend | Setting | Type | Default | Where read |
|---|---|---|---|---|
| Qdrant | qdrantQuantization (DB key) |
"none" | "int8" | "binary" |
"none" |
src/lib/memory/qdrant.ts::normalizeQdrantConfig() |
| sqlite-vec | MEMORY_VEC_QUANTIZATION (env) |
"none" | "int8" |
"none" |
src/lib/memory/vectorStore.ts::requestedVecQuantization() |
- Qdrant konfiguruje się per-instancja przez klucz ustawień
qdrantQuantization(wystawiony jako polequantizationnaPUT /api/settings/qdrant). Przy"int8"buildQuantizationConfig()żąda scalar quantization (always_ram, quantile0.99), a wyszukiwania włączająrescore: true, żeby wektory pełnej precyzji doprecyzowały zbiór kandydatów int8. - sqlite-vec kwantyzacja jest tylko środowiskowa (nie ustawienie DB): ustaw
MEMORY_VEC_QUANTIZATION=int8, aby przechowywać lokalne wektory jako kolumnęint8[dim]przezvec_quantize_int8(?, 'unit'). Wybrany tryb jest włączany doembedding_signature(sufiks:int8), więc zmiana trybu wymusza pełny reindex tabelivec_memories— ta sama ścieżka lazy-backfill co przy zmianie modelu embeddingów.
Typy Memory
MemoryType (src/lib/memory/types.ts):
| Type | Used for |
|---|---|
factual |
Preferencje, stabilne fakty użytkownika, wzorce behawioralne |
episodic |
Decyzje powiązane z konkretnym momentem („I chose Postgres”) |
procedural |
Pamięć workflow / how-to (zarezerwowane; brak auto-extractora) |
semantic |
Zarezerwowane dla wpisów magazynu wektorowego |
Strategia retrieval w MemoryConfig to jedno z: exact, semantic lub hybrid,
a scope to jedno z: session, apiKey lub global. Domyślny scope z
getMemorySettings() to apiKey.
Ekstrakcja faktów (extraction.ts)
Ekstrakcja jest oparta na regex, nie na LLM — działa in-process z
setImmediate(), więc nigdy nie blokuje strumienia odpowiedzi:
- Wzorce preferencji →
MemoryType.FACTUAL(np.I prefer …,I really like …,my favorite is …,I hate …) - Wzorce decyzji →
MemoryType.EPISODIC(np.I'll use …,I chose …,I went with …,I'm going to adopt …) - Wzorce wzorców →
MemoryType.FACTUAL(np.I usually …,I always …,I tend to …)
Każde dopasowanie jest sanityzowane (trim, zwinięcie białych znaków, limit 500 znaków),
deduplikowane w batchu przez stabilny factKey(category, content) i
przechowywane przez createMemory() z metadanymi
{category, extractedAt, source: "llm_response"}. Tekst wejściowy jest ograniczony do
64 KiB (MAX_EXTRACTION_TEXT_LENGTH) — gdy dłuższy, używany jest ogon tekstu,
żeby najnowsza treść asystenta zawsze brała udział.
extractFactsFromText(text) jest eksportowane do testów i zwraca ustrukturyzowane
fakty bez ich zapisywania.
Retrieval (retrieval.ts)
retrieveMemories(apiKeyId, config) to główny punkt wejścia. Robi:
- Normalizuje i waliduje config przez
MemoryConfigSchema. - Zwraca
[]natychmiast, gdyenabledjest false lubmaxTokens <= 0. - Ogranicza
maxTokensdo[1, 8000]. - Wykrywa, czy istnieje nowoczesna tabela
memories(vs legacymemory), żeby starsze bazy nadal działały. - Buduje bazowe zapytanie z ochroną wygaśnięcia
(
expires_at IS NULL OR datetime(expires_at) > datetime('now')), opcjonalnym zakresem sesji i opcjonalnym progiemretentionDays. - Rozgałęzia się po strategii:
exact(domyślna): chronologiczneORDER BY created_at DESC LIMIT 100.semantic: jeśli jestconfig.queryi istniejememory_fts, JOINmemory_fts MATCH ?i sortowanie po rankingu FTS; fallback do chronologicznego, gdy FTS zwróci 0 wierszy.hybrid: unia wyników FTS (wyższa relevantność) i zbioru chronologicznego, deduplikowana po id.
- Liczy keyword relevance score (
getRelevanceScore) pocontent,keyi JSONmetadata, gdy podano query. Wiersze z zerowym score są odfiltrowywane. - Sortuje po score desc, potem
createdAtdesc. - Przechodzi ranking i akceptuje wpisy, dopóki bieżący
estimateTokens(content)(≈length / 4) mieści się w budżecie. Zawsze zwraca co najmniej jeden wpis, gdy cokolwiek pasowało.
estimateTokens jest eksportowane i używane przez retrieval, summarisation oraz narzędzie MCP
omniroute_memory_search.
Injection (injection.ts)
injectMemory(request, memories, provider):
- Łączy wszystkie treści memories w jeden ciąg
Memory context: …. - Wybiera strategię według nazwy providera:
- Wiadomość system (domyślnie dla OpenAI, Anthropic, Gemini, …) — wstawia
{role: "system", content: memoryText}przed istniejącymi wiadomościami system, żeby system prompy użytkownika nadal miały pierwszeństwo. - Wiadomość user (fallback) — dla providerów w
PROVIDERS_WITHOUT_SYSTEM_MESSAGE:o1,o1-mini,o1-preview,glm,glmt,glm-cn,zai,qianfan. Te odrzucają rolę system i w przeciwnym razie zwróciłyby 400 (por. issue #1701 dla GLM/Zhipu).
- Wiadomość system (domyślnie dla OpenAI, Anthropic, Gemini, …) — wstawia
- Loguje count, strategię i model pod
memory.injection.injected.
providerSupportsSystemMessage(provider) jest eksportowane dla wywołujących, którzy
chcą sami podejmować decyzje routingu. Nieznani providerzy domyślnie dostają true
(rola system dozwolona) dla bezpieczeństwa.
Ustawienia (settings.ts)
Konfiguracja Memory jest przechowywana w tabeli settings DB, nie w zmiennych env.
getMemorySettings() czyta z getSettings() i cache'uje wynik
in-process; invalidateMemorySettingsCache() jest wołane przez trasę settings PUT
po zapisach.
Pola legacy (wszystkie wersje)
| DB key | Type | Default | UI control |
|---|---|---|---|
memoryEnabled |
boolean | false (wyłączone domyślnie od v3.8.30) |
Memory on/off |
memoryMaxTokens |
integer | 2000 (zakres 0–16000) |
Budżet tokenów na injection |
memoryRetentionDays |
integer | 30 (zakres 1–365) |
Okno retencji |
memoryStrategy |
enum | "hybrid" (jedno z recent, semantic, hybrid) |
Strategia retrieval |
skillsEnabled |
boolean | false |
Przełącza injection skilli per-key (zob. SKILLS.md) |
Uwaga: strategia UI "recent" mapuje się na wewnętrzną strategię retrieval "exact"
przez toMemoryRetrievalConfig() (kolejność chronologiczna).
Nowe pola (v3.8.6, plan 21 D9)
Zobacz też sekcję „Rozszerzenie ustawień” powyżej dla opisów pól.
| DB key | API field | Default |
|---|---|---|
memoryEmbeddingSource |
embeddingSource |
"auto" |
memoryEmbeddingModel |
embeddingProviderModel |
null |
memoryTransformersEnabled |
transformersEnabled |
false |
memoryStaticEnabled |
staticEnabled |
false |
memoryRerankEnabled |
rerankEnabled |
false |
memoryRerankModel |
rerankProviderModel |
null |
memoryVectorStore |
vectorStore |
"auto" |
Klucze DB związane z Qdrant (qdrantEnabled, qdrantHost, qdrantPort,
qdrantApiKey, qdrantCollection domyślnie "omniroute_memory",
qdrantEmbeddingModel domyślnie "openai/text-embedding-3-small") czyta
normalizeQdrantConfig() w qdrant.ts.
Zmienne środowiskowe (v3.8.6)
Sześć opcjonalnych zmiennych env dostraja zachowanie runtime silnika (udokumentowane w .env.example):
| Variable | Default | Description |
|---|---|---|
MEMORY_EMBEDDING_CACHE_TTL_MS |
300000 |
TTL cache embeddingów (5 min) |
MEMORY_EMBEDDING_CACHE_MAX |
1000 |
Maks. wpisów w LRU cache embeddingów |
MEMORY_TRANSFORMERS_MODEL |
Xenova/all-MiniLM-L6-v2 |
Repo HF dla modelu Transformers.js |
MEMORY_STATIC_MODEL |
minishlab/potion-base-8M |
Repo HF dla modelu static potion |
MEMORY_STATIC_CACHE_DIR |
<DATA_DIR>/embeddings |
Gdzie przechowywać pobrane modele |
MEMORY_VEC_TOP_K |
20 |
Domyślne top-K dla wyszukiwania wektorowego |
MEMORY_RRF_K |
60 |
Stała k RRF dla hybrid search |
MEMORY_VEC_QUANTIZATION |
none |
Ustaw int8, aby przechowywać lokalne wektory sqlite-vec skwantyzowane (~4× mniejsze; opt-in). Zmiana trybu wymusza reindex. |
Summarisation (summarization.ts)
summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000) kompaktuje starszą
treść, gdy bieżąca suma tokenów nad memories klucza przekracza
budżet. Iteruje wiersze DESC po created_at, zachowuje te, które mieszczą się,
a dla reszty zastępuje content w miejscu pierwszymi trzema zdaniami
oryginału. tokensSaved to różnica estimateTokens między starą a
nową treścią.
Ta procedura jest dostępna, ale nie wywoływana automatycznie w bieżącym
pipeline chatu — wołaj ją z crona, akcji admina albo
kleju MemoryConfig.autoSummarize, jeśli potrzebujesz ciągłej kompakcji. Utrata
danych jest jednokierunkowa: oryginalny tekst jest nadpisywany.
REST API
Wszystkie endpointy wymagają management auth (requireManagementAuth).
Główne endpointy memory (istniejące + zaktualizowane)
| Method | Path | Description |
|---|---|---|
GET |
/api/memory |
Lista stronicowana z filtrami: apiKeyId, type, sessionId, q, limit, page, offset. Odpowiedź zawiera stats.total, stats.tokensUsed, stats.hitRate, cacheStats |
POST |
/api/memory |
Utwórz wpis (walidacja Zod: content, key, opcjonalnie type, sessionId, apiKeyId, metadata, expiresAt). Woła createMemory(), które upsertuje po (apiKeyId, key) |
GET |
/api/memory/[id] |
Pobierz pojedynczy wpis po UUID |
PUT |
/api/memory/[id] |
Aktualizuj pola wpisu (type, key, content, metadata). Body: MemoryUpdatePutSchema. Synchronizuje też wektor, jeśli źródło embeddingów jest dostępne. |
DELETE |
/api/memory/[id] |
Usuń wpis; usuwa też z vec_memories (D15) i Qdrant best-effort. Zwraca 404, gdy brak. |
GET |
/api/memory/health |
Uruchamia verifyExtractionPipeline("health-check") — round-trip create→list→delete. Zwraca {working, latencyMs, error?} |
Nowe endpointy memory engine (plan 21)
| Method | Path | Description |
|---|---|---|
POST |
/api/memory/retrieve-preview |
Dry-run retrieveMemories — zwraca ranking z score, tier, tokens. Body: RetrievePreviewSchema. NIE wstrzykuje ani nie modyfikuje memories. |
GET |
/api/memory/embedding-providers |
Lista providerów z modelami embeddingów, ze wskazaniem które mają skonfigurowany klucz API. |
GET |
/api/memory/engine-status |
Pełny status silnika: keyword tier, rozstrzygnięcie embeddingów, stats magazynu wektorowego, health Qdrant, config rerank. Shape: MemoryEngineStatusSchema. |
POST |
/api/memory/summarize |
Ręczne uruchomienie kompakcji pamięci. Body: MemorySummarizeSchema (olderThanDays, apiKeyId?, dryRun). Zwraca {candidates, tokensSaved}. |
POST |
/api/memory/reindex |
Wyzwól reindex wektorowy dla memories z needs_reindex=1. Body: MemoryReindexSchema (force). Zwraca {started, pending}. |
Endpointy ustawień
| Method | Path | Description |
|---|---|---|
GET |
/api/settings/memory |
Bieżące znormalizowane MemorySettingsExtended (7 nowych pól + legacy) |
PUT |
/api/settings/memory |
Aktualizuj dowolne pole z MemorySettingsExtendedSchema (12 pól łącznie) |
GET |
/api/settings/qdrant |
Bieżące ustawienia Qdrant (QdrantSettingsSchema) |
PUT |
/api/settings/qdrant |
Aktualizuj ustawienia Qdrant. Body: QdrantSettingsUpdateSchema. apiKey = pusty string usuwa klucz. |
GET |
/api/settings/qdrant/health |
Sonda liveness wobec skonfigurowanej instancji Qdrant. Zwraca QdrantHealthResultSchema. |
POST |
/api/settings/qdrant/search |
Test wyszukiwania semantycznego wobec Qdrant. Body: QdrantSearchSchema (query, topK). |
POST |
/api/settings/qdrant/cleanup |
Usuń punkty Qdrant dla wygasłych / starych memories. |
GET |
/api/settings/qdrant/embedding-models |
Lista modeli embeddingów dostępnych dla Qdrant. |
Query listy /api/memory obsługuje albo paginację opartą o page
(parsePaginationParams) albo surowy offset — gdy offset jest obecny,
ma pierwszeństwo, a wyprowadzone page jest liczone pod kształt odpowiedzi.
Narzędzia MCP (open-sse/mcp-server/tools/memoryTools.ts)
Gdy serwer MCP jest włączony, rejestrowane są trzy narzędzia memory:
omniroute_memory_search—{apiKeyId, query?, type?, maxTokens?, limit?}→ opakowujeretrieveMemories(). Od v3.8.6 (D16)strategyjest czytane zgetMemorySettings()zamiast być zahardkodowane na"exact". Jeśli podanoqueryistrategytosemanticlubhybrid, używany jest magazyn wektorowy, gdy dostępny.omniroute_memory_add—{apiKeyId, sessionId?, type, key, content, metadata?}→ opakowujecreateMemory(). Przyjmuje tylko 4 kanoniczne typy:factual,episodic,procedural,semantic(D17).omniroute_memory_clear—{apiKeyId, type?, olderThan?}→ listuje pasujące wpisy, opcjonalnie filtruje po znaczniku created-before, potem usuwa każdy przezdeleteMemory()(co usuwa też wektory z sqlite-vec + Qdrant).
Zobacz MCP-SERVER.md po szczegóły transportu i scope'ów.
Dashboard (Memory Studio)
src/app/(dashboard)/dashboard/memory/page.tsx to teraz Studio 3-zakładkowe:
Zakładka: Memories
- Karta koncepcji (zwijany explainer „How it works”).
- Lista w czasie rzeczywistym, wyszukiwanie i paginacja (debounce 300 ms).
- Filtr typu (
factual/episodic/procedural/semantic/ all). - Modal dodawania memory (key, content, type).
- Inline edit (przycisk ołówka →
PUT /api/memory/[id]). - Usuwanie per wiersz (z dialogiem potwierdzenia).
- Eksport JSON bieżącej strony; import JSON przez file picker.
- Karty statystyk:
totalEntries,tokensUsed,hitRate. - Przycisk „Compact old” →
POST /api/memory/summarize(najpierw dry-run pokazuje liczbę kandydatów, potem potwierdzenie). - Zielona/czerwona kropka health napędzana przez
GET /api/memory/health.
Zakładka: Playground
- Input zapytania + selektor strategii (Exact / Semantic / Hybrid) + budżet tokenów.
- „Simulate” →
POST /api/memory/retrieve-preview— pokazuje ranking zscore,tier,tokens,vecScore,ftsScore. - Panel rozstrzygnięcia pokazujący, które źródło embeddingów / magazyn wektorowy zostało użyte i czy nastąpił fallback.
Zakładka: Engine
- Panel statusu silnika (chip keyword FTS5, chip embedding, chip vector store, chip health Qdrant, chip rerank).
- Przycisk „Reindex Now” →
POST /api/memory/reindex. - Selektor źródła embeddingów (auto / remote / static / transformers + przełączniki).
- Karta config Qdrant (toggle enable, host/port/collection/key, test połączenia, test wyszukiwania semantycznego, cleanup).
- Karta config rerank (toggle enable, selektor provider/model).
Ustawienia Memory i Qdrant żyją też pod
/dashboard/settings → Memory & Skills (MemorySkillsTab.tsx) jako
legacy/globalna powierzchnia ustawień.
Caching
src/lib/memory/store.ts trzyma in-process cache w stylu LRU
(MEMORY_CACHE_TTL = 5 min, MEMORY_MAX_CACHE_SIZE = 10 000, z 20%
ewikcją najstarszych) dla odczytów getMemory(id), plus generyczną warstwę
key/value memoryCache (src/lib/memory/cache.ts) z metodami get/set/invalidate
używanymi przez wywołujących, którzy chcą własny scoped cache (LRU 1000 wpisów,
domyślny TTL 5 min).
Prywatność i cykl życia
- Własność memory to id klucza API (
resolveMemoryOwnerIdwchatCore.ts). BezapiKeyInfo.idnie działa ani retrieval, ani injection, ani extraction. - Wpisy z przyszłym
expires_atsą odfiltrowywane z retrieval; stare wpisy pozaretentionDayssą wykluczane przez klauzulęcreated_at >= cutoffwretrieveMemories. - Do twardego usunięcia użyj
DELETE /api/memory/[id]lubomniroute_memory_clear. - Extraction jest fire-and-forget przez
setImmediate; błędy logowane są podmemory.extraction.background.failedi nigdy nie wychodzą do wywołującego. - Round-tripy weryfikacji (
verifyExtractionPipeline) sprzątają własne wpisy testowe w blokufinally.
Zobacz też
- SKILLS.md — ustawienie
skillsEnabledwstrzykuje definicje narzędzi obok memory. - MCP-SERVER.md — transport / scope'y MCP.
- API_REFERENCE.md — szersza powierzchnia API.
- Moduły źródłowe:
src/lib/memory/types.ts,schemas.tssrc/lib/memory/store.ts,retrieval.ts,injection.ts,reindex.tssrc/lib/memory/extraction.ts,summarization.ts,verify.tssrc/lib/memory/settings.ts,qdrant.ts,cache.tssrc/lib/memory/vectorStore.ts— sqlite-vec + hybrid RRFsrc/lib/memory/embedding/index.ts— wieloźródłowa warstwa embeddingówsrc/lib/memory/embedding/types.ts,remote.ts,staticPotion.ts,transformersLocal.ts,cache.tssrc/shared/schemas/memory.ts— schematy Zod dla wszystkich body API memorysrc/shared/schemas/qdrant.ts— schematy Zod dla ustawień/ops Qdrantsrc/lib/db/memoryVec.ts— CRUD dlamemory_vec_metasrc/lib/db/migrations/015_create_memories.sql,022_add_memory_fts5.sql,023_fix_memory_fts_uuid.sql,073_memory_vec.sqlsrc/app/api/memory/route.ts,[id]/route.ts,health/route.tssrc/app/api/memory/retrieve-preview/route.tssrc/app/api/memory/engine-status/route.tssrc/app/api/memory/embedding-providers/route.tssrc/app/api/memory/summarize/route.tssrc/app/api/memory/reindex/route.tssrc/app/api/settings/memory/route.tssrc/app/api/settings/qdrant/route.ts+ sub-routessrc/app/(dashboard)/dashboard/memory/— UI Studio (page + components + tabs + hooks)open-sse/handlers/chatCore.ts(wiring injection / extraction)open-sse/mcp-server/tools/memoryTools.ts
Wybór providera embeddingów (v3.8.16+)
Silnik memory OmniRoute wspiera cztery źródła embeddingów (src/lib/memory/embedding/). Każde ma inne kompromisy w latencji, koszcie, jakości modelu i złożoności setupu.
Cztery providery
| Provider | Source | Latency | Cost | Quality | Setup |
|---|---|---|---|---|---|
transformers |
Lokalny model ONNX (Xenova/all-MiniLM-L6-v2) | ~50-150ms (CPU) | Free | Good | tylko npm install |
static |
Wstępnie wyliczone wektory (cache) | <1ms | Free | N/A (zależy od cache hit) | None |
remote |
API OpenAI / Cohere / Voyage | ~100-300ms | $0.02-0.10/1M tokens | Excellent | API key |
cache |
Warstwa LRU in-memory nad dowolnym źródłem | <1ms (hit), pełna latencja (miss) | Free | Taka jak underlying | None |
Drzewo decyzyjne
What's your deployment context?
│
┌───────────┼───────────┬──────────────┐
│ │ │ │
DEV/TEST SMALL PROD LARGE PROD EDGE / OFFLINE
│ │ │ │
▼ ▼ ▼ ▼
transformers transformers remote (Qdrant) transformers
(free, no API) (best quality) (no internet)
│ │ │ │
└────────┬──┴───────────┴──────────────┘
│
▼
ALWAYS add `cache` layer on top
(LruCache wraps any provider)
Konfiguracja Database & API
Opcje embeddingów memory konfiguruje się przez Settings API/UI, nie zmienne środowiskowe. Istotne klucze ustawień w DB pod Settings (normalizeMemorySettings w src/lib/memory/settings.ts) to:
memoryEmbeddingSource:"transformers"(lokalne),"remote"(API, np. OpenAI),"static"(zewnętrzny store) lub"auto"memoryEmbeddingProviderModel: Identyfikator modelu dla źródeł remote/static (np."text-embedding-3-small")memoryTransformersEnabled:true|falsememoryStaticEnabled:true|falsememoryVectorStore:"sqlite-vec","qdrant"lub"auto"
Model lokalny (transformers)
Używa transformers.js wewnętrznie do uruchamiania lokalnych modeli:
# Env vars read in code (src/lib/memory/embedding/index.ts):
MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # HF model repo
MEMORY_STATIC_MODEL=minishlab/potion-base-8M # HF static potion model
MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # Cache directory
LRU Embedding Cache
Cache jest domyślnie zawsze włączony i konfigurowany przez zmienne env:
MEMORY_EMBEDDING_CACHE_MAX=1000 # Max cached items
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL (5 min)
Liczby wydajnościowe
Benchmark na typowym serwerze 4-core x86 (teksty ~100 tokenów każdy):
| Provider | p50 | p95 | p99 | Cost / 1M embeddings |
|---|---|---|---|---|
transformers (CPU) |
80ms | 180ms | 350ms | Free |
remote (OpenAI) |
120ms | 220ms | 400ms | ~$0.02 (ada-002) / $0.13 (3-large) |
static (Qdrant) |
15ms | 30ms | 60ms | Depends on Qdrant hosting |
cache (hit) |
<1ms | <1ms | 2ms | Free |
Wzorce ekstrakcji faktów (v3.8.16+)
Moduł extraction.ts (src/lib/memory/extraction.ts) używa dopasowania wzorców regex do wyodrębniania ustrukturyzowanych faktów z wiadomości rozmowy. Zrozumienie tych wzorców pomaga dostroić jakość ekstrakcji do Twojego przypadku użycia.
Domyślne kategorie wzorców
| Category | Example pattern | Captures |
|---|---|---|
| PREFERENCE_PATTERNS | "I prefer <X>", "I like <X>", "I hate <X>" |
Preferencje użytkownika |
| DECISION_PATTERNS | "I'll use <X>", "I decided to <X>", "I went with <X>" |
Decyzje użytkownika (episodic) |
| PATTERN_PATTERNS | "I usually <X>", "I always <X>", "I never <X>" |
Trwałe wzorce behawioralne |
Przykładowe wzorce (uproszczone)
// From src/lib/memory/extraction.ts
const PREFERENCE_PATTERNS = [
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi,
];
const DECISION_PATTERNS = [
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi,
];
const PATTERN_PATTERNS = [/\bI\s+usually\s+([^.,\n]+)/gi, /\bI\s+always\s+([^.,\n]+)/gi];
Co jest wyodrębniane
Gdy użytkownik mówi:
"I prefer TypeScript. I'll use Postgres for this project. I always commit before pushing. I don't like Python." Ekstrakcja produkuje 4 memories:
Key Category Type Content preference:typescriptpreference factual "TypeScript" decision:postgres_for_this_projectdecision episodic "Postgres for this project" pattern:commit_before_pushingpattern factual "commit before pushing" preference:pythonpreference factual "Python"
Limity ekstrakcji
Aby zapobiec niekontrolowanej ekstrakcji, obowiązują następujące limity:
| Min content length | 3 chars | | Max content length | 500 chars |
Kiedy wyłączyć ekstrakcję
Ekstrakcja działa automatycznie, gdy memory jest włączone; nie ma osobnego
przełącznika tylko-ekstrakcja. Aby ją wyłączyć, wyłącz całkowicie memory (enabled: false
przez PUT /api/settings/memory). Rozważ to, gdy:
- Masz duży wolumen wiadomości i koszt ekstrakcji jest niebanalny
- Twoje rozmowy są głównie efemeryczne (chat, debugging) bez długoterminowej wartości
- Już przechwytujesz kontekst przez własne pluginy
Dostrajanie Hybrid RRF (v3.8.16+)
Algorytm Reciprocal Rank Fusion (RRF) łączy wyniki FTS5 (słowa kluczowe) i wektorowe (semantyczne). Parametr k kontroluje, ile wagi dostają niżej rankingowane wyniki.
Formuła
Dla każdego kandydata memory score RRF wynosi:
RRF(d) = Σ 1 / (k + rank_i(d))
Gdzie:
kto stała (domyślnie 60)rank_i(d)to ranga dokumentudw i-tym systemie retrieval (FTS, vector)- Suma biegnie po wszystkich systemach retrieval
Jak k wpływa na wyniki
k value |
Effect | Best for |
|---|---|---|
k=0 |
Czysta fuzja rang (bez wygładzania) | Teoretyczna baza |
k=10-30 |
Silnie waży top wyniki, niski rank prawie nie wnosi | Gdy top-3 zwykle jest poprawne |
k=60 (default) |
Zbalansowane — top-10 wyniki wszystkie wnoszą sensownie | Retrieval ogólnego przeznaczenia |
k=100+ |
Płaskiej — nawet niski rank może dominować, jeśli pojawia się w wielu systemach | Gdy recall > precision jest krytyczne |
Dostrajanie k w praktyce
# Default
MEMORY_RRF_K=60
# Aggressive precision (small memory, few docs)
MEMORY_RRF_K=20
# Maximum recall (large memory, varied queries)
MEMORY_RRF_K=120
Przykład z k=20:
- FTS rank 1 → wkład
1/21 = 0.048 - FTS rank 10 → wkład
1/30 = 0.033 - Vector rank 1 → wkład
0.048 - Combined max:
0.096
Przykład z k=60:
- FTS rank 1 → wkład
1/61 = 0.016 - FTS rank 10 → wkład
1/70 = 0.014 - Vector rank 1 → wkład
0.016 - Combined max:
0.033
Przy wyższym k względna różnica między top-1 a rank-10 jest mniejsza, więc algorytm bardziej polega na konsensusie między systemami retrieval niż na pewności top-rank.
Kiedy zmieniać k
| Symptom | Try |
|---|---|
| Top wynik zawsze wygrywa, ale jest zły | Obniż k (np. 20) — pewność top-rank ma większe znaczenie |
| Dobra odpowiedź jest w top-5, nie top-1 | Podnieś k (np. 100) — płaskie scoring nagradza konsensus |
| Recall wysoki, precision niska | Obniż k — wyostrz ranking |
| Recall niski (brakuje relevantnych docs) | Podnieś k — daj szansę niżej rankingowanym docs |
Wagi RRF
Reciprocal rank fusion używa równych wag dla rangi wektorowej semantycznej i rangi full-text search:
RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)
Nie ma zmiennych środowiskowych do regulacji wag indywidualnych (MEMORY_RRF_VECTOR_WEIGHT/MEMORY_RRF_FTS_WEIGHT nie istnieją).
Strategia summarization (v3.8.16+)
Moduł summarization.ts (src/lib/memory/summarization.ts) kompresuje starsze memories, żeby aktywny zbiór był mały przy zachowaniu recall.
Kiedy wyzwala się summarization
| Trigger | Threshold (default) |
|---|---|
| Manual trigger via API | n/a |
Co jest summarizowane
Dwa punkty wejścia eksportowane z summarization.ts:
summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000)— kondensuje memories sesji do pojedynczego tekstu podsumowania ograniczonego budżetem tokenów.summarizeMemoriesOlderThan(apiKeyId, days, dryRun)— kompakcja oparta o wiek używana przez API: wybiera każde memory starsze niżdays, buduje jedno skondensowane summary memory i (gdydryRunjestfalse) usuwa oryginały. PodajdryRun: true, aby podejrzeć zbiór kandydatów i sumę tokenów bez modyfikacji.
Nie ma przejścia klastrowania tag/key ani scoringu per-memory „core vs summarizable” — selekcja to czysto próg wieku, a tekst summary to skondensowana, prefiksowana typem linia na kandydata.
Wyzwalanie summarization
Summarization jest ręczne / opt-in — ustawienie autoSummarize jest false domyślnie,
więc nic nie jest kompaktowane automatycznie. Wyzwól przez API:
curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"
Aby zostawić wyłączone, po prostu trzymaj autoSummarize na domyślnym (false).
Wskazówki jakości summarization
- Najpierw podgląd z
dryRun—summarizeMemoriesOlderThan(..., true)zwraca listę kandydatów i całkowitą liczbę tokenów, żeby potwierdzić, co zostałoby scalone przed usunięciem oryginałów. - Uruchamiaj summarization w godzinach niskiego ruchu, jeśli masz duży korpus memory — wywołanie LLM to wolna część
# Cron-style: summarize at 3am daily
0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \
-H "Authorization: Bearer $OMNIROUTE_KEY"