fix(ResponseProcessor): salvage synthetic summary when AI returns <observation> instead of <summary>
Fixes Issue #1312: AI sometimes returns <observation> XML tags instead of <summary> tags during the summarize phase, despite clear instructions in buildSummaryPrompt() requiring <summary> 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 <sisyphus@openclaw>
This commit is contained in:
@@ -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 <observation> instead of <summary> 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 <observation> instead of <summary>`
|
||||
};
|
||||
logger.warn('PARSER', `SALVAGED summary from ${observations.length} observation(s) — AI did not output <summary> 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,
|
||||
|
||||
Reference in New Issue
Block a user