From 83828e5a4420a00599fe9fa361bb90955d346eeb Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 8 Apr 2026 22:19:17 +0200 Subject: [PATCH] =?UTF-8?q?fix(research):=20handle=20zero-hit=20retrieval?= =?UTF-8?q?=20=E2=80=94=20skip=20empty=20insert=20+=20graceful=20summary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke-testing /api/v1/research/start with mana-search down surfaced a crash: drizzle's .values([]) throws "values() must be called with at least one value", which dropped the run into status='error' even though the failure is a perfectly normal "no results" case. Two changes: - Guard the sources insert behind enriched.length > 0 - If retrieval returns nothing, short-circuit straight to status='done' with an explicit German "keine Quellen gefunden" summary instead of feeding an empty corpus to the synthesiser The same path also triggers when every sub-query genuinely returns no results (very specific question, niche domain) so this isn't just an ops-failure case. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/modules/research/orchestrator.ts | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/apps/api/src/modules/research/orchestrator.ts b/apps/api/src/modules/research/orchestrator.ts index c2809d1a4..676442723 100644 --- a/apps/api/src/modules/research/orchestrator.ts +++ b/apps/api/src/modules/research/orchestrator.ts @@ -143,19 +143,41 @@ export async function runPipeline( } // Persist sources with stable rank order so citations [n] map to sources[n-1]. - await db.insert(sources).values( - enriched.map((e, idx) => ({ - researchResultId: id, - url: e.hit.url, - title: e.hit.title, - snippet: e.hit.snippet, - extractedContent: e.extractedText, - category: e.hit.category, - rank: idx + 1, - })) - ); + // Drizzle's .values([]) throws — only insert when we actually have hits. + if (enriched.length > 0) { + await db.insert(sources).values( + enriched.map((e, idx) => ({ + researchResultId: id, + url: e.hit.url, + title: e.hit.title, + snippet: e.hit.snippet, + extractedContent: e.extractedText, + category: e.hit.category, + rank: idx + 1, + })) + ); + } emit({ type: 'sources', count: enriched.length }); + // If retrieval found nothing (all sub-queries failed or genuinely + // no hits), skip synthesis and surface an explicit "no sources" + // summary instead of asking the LLM to fabricate one. + if (enriched.length === 0) { + await db + .update(researchResults) + .set({ + status: 'done', + summary: + 'Keine Quellen gefunden. Die Web-Suche hat keine Treffer geliefert — entweder ist die Frage zu spezifisch oder die Suchdienste sind aktuell nicht erreichbar.', + keyPoints: [], + followUpQuestions: [], + finishedAt: new Date(), + }) + .where(eq(researchResults.id, id)); + emit({ type: 'done', researchResultId: id }); + return; + } + // ─── Phase 3: Synthesise ─────────────────────────── await setStatus(id, 'synthesizing'); emit({ type: 'status', status: 'synthesizing' });