From 521772c0826cf2df24a2e2b6140f65580eae82f8 Mon Sep 17 00:00:00 2001
From: Diego Rodrigues de Sa e Souza
<8016841+diegosouzapw@users.noreply.github.com>
Date: Wed, 1 Jul 2026 01:15:46 -0300
Subject: [PATCH] fix(memory): enabling Qdrant activates it as the engine +
inline guidance (#5597) (#5741)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(memory): enabling Qdrant now activates it as the engine + inline guidance (#5597)
Enabling Qdrant in the Engine tab was inert: retrieval only routes to Qdrant when
memoryVectorStore === "qdrant" (the default "auto" never selects it), and the card
only wrote qdrantEnabled — nothing set the engine selector, and there is no UI for
it. So users configured Qdrant, saw "enabled", but it was never actually used.
- PUT /api/settings/qdrant now sets memoryVectorStore alongside the toggle:
enable -> "qdrant", disable -> "auto". Editing other fields leaves it untouched.
- Add inline guidance to QdrantConfigCard: a Tier-1-vs-Tier-2 banner + per-field
help (host, collection, embedding model). Note there is no "vector dimension" or
"distance metric" field: dimension is auto-detected from the embedder, distance
is always Cosine.
- Document the real behavior in MEMORY.md: engine gate, no back-fill of existing
memories, dimension auto-detect, Cosine-only, API-key-only auth.
Tests: tests/integration/qdrant-routes.test.ts — enable->qdrant, disable->auto, and
field-edit-without-enabled leaves the engine untouched (TDD: red -> green).
Closes #5597
* fix(memory): invalidate memory-settings cache on Qdrant toggle (#5597)
The PUT handler wrote memoryVectorStore to the DB but retrieval reads through
getMemorySettings(), a module-level cache. Without busting it, the engine switch
did not take effect until a process restart (the DB said qdrant, retrieval kept
routing to sqlite-vec). Now calls invalidateMemorySettingsCache() after the write,
mirroring src/app/api/settings/memory/route.ts.
Regression test warms the cache, toggles via the route, and asserts
getMemorySettings().vectorStore flips to qdrant (fails without the invalidate call).
---
docs/frameworks/MEMORY.md | 28 +++++-
.../memory/components/QdrantConfigCard.tsx | 8 ++
src/app/api/settings/qdrant/route.ts | 14 ++-
src/i18n/messages/en.json | 4 +
tests/integration/qdrant-routes.test.ts | 97 +++++++++++++++++++
5 files changed, 149 insertions(+), 2 deletions(-)
diff --git a/docs/frameworks/MEMORY.md b/docs/frameworks/MEMORY.md
index 306eac9ca0..14f35f68a6 100644
--- a/docs/frameworks/MEMORY.md
+++ b/docs/frameworks/MEMORY.md
@@ -223,7 +223,13 @@ chronological order if the FTS table is missing or the FTS query throws.
### Optional: Qdrant (vector store tier 2)
`src/lib/memory/qdrant.ts` implements an optional Qdrant integration as tier 2
-vector store. Enabled via `qdrantEnabled` in settings / toggle in Engine tab.
+vector store. Retrieval only routes to Qdrant when the engine selector
+`memoryVectorStore === "qdrant"` — the default `"auto"` (and `"sqlite-vec"`)
+**never** select Qdrant. The Engine-tab toggle sets **both** `qdrantEnabled` and
+`memoryVectorStore` together: enabling makes Qdrant the primary store, disabling
+resets to `"auto"` (#5597 — before that fix, enabling was inert because nothing
+wrote the engine selector). If Qdrant is unreachable or returns nothing, retrieval
+falls back to sqlite-vec → FTS5.
- `upsertSemanticMemoryPoint()` — embed `key + content` with the configured
embedding model, ensure the collection exists (creates cosine-distance
@@ -251,6 +257,26 @@ routes under `src/app/api/settings/qdrant/` are all wired as of v3.8.6:
| `/api/settings/qdrant/cleanup` | `POST` | Remove expired / old points |
| `/api/settings/qdrant/embedding-models` | `GET` | List available embedding models |
+**Behavior notes (what to expect):**
+
+- **Engine selection** — enabling Qdrant in the Engine tab makes it the primary
+ store (sets `memoryVectorStore="qdrant"`); disabling resets to `"auto"` (#5597).
+- **No back-fill** — only memories created/updated **after** Qdrant is enabled are
+ written to it (fire-and-forget dual-write). Pre-existing SQLite memories are **not**
+ migrated; "Reindex Now" rebuilds the sqlite-vec index only, not Qdrant.
+- **Vector dimension is auto-detected** from the actual embedding on first use — there
+ is no dimension field to fill in. Changing the embedding model after a collection
+ exists is **not** auto-handled: the existing collection is left untouched, dimension-
+ mismatched writes/searches fail and fall back to sqlite-vec. Recreate the collection
+ (new name, or delete it in Qdrant) to switch embedders.
+- **Distance metric** — always **Cosine** (hardcoded on collection creation; not
+ configurable).
+- **Auth** — API key only (sent as the `api-key` header; optional for unauthenticated
+ local Docker). JWT/RBAC are not used.
+- **Config fields** — the UI exposes `host`, `port`, `collection`, `embeddingModel`,
+ `apiKey`. `vectorSize` / `hnswEfConstruct` are env/DB only and `vectorSize` is not
+ used for collection creation (dimension comes from the embedding).
+
### Vector quantization (int8 — opt-in, both backends)
Both vector backends support **opt-in int8 quantization** to cut the memory
diff --git a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx
index 30b44ffeca..68ab2231d1 100644
--- a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx
+++ b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx
@@ -205,6 +205,11 @@ export default function QdrantConfigCard() {
+ {/* Tier 1 vs Tier 2 guidance */}
+