From b7c23ca23222d204689e0be2342251c00a4fff00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sisyphus=20=F0=9F=8F=94=EF=B8=8F?= Date: Sat, 11 Apr 2026 20:29:35 +0800 Subject: [PATCH] fix(ResponseProcessor): salvage synthetic summary when AI returns instead of Fixes Issue #1312: AI sometimes returns XML tags instead of tags during the summarize phase, despite clear instructions in buildSummaryPrompt() requiring ONLY output. When this occurs, parseSummary() returns null and the entire session summary is lost. This fix detects the condition (summary missing + observations present) and synthesizes a summary from the observation data, ensuring session summaries are not completely lost. The salvage mapping: - request: observation title - investigated: observation narrative or facts - learned: observation facts joined - completed: title if type is feature/bugfix - notes: indicates this is a synthetic salvage summary Observations are stored normally regardless of this fallback. Co-authored-by: Sisyphus --- .../worker/agents/ResponseProcessor.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/services/worker/agents/ResponseProcessor.ts b/src/services/worker/agents/ResponseProcessor.ts index 3487764f..74bd87ef 100644 --- a/src/services/worker/agents/ResponseProcessor.ts +++ b/src/services/worker/agents/ResponseProcessor.ts @@ -85,6 +85,27 @@ export async function processAgentResponse( // Convert nullable fields to empty strings for storeSummary (if summary exists) const summaryForStore = normalizeSummaryForStorage(summary); + // Fallback: When summary parse fails but observations exist, salvage a synthetic summary. + // Fixes Issue #1312: AI sometimes returns instead of despite clear instructions. + // Observations are stored normally; this only affects the session summary. + let finalSummaryForStore = summaryForStore; + if (!summaryForStore && observations.length > 0) { + const primary = observations[0]; + finalSummaryForStore = { + request: primary.title || `Session observations (${observations.length} items)`, + investigated: primary.narrative || primary.facts?.join('; ') || '', + learned: primary.facts?.join('; ') || '', + completed: primary.type === 'feature' || primary.type === 'bugfix' ? (primary.title || '') : '', + next_steps: '', + notes: `[Salvaged from ${observations.length} observation(s)] AI returned instead of ` + }; + logger.warn('PARSER', `SALVAGED summary from ${observations.length} observation(s) — AI did not output tags`, { + sessionId: session.sessionDbId, + agentName, + observationIds: observations.map(o => o.title).filter(Boolean).slice(0, 3) + }); + } + // Get session store for atomic transaction const sessionStore = dbManager.getSessionStore(); @@ -102,7 +123,7 @@ export async function processAgentResponse( sessionStore.ensureMemorySessionIdRegistered(session.sessionDbId, session.memorySessionId); // Log pre-storage with session ID chain for verification - logger.info('DB', `STORING | sessionDbId=${session.sessionDbId} | memorySessionId=${session.memorySessionId} | obsCount=${observations.length} | hasSummary=${!!summaryForStore}`, { + logger.info('DB', `STORING | sessionDbId=${session.sessionDbId} | memorySessionId=${session.memorySessionId} | obsCount=${observations.length} | hasSummary=${!!finalSummaryForStore}`, { sessionId: session.sessionDbId, memorySessionId: session.memorySessionId }); @@ -113,7 +134,7 @@ export async function processAgentResponse( session.memorySessionId, session.project, observations, - summaryForStore, + finalSummaryForStore, session.lastPromptNumber, discoveryTokens, originalTimestamp ?? undefined, @@ -153,7 +174,7 @@ export async function processAgentResponse( // Sync and broadcast summary if present await syncAndBroadcastSummary( summary, - summaryForStore, + finalSummaryForStore, result, session, dbManager,