4b78e336a2
F1 [correctness] — supersede: перекрытие стримов рушило свежий run. ai@6 AbstractChat.makeRequest в finally читает и обнуляет общий activeResponse, поэтому параллельные стрим-A и стрим-B корёжат друг друга, а незастемпленный onFinish мёртвого A уводил ЖИВОЙ новый run в ложный reconnect / сбрасывал runFact. Фикс: (1) FINISH_*/STREAM_INCOMPLETE штампуются per-stream generation (turnEpochRef, взводится в момент старта стрима) → фильтр I1 отбрасывает финиш чужого поколения; (2) sendNow-supersede АБОРТИТ A и стартует B только из onFinish A (микротаск, после того как finally A обнулил activeResponse) — гарантия отсутствия перекрытия. Тест на поздний isDisconnect A после SUPERSEDE_REQUESTED: машина НЕ уходит в reconnect, B отправлен. MUTATION-VERIFY: снятие epoch-штампа у FINISH_DISCONNECT → тест краснеет («Connection lost — reconnecting»). F2 [correctness] — гонка mount getRun→ATTACH_START с локальным send. Редьюсер ATTACH_START теперь игнорирует любую не-idle фазу, поэтому поздний резолв getRun не перехватывает начавшийся локальный турн в observer-attach. Тест на гонку. F3 [ghost feature] — RUN_SUPERSEDED объявлен+покрыт тестом, но НИКОГДА не диспатчился. Удалён (событие+обработчик+тест+postRun-reason observer-follow) как сознательный scope-cut: наблюдатель убитого supersede-рана и так следует за новым через деградированный поллинг (свежие строки истории, независимо от runId). F4 [hygiene] — мёртвые события. STREAM_START подключён (первый ассистент-фрейм локального турна: sending→streaming, спека↔код совпали). RECONNECT_BEGIN и POLL_ACTIVITY удалены (не диспатчились). Множество событий редьюсера = множеству диспатчащихся; редьюсер тотален. Тесты: FSM 35 + chat-thread 37 (+error 26 +adopt 16) = 114 зелёных; tsc ai-chat 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>