When the chat is opened while a chain is already streaming, the recovery
poll (_streamingPollInterval) is started. Its forced pollHistory(true)
renders the DB messages while the live SSE bubbles render them too — the
live bubbles never advance lastKnownMessageId, so the poll re-fetches the
same assistant message and renders a second bubble.
Make live SSE and the recovery poll mutually exclusive:
- Stop a lingering recovery poll at the start of a real live stream so the
SSE renders alone.
- Re-ensure the regular (isStreaming-guarded) history polling idempotently
in the finally block, so it is not left off after entering a running chain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>