The persisted sync cursor only advanced on sync.done, so on every restart the event stream re-delivered all frames consumed since the last sync.done - and replayed notification frames re-showed their banner (e.g. 'thread deleted' again after close/reopen). Track the consumed outbox high-water cursor in the controller, persist it (debounced + on backgrounding), and skip notification frames whose cursor is at/below the mark (also covers the duplicate delivery of SSE catch-up + explicit sync replay on reconnect).