Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assistant/src/__tests__/auto-analysis-end-to-end.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mock.module("../memory/qdrant-client.js", () => ({
deletePoints: async () => {},
}),
initQdrantClient: () => {},
resolveQdrantUrl: () => "http://127.0.0.1:6333",
}));

// ── Test config ────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions assistant/src/__tests__/memory-upsert-concurrency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions assistant/src/__tests__/task-memory-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mock.module("../memory/qdrant-client.js", () => ({
deletePoints: async () => {},
}),
initQdrantClient: () => {},
resolveQdrantUrl: () => "http://127.0.0.1:6333",
}));

const TEST_CONFIG = {
Expand Down
5 changes: 2 additions & 3 deletions assistant/src/cli/commands/conversations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Command } from "commander";

import {
getQdrantUrlEnv,
getRuntimeHttpHost,
getRuntimeHttpPort,
} from "../../config/env.js";
Expand All @@ -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,
Expand Down Expand Up @@ -247,7 +246,7 @@ Examples:
);

const config = getConfig();

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.

I would've expected qdrant.url to be "http://127.0.0.1:20200" in the workspace config if "resources.qdrantPort": 20200 in the lock file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. The CLI does not currently write memory.qdrant.url from the lock file — cli/src/lib/local.ts sets env.QDRANT_HTTP_PORT when spawning the daemon but never materializes the port into the workspace config. That is why resolveQdrantUrl has to fall back to the env var.

Treating that as a follow-up: the CLI should either (a) update memory.qdrant.url in the workspace config whenever resources.qdrantPort changes, or (b) remove memory.qdrant.url from the static config entirely and make the resolved URL derive solely from allocated resources + env overrides. Option (b) is probably cleaner — the static config default has caused divergence more than once.

Happy to open a separate ticket/PR for that once this centralization lands.

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}`
Expand Down
12 changes: 2 additions & 10 deletions assistant/src/daemon/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -741,13 +739,7 @@ export async function runDaemon(): Promise<void> {
// 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<void> {
// 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;
Expand Down
4 changes: 2 additions & 2 deletions assistant/src/memory/graph/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions assistant/src/memory/graph/graph-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mock.module("../qdrant-client.js", () => ({
},
}),
initQdrantClient: () => {},
resolveQdrantUrl: () => "http://127.0.0.1:6333",
VellumQdrantClient: class {},
}));

Expand Down
4 changes: 2 additions & 2 deletions assistant/src/memory/graph/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,7 +29,7 @@ initializeDb();
const config = getConfig();
try {

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.

As part of OOS'ing initQdrantClient, keep out try/catch wrapping

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — restored the try/catch here and in bootstrap.ts as part of OOS-ing the initializer changes.

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,
Expand Down
1 change: 1 addition & 0 deletions assistant/src/memory/pkb/pkb-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions assistant/src/memory/pkb/pkb-reconcile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mock.module("../qdrant-client.js", () => ({
deleteCalls.push({ path, memoryScopeId });
},
}),
resolveQdrantUrl: () => "http://127.0.0.1:6333",
}));

// Circuit breaker — pass-through.
Expand Down
1 change: 1 addition & 0 deletions assistant/src/memory/pkb/pkb-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
60 changes: 60 additions & 0 deletions assistant/src/memory/qdrant-client.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
25 changes: 25 additions & 0 deletions assistant/src/memory/qdrant-client.ts
Original file line number Diff line number Diff line change
@@ -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:<port>.
* 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[];
Expand Down
1 change: 1 addition & 0 deletions assistant/src/runtime/routes/memory-item-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand Down