From 5fba201fb10e2806d798a22d3c1ac62e8f5b68df Mon Sep 17 00:00:00 2001 From: ApolloBot Date: Wed, 22 Apr 2026 14:22:04 +0000 Subject: [PATCH 1/2] fix(memory): centralize Qdrant URL resolution in resolveQdrantUrl helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every site that constructs a Qdrant URL (daemon/lifecycle.ts, memory/graph/bootstrap.ts, memory/graph/inspect.ts, cli/commands/conversations.ts) now routes through a single helper `resolveQdrantUrl(config)` with the canonical precedence: 1. `QDRANT_HTTP_PORT` — locally-spawned sidecar on 127.0.0.1: (set by the CLI for multi-local instances) 2. `QDRANT_URL` — external Qdrant (K8s sidecar, remote URL) 3. `config.memory.qdrant.url` — static config default Previously bootstrap.ts and inspect.ts constructed the URL as `config.memory.qdrant.url ?? "http://127.0.0.1:6333"` — ignoring both env vars. When the CLI spawned the daemon on a non-default port via `QDRANT_HTTP_PORT` (e.g. 20200), the background `graph_bootstrap` memory job re-initialized the singleton against the schema default, silently redirecting every subsequent `getQdrantClient()` call away from the real sidecar. --- assistant/src/cli/commands/conversations.ts | 5 +- assistant/src/daemon/lifecycle.ts | 12 +---- assistant/src/memory/graph/bootstrap.ts | 4 +- assistant/src/memory/graph/inspect.ts | 4 +- assistant/src/memory/qdrant-client.test.ts | 60 +++++++++++++++++++++ assistant/src/memory/qdrant-client.ts | 25 +++++++++ 6 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 assistant/src/memory/qdrant-client.test.ts diff --git a/assistant/src/cli/commands/conversations.ts b/assistant/src/cli/commands/conversations.ts index fc307d4ca2e..b5e4eb62d9f 100644 --- a/assistant/src/cli/commands/conversations.ts +++ b/assistant/src/cli/commands/conversations.ts @@ -1,7 +1,6 @@ import type { Command } from "commander"; import { - getQdrantUrlEnv, getRuntimeHttpHost, getRuntimeHttpPort, } from "../../config/env.js"; @@ -25,7 +24,7 @@ import { SPARSE_EMBEDDING_VERSION, } from "../../memory/embedding-backend.js"; import { enqueueMemoryJob } from "../../memory/jobs-store.js"; -import { initQdrantClient } from "../../memory/qdrant-client.js"; +import { initQdrantClient, resolveQdrantUrl } from "../../memory/qdrant-client.js"; import { initAuthSigningKey, loadOrCreateSigningKey, @@ -247,7 +246,7 @@ Examples: ); const config = getConfig(); - const qdrantUrl = getQdrantUrlEnv() || config.memory.qdrant.url; + const qdrantUrl = resolveQdrantUrl(config); const embeddingSelection = await selectEmbeddingBackend(config); const embeddingModel = embeddingSelection.backend ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}:sparse-v${SPARSE_EMBEDDING_VERSION}` diff --git a/assistant/src/daemon/lifecycle.ts b/assistant/src/daemon/lifecycle.ts index c4572da6b6e..c25bf4867bd 100644 --- a/assistant/src/daemon/lifecycle.ts +++ b/assistant/src/daemon/lifecycle.ts @@ -12,8 +12,6 @@ import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js"; import { initFeatureFlagOverrides } from "../config/assistant-feature-flags.js"; import { getPlatformAssistantId, - getQdrantHttpPortEnv, - getQdrantUrlEnv, getRuntimeHttpHost, getRuntimeHttpPort, setIngressPublicBaseUrl, @@ -63,7 +61,7 @@ import { } from "../memory/embedding-backend.js"; import { enqueueMemoryJob } from "../memory/jobs-store.js"; import { startMemoryJobsWorker } from "../memory/jobs-worker.js"; -import { initQdrantClient } from "../memory/qdrant-client.js"; +import { initQdrantClient, resolveQdrantUrl } from "../memory/qdrant-client.js"; import { QdrantManager } from "../memory/qdrant-manager.js"; import { rotateToolInvocations } from "../memory/tool-usage-store.js"; import { deleteOldTraceEvents } from "../memory/trace-event-store.js"; @@ -741,13 +739,7 @@ export async function runDaemon(): Promise { // Initialize Qdrant vector store and memory worker in the background so the // RuntimeHttpServer can start accepting requests without waiting for Qdrant. async function initializeQdrantAndMemory(): Promise { - // Prefer QDRANT_HTTP_PORT (locally-spawned Qdrant on a specific port) over - // QDRANT_URL (external Qdrant instance) so the CLI can set the port without - // triggering QdrantManager's external mode which skips local process spawn. - const qdrantHttpPort = getQdrantHttpPortEnv(); - const qdrantUrl = qdrantHttpPort - ? `http://127.0.0.1:${qdrantHttpPort}` - : getQdrantUrlEnv() || config.memory.qdrant.url; + const qdrantUrl = resolveQdrantUrl(config); log.info({ qdrantUrl }, "Daemon startup: initializing Qdrant"); const manager = new QdrantManager({ url: qdrantUrl }); bgRefs.qdrantManager = manager; diff --git a/assistant/src/memory/graph/bootstrap.ts b/assistant/src/memory/graph/bootstrap.ts index 49f0232a9d0..1605e34815b 100644 --- a/assistant/src/memory/graph/bootstrap.ts +++ b/assistant/src/memory/graph/bootstrap.ts @@ -21,7 +21,7 @@ import { getWorkspaceDir } from "../../util/platform.js"; import { getMemoryCheckpoint, setMemoryCheckpoint } from "../checkpoints.js"; import { getDb, rawAll, rawGet, rawRun } from "../db.js"; import { enqueueMemoryJob, hasActiveJobOfType } from "../jobs-store.js"; -import { initQdrantClient } from "../qdrant-client.js"; +import { initQdrantClient, resolveQdrantUrl } from "../qdrant-client.js"; import { conversations, memoryGraphNodes, memorySegments } from "../schema.js"; import { runGraphExtraction } from "./extraction.js"; import { countNodes } from "./store.js"; @@ -71,7 +71,7 @@ export async function bootstrapFromHistory( // Initialize Qdrant client for inline embedding try { initQdrantClient({ - url: config.memory.qdrant.url ?? "http://127.0.0.1:6333", + url: resolveQdrantUrl(config), collection: config.memory.qdrant.collection, vectorSize: config.memory.qdrant.vectorSize, onDisk: config.memory.qdrant.onDisk ?? true, diff --git a/assistant/src/memory/graph/inspect.ts b/assistant/src/memory/graph/inspect.ts index 6e91dec96f4..05d548cb987 100644 --- a/assistant/src/memory/graph/inspect.ts +++ b/assistant/src/memory/graph/inspect.ts @@ -14,7 +14,7 @@ import { getConfig } from "../../config/loader.js"; import { initializeDb } from "../db-init.js"; -import { initQdrantClient } from "../qdrant-client.js"; +import { initQdrantClient, resolveQdrantUrl } from "../qdrant-client.js"; import { countNodes, getEdgesForNode, @@ -29,7 +29,7 @@ initializeDb(); const config = getConfig(); try { initQdrantClient({ - url: config.memory.qdrant.url ?? "http://127.0.0.1:6333", + url: resolveQdrantUrl(config), collection: config.memory.qdrant.collection, vectorSize: config.memory.qdrant.vectorSize, onDisk: config.memory.qdrant.onDisk ?? true, diff --git a/assistant/src/memory/qdrant-client.test.ts b/assistant/src/memory/qdrant-client.test.ts new file mode 100644 index 00000000000..ce21c6a9ec0 --- /dev/null +++ b/assistant/src/memory/qdrant-client.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; + +import { applyNestedDefaults } from "../config/loader.js"; +import type { AssistantConfig } from "../config/types.js"; +import { resolveQdrantUrl } from "./qdrant-client.js"; + +const DEFAULT_CONFIG: AssistantConfig = applyNestedDefaults({}); + +describe("resolveQdrantUrl", () => { + const savedPort = process.env.QDRANT_HTTP_PORT; + const savedUrl = process.env.QDRANT_URL; + + beforeEach(() => { + delete process.env.QDRANT_HTTP_PORT; + delete process.env.QDRANT_URL; + }); + + afterEach(() => { + if (savedPort === undefined) delete process.env.QDRANT_HTTP_PORT; + else process.env.QDRANT_HTTP_PORT = savedPort; + if (savedUrl === undefined) delete process.env.QDRANT_URL; + else process.env.QDRANT_URL = savedUrl; + }); + + test("falls back to config when no env vars are set", () => { + expect(resolveQdrantUrl(DEFAULT_CONFIG)).toBe("http://127.0.0.1:6333"); + }); + + test("honours QDRANT_URL when set", () => { + process.env.QDRANT_URL = "http://qdrant.example.com:6333"; + expect(resolveQdrantUrl(DEFAULT_CONFIG)).toBe( + "http://qdrant.example.com:6333", + ); + }); + + test("QDRANT_HTTP_PORT wins over QDRANT_URL", () => { + process.env.QDRANT_URL = "http://qdrant.example.com:6333"; + process.env.QDRANT_HTTP_PORT = "20200"; + expect(resolveQdrantUrl(DEFAULT_CONFIG)).toBe("http://127.0.0.1:20200"); + }); + + test("QDRANT_HTTP_PORT wins over config default", () => { + process.env.QDRANT_HTTP_PORT = "20200"; + expect(resolveQdrantUrl(DEFAULT_CONFIG)).toBe("http://127.0.0.1:20200"); + }); + + test("respects a non-default config URL when no env is set", () => { + const config: AssistantConfig = { + ...DEFAULT_CONFIG, + memory: { + ...DEFAULT_CONFIG.memory, + qdrant: { + ...DEFAULT_CONFIG.memory.qdrant, + url: "http://custom-host:9999", + }, + }, + }; + expect(resolveQdrantUrl(config)).toBe("http://custom-host:9999"); + }); +}); diff --git a/assistant/src/memory/qdrant-client.ts b/assistant/src/memory/qdrant-client.ts index df67fa1fa0f..848a50c07d8 100644 --- a/assistant/src/memory/qdrant-client.ts +++ b/assistant/src/memory/qdrant-client.ts @@ -1,10 +1,35 @@ import { QdrantClient as QdrantRestClient } from "@qdrant/js-client-rest"; import { v4 as uuid } from "uuid"; +import { getQdrantHttpPortEnv, getQdrantUrlEnv } from "../config/env.js"; +import type { AssistantConfig } from "../config/types.js"; import { getLogger } from "../util/logger.js"; const log = getLogger("qdrant-client"); +/** + * Resolve the Qdrant base URL for this process. + * + * Precedence (highest first): + * 1. `QDRANT_HTTP_PORT` — a locally-spawned Qdrant sidecar on 127.0.0.1:. + * Set by the CLI when spawning the daemon with a non-default port + * (multi-local instances). + * 2. `QDRANT_URL` — an external Qdrant instance (K8s sidecar, remote URL). + * 3. `config.memory.qdrant.url` — static config (defaults to + * `http://127.0.0.1:6333`). + * + * Every caller that constructs a Qdrant URL should route through this helper + * so the precedence stays consistent across the daemon, CLI, and any + * background jobs that re-read the config. + */ +export function resolveQdrantUrl(config: AssistantConfig): string { + const port = getQdrantHttpPortEnv(); + if (port) return `http://127.0.0.1:${port}`; + const url = getQdrantUrlEnv(); + if (url) return url; + return config.memory.qdrant.url; +} + export interface QdrantSparseVector { indices: number[]; values: number[]; From ab2635f4af6638c4823e5b67413d80ba1706e5f9 Mon Sep 17 00:00:00 2001 From: ApolloBot Date: Wed, 22 Apr 2026 14:53:46 +0000 Subject: [PATCH 2/2] test(memory): add resolveQdrantUrl stub to qdrant-client mocks Ten test files use `mock.module("../qdrant-client.js", ...)` with explicit export shapes. Adding `resolveQdrantUrl` to the exported surface broke graph-search.test.ts because its mock didn't declare the new symbol, and a later module-reload pathway needed it. Stubs return the schema default (`http://127.0.0.1:6333`); no test exercises the URL resolution itself, so the value is inert. --- assistant/src/__tests__/auto-analysis-end-to-end.test.ts | 1 + assistant/src/__tests__/conversation-history-web-search.test.ts | 1 + assistant/src/__tests__/memory-upsert-concurrency.test.ts | 1 + assistant/src/__tests__/regenerate-fire-and-forget-trace.test.ts | 1 + assistant/src/__tests__/task-memory-cleanup.test.ts | 1 + assistant/src/memory/graph/graph-search.test.ts | 1 + assistant/src/memory/pkb/pkb-index.test.ts | 1 + assistant/src/memory/pkb/pkb-reconcile.test.ts | 1 + assistant/src/memory/pkb/pkb-search.test.ts | 1 + assistant/src/runtime/routes/memory-item-routes.test.ts | 1 + 10 files changed, 10 insertions(+) diff --git a/assistant/src/__tests__/auto-analysis-end-to-end.test.ts b/assistant/src/__tests__/auto-analysis-end-to-end.test.ts index ff467ccbeb3..f92536f6e72 100644 --- a/assistant/src/__tests__/auto-analysis-end-to-end.test.ts +++ b/assistant/src/__tests__/auto-analysis-end-to-end.test.ts @@ -50,6 +50,7 @@ mock.module("../memory/qdrant-client.js", () => ({ deletePoints: async () => {}, }), initQdrantClient: () => {}, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); // ── Test config ──────────────────────────────────────────────────── diff --git a/assistant/src/__tests__/conversation-history-web-search.test.ts b/assistant/src/__tests__/conversation-history-web-search.test.ts index d8e26a35aec..8a20f69a99f 100644 --- a/assistant/src/__tests__/conversation-history-web-search.test.ts +++ b/assistant/src/__tests__/conversation-history-web-search.test.ts @@ -72,6 +72,7 @@ mock.module("../memory/qdrant-client.js", () => ({ getQdrantClient: () => { throw new Error("Qdrant not initialized"); }, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); // Import after mocking diff --git a/assistant/src/__tests__/memory-upsert-concurrency.test.ts b/assistant/src/__tests__/memory-upsert-concurrency.test.ts index c9d59a53c5f..216388f8dc5 100644 --- a/assistant/src/__tests__/memory-upsert-concurrency.test.ts +++ b/assistant/src/__tests__/memory-upsert-concurrency.test.ts @@ -35,6 +35,7 @@ mock.module("../memory/qdrant-client.js", () => ({ deletePoints: async () => {}, }), initQdrantClient: () => {}, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); import { DEFAULT_CONFIG } from "../config/defaults.js"; diff --git a/assistant/src/__tests__/regenerate-fire-and-forget-trace.test.ts b/assistant/src/__tests__/regenerate-fire-and-forget-trace.test.ts index 720e9a9a29b..d33d090a1fd 100644 --- a/assistant/src/__tests__/regenerate-fire-and-forget-trace.test.ts +++ b/assistant/src/__tests__/regenerate-fire-and-forget-trace.test.ts @@ -54,6 +54,7 @@ mock.module("../memory/qdrant-client.js", () => ({ getQdrantClient: () => { throw new Error("Qdrant not initialized"); }, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); import { diff --git a/assistant/src/__tests__/task-memory-cleanup.test.ts b/assistant/src/__tests__/task-memory-cleanup.test.ts index 0a3caaac1e0..ef96992f231 100644 --- a/assistant/src/__tests__/task-memory-cleanup.test.ts +++ b/assistant/src/__tests__/task-memory-cleanup.test.ts @@ -18,6 +18,7 @@ mock.module("../memory/qdrant-client.js", () => ({ deletePoints: async () => {}, }), initQdrantClient: () => {}, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); const TEST_CONFIG = { diff --git a/assistant/src/memory/graph/graph-search.test.ts b/assistant/src/memory/graph/graph-search.test.ts index 5c05647f21f..347ed0e38c0 100644 --- a/assistant/src/memory/graph/graph-search.test.ts +++ b/assistant/src/memory/graph/graph-search.test.ts @@ -49,6 +49,7 @@ mock.module("../qdrant-client.js", () => ({ }, }), initQdrantClient: () => {}, + resolveQdrantUrl: () => "http://127.0.0.1:6333", VellumQdrantClient: class {}, })); diff --git a/assistant/src/memory/pkb/pkb-index.test.ts b/assistant/src/memory/pkb/pkb-index.test.ts index 0c51b4b4f8a..024c629784d 100644 --- a/assistant/src/memory/pkb/pkb-index.test.ts +++ b/assistant/src/memory/pkb/pkb-index.test.ts @@ -96,6 +96,7 @@ mock.module("../qdrant-client.js", () => ({ return scrollReturnPoints; }, }), + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); // The circuit breaker is a thin wrapper; just call the function through. diff --git a/assistant/src/memory/pkb/pkb-reconcile.test.ts b/assistant/src/memory/pkb/pkb-reconcile.test.ts index 88821bf2dc7..a308c88c136 100644 --- a/assistant/src/memory/pkb/pkb-reconcile.test.ts +++ b/assistant/src/memory/pkb/pkb-reconcile.test.ts @@ -46,6 +46,7 @@ mock.module("../qdrant-client.js", () => ({ deleteCalls.push({ path, memoryScopeId }); }, }), + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); // Circuit breaker — pass-through. diff --git a/assistant/src/memory/pkb/pkb-search.test.ts b/assistant/src/memory/pkb/pkb-search.test.ts index b10d3d78c5d..09c67dc7b0c 100644 --- a/assistant/src/memory/pkb/pkb-search.test.ts +++ b/assistant/src/memory/pkb/pkb-search.test.ts @@ -56,6 +56,7 @@ mock.module("../qdrant-client.js", () => ({ return denseResults; }, }), + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); const { searchPkbFiles } = await import("./pkb-search.js"); diff --git a/assistant/src/runtime/routes/memory-item-routes.test.ts b/assistant/src/runtime/routes/memory-item-routes.test.ts index 69d9031cc7a..c5a13aab0cf 100644 --- a/assistant/src/runtime/routes/memory-item-routes.test.ts +++ b/assistant/src/runtime/routes/memory-item-routes.test.ts @@ -56,6 +56,7 @@ mock.module("../../memory/qdrant-client.js", () => ({ searchWithFilter: async () => [...mockHybridSearchResults], }), initQdrantClient: () => {}, + resolveQdrantUrl: () => "http://127.0.0.1:6333", })); mock.module("../../memory/qdrant-circuit-breaker.js", () => ({