f3ecf8ffe4
The UI POSTs the SSE stream and, when nothing streams to the browser (iOS can't read a fetch-stream body → the fetch throws in ~1s), falls back to the blocking endpoint. But the server-side stream runs to completion regardless, so BOTH turns executed — double-persisting the message and (once logging became guaranteed) double-logging the hand. Make a turn idempotent instead of chasing why the client bails: the first request for a (session, message) owns it; a concurrent duplicate waits on the owner's Event and reuses its reply rather than running a second full turn. respond and respond_stream both claim/await; a finally always releases waiters. Short window so a genuine later resend still runs fresh. Verified with a threaded race: two simultaneous calls, body runs once, both get the same reply. Also fixes the duplicate user-message persistence (the same double-execution) that was polluting reconstructed history. 6 dedup tests + concurrency check; suite 232. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>