From f0d841f605663963c97f3f6625b03c99e73ad07d Mon Sep 17 00:00:00 2001 From: siddseethepalli Date: Sat, 14 Feb 2026 18:37:13 +0000 Subject: [PATCH] Replace daemon restart with resilient Qdrant collection recovery Instead of restarting the entire daemon after `sessions clear` deletes the Qdrant collection, make VellumQdrantClient self-healing: detect "collection not found" errors, reset the collectionReady flag, and retry the operation. This avoids the production-impacting issue where restarting via stopDaemon()/startDaemon() relaunches the daemon with the CLI process's environment instead of the original daemon context, silently changing behavior-affecting env vars like QDRANT_URL and RUNTIME_HTTP_PORT. Co-Authored-By: Claude Opus 4.6 --- assistant/src/index.ts | 9 -- assistant/src/memory/qdrant-client.ts | 122 +++++++++++++++++++++----- 2 files changed, 99 insertions(+), 32 deletions(-) diff --git a/assistant/src/index.ts b/assistant/src/index.ts index 67f2262a809..e8001281ade 100755 --- a/assistant/src/index.ts +++ b/assistant/src/index.ts @@ -331,15 +331,6 @@ sessions console.log('Qdrant collection not found or not reachable (skipped)'); } - // Restart the daemon so its in-memory Qdrant client drops the stale - // collectionReady flag and will re-create the collection on next use. - const status = getDaemonStatus(); - if (status.running) { - await stopDaemon(); - await startDaemon(); - console.log('Daemon restarted'); - } - console.log('Done.'); }); diff --git a/assistant/src/memory/qdrant-client.ts b/assistant/src/memory/qdrant-client.ts index ba3ba8536a2..a20f3c204d4 100644 --- a/assistant/src/memory/qdrant-client.ts +++ b/assistant/src/memory/qdrant-client.ts @@ -146,20 +146,43 @@ export class VellumQdrantClient { const existing = await this.findByTarget(targetType, targetId); const pointId = existing ?? uuid(); - await this.client.upsert(this.collection, { - wait: true, - points: [ - { - id: pointId, - vector, - payload: { - target_type: targetType, - target_id: targetId, - ...payload, + try { + await this.client.upsert(this.collection, { + wait: true, + points: [ + { + id: pointId, + vector, + payload: { + target_type: targetType, + target_id: targetId, + ...payload, + }, }, - }, - ], - }); + ], + }); + } catch (err) { + if (this.isCollectionMissing(err)) { + this.collectionReady = false; + await this.ensureCollection(); + await this.client.upsert(this.collection, { + wait: true, + points: [ + { + id: pointId, + vector, + payload: { + target_type: targetType, + target_id: targetId, + ...payload, + }, + }, + ], + }); + } else { + throw err; + } + } return pointId; } @@ -171,13 +194,30 @@ export class VellumQdrantClient { ): Promise { await this.ensureCollection(); - const results = await this.client.search(this.collection, { - vector, - limit, - with_payload: true, - score_threshold: 0.0, - filter: filter as Parameters[1]['filter'], - }); + let results; + try { + results = await this.client.search(this.collection, { + vector, + limit, + with_payload: true, + score_threshold: 0.0, + filter: filter as Parameters[1]['filter'], + }); + } catch (err) { + if (this.isCollectionMissing(err)) { + this.collectionReady = false; + await this.ensureCollection(); + results = await this.client.search(this.collection, { + vector, + limit, + with_payload: true, + score_threshold: 0.0, + filter: filter as Parameters[1]['filter'], + }); + } else { + throw err; + } + } return results.map((result) => ({ id: typeof result.id === 'string' ? result.id : String(result.id), @@ -235,7 +275,7 @@ export class VellumQdrantClient { async deleteByTarget(targetType: string, targetId: string): Promise { await this.ensureCollection(); - await this.client.delete(this.collection, { + const doDelete = () => this.client.delete(this.collection, { wait: true, filter: { must: [ @@ -244,12 +284,35 @@ export class VellumQdrantClient { ], }, }); + + try { + await doDelete(); + } catch (err) { + if (this.isCollectionMissing(err)) { + this.collectionReady = false; + await this.ensureCollection(); + await doDelete(); + } else { + throw err; + } + } } async count(): Promise { await this.ensureCollection(); - const result = await this.client.count(this.collection, { exact: false }); - return result.count; + + try { + const result = await this.client.count(this.collection, { exact: false }); + return result.count; + } catch (err) { + if (this.isCollectionMissing(err)) { + this.collectionReady = false; + await this.ensureCollection(); + const result = await this.client.count(this.collection, { exact: false }); + return result.count; + } + throw err; + } } async deleteCollection(): Promise { @@ -265,6 +328,19 @@ export class VellumQdrantClient { } } + /** + * Detect "collection not found" errors from Qdrant so callers can + * reset collectionReady and retry after an external deletion + * (e.g. `vellum sessions clear`). + */ + private isCollectionMissing(err: unknown): boolean { + if (err && typeof err === 'object' && 'status' in err && (err as { status: number }).status === 404) { + return true; + } + const msg = err instanceof Error ? err.message : String(err); + return msg.includes('Not found') || msg.includes('doesn\'t exist') || msg.includes('not found'); + } + private async findByTarget(targetType: string, targetId: string): Promise { try { const results = await this.client.scroll(this.collection, {