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.