From 53ebb683397bd1b56d7843dd50335f42934a0609 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 03:52:42 +0000 Subject: [PATCH] Use full article body, not just the RSS blurb, in synthesis prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildPrompt() only ever sent ContentItem.summary (a ~500-char RSS description) to the model, never .body (the full article text when the feed provides ) — even though publishDirect already preferred body over summary for the no-AI-merge path. A single-source cluster was effectively asking the model to "lightly rewrite" a one-paragraph blurb, which it did almost verbatim, producing a short repeated synopsis instead of an actual article. Now mirrors publishDirect's item.body || item.summary fallback. Body is already HTML-stripped at ingestion (ingestion/adapters/base.ts), so no new sanitization needed. The per-entry character budget added in the previous truncation fix now does real work here, since full bodies can be much longer than summaries. --- backend/src/pipeline/synthesis.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/src/pipeline/synthesis.ts b/backend/src/pipeline/synthesis.ts index 2cbac86..1405563 100644 --- a/backend/src/pipeline/synthesis.ts +++ b/backend/src/pipeline/synthesis.ts @@ -50,12 +50,17 @@ function buildPrompt(items: ContentItem[]): string { const budgetPerItem = Math.max(MIN_ENTRY_CHARS, Math.floor(MAX_INPUT_CHARS / items.length)); let truncated = 0; const entries = items.map((item, i) => { - const summary = capEntryText(item.summary, budgetPerItem); - if (summary !== item.summary) truncated++; - return `Source ${i + 1} (${item.sourceId}):\nTitle: ${item.title}\nSummary: ${summary}`; + // Same fallback publishDirect uses (publish.ts) — body is the full article text + // when the feed supplies it (e.g. RSS ), summary is a ~500-char + // blurb. Using summary alone starved the model of real content to synthesize + // from, so a single-source cluster just echoed the blurb back nearly verbatim. + const full = item.body || item.summary; + const text = capEntryText(full, budgetPerItem); + if (text !== full) truncated++; + return `Source ${i + 1} (${item.sourceId}):\nTitle: ${item.title}\nSummary: ${text}`; }); if (truncated > 0) { - logger.warn('synthesis', `Trimmed ${truncated}/${items.length} source summar${truncated === 1 ? 'y' : 'ies'} to fit the model's context window`); + logger.warn('synthesis', `Trimmed ${truncated}/${items.length} source article${truncated === 1 ? '' : 's'} to fit the model's context window`); } return entries.join('\n\n'); }