fix: writing stuck - handle empty model response + no-divider fallback + timeline cleanup
Root causes of writing getting stuck: 1. Model returns empty response for a section → now detected early with actionable error message including model name 2. Model responds but without ~~~ARTICLE~~~ divider (happens with fallback models like Gemini) → now treats entire response as markdown content 3. Stream ends without 'complete' event (error/exit in PHP) → JS timeline entries lingered as 'active' forever. Now deactivated on stream close. 4. Error messages in execution flow now use structured formatAiErrorMessage with retry button instead of raw text Also: deactivateActiveTimelineEntries called in catch block so errors properly clear the 'Writing section X' status indicator.
This commit is contained in:
@@ -2912,12 +2912,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
// If stream ended without a 'complete' event, deactivate lingering timeline entries
|
||||||
|
setMessages(prev => {
|
||||||
|
const hasActive = prev.some(m => m.type === 'timeline' && m.status && !['complete', 'inactive', 'stopped'].includes(m.status));
|
||||||
|
if (hasActive) {
|
||||||
|
return deactivateActiveTimelineEntries(prev);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setAgentMode(currentPlanRef.current ? 'planning' : 'chat');
|
setAgentMode(currentPlanRef.current ? 'planning' : 'chat');
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...deactivateActiveTimelineEntries(prev), {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
content: 'Error: ' + (error.message || 'Failed to execute outline'),
|
content: formatAiErrorMessage(error, 'Failed to execute outline'),
|
||||||
canRetry: true,
|
canRetry: true,
|
||||||
retryType: 'execute',
|
retryType: 'execute',
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -3343,6 +3343,42 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle empty response from model
|
||||||
|
if ( empty( trim( (string) $accumulated_content ) ) ) {
|
||||||
|
$model_used = $response['model'] ?? 'unknown';
|
||||||
|
wpaw_debug_log( "Section writing got empty response from model: {$model_used}" );
|
||||||
|
echo "data: " . wp_json_encode(
|
||||||
|
array(
|
||||||
|
'type' => 'error',
|
||||||
|
'message' => sprintf( 'Section "%s" got an empty response from the AI model (%s). Please retry.', $heading, $model_used ),
|
||||||
|
)
|
||||||
|
) . "\n\n";
|
||||||
|
flush();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If divider was never found, treat the entire content as markdown
|
||||||
|
if ( ! $divider_found ) {
|
||||||
|
wpaw_debug_log( 'No ~~~ARTICLE~~~ divider found in section response. Using full content as markdown.' );
|
||||||
|
$markdown_content = $accumulated_content;
|
||||||
|
// Strip any leading conversational fluff (first line if it looks like a note)
|
||||||
|
$lines = explode( "\n", $markdown_content );
|
||||||
|
if ( ! empty( $lines[0] ) && ! preg_match( '/^#{1,3}\s/', $lines[0] ) && strlen( $lines[0] ) < 200 ) {
|
||||||
|
// First line might be a brief conversational note, skip it
|
||||||
|
$first_line = array_shift( $lines );
|
||||||
|
if ( ! empty( $first_line ) ) {
|
||||||
|
echo "data: " . wp_json_encode(
|
||||||
|
array(
|
||||||
|
'type' => 'conversational',
|
||||||
|
'content' => trim( $first_line ),
|
||||||
|
)
|
||||||
|
) . "\n\n";
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
$markdown_content = implode( "\n", $lines );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$section_cost = $response['cost'] ?? 0;
|
$section_cost = $response['cost'] ?? 0;
|
||||||
$total_cost += $section_cost;
|
$total_cost += $section_cost;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user