86eb0632e0e85a3b7c6b4ec2cfbf365941b9a7a2
Три фикса в runTurn/interrupt: 1. **Race condition в finally-блоке.** Раньше сбрасывал interrupted.set(false) только если флаг был установлен при чтении wasInterrupted в начале finally. Если interrupt() приходил между этими двумя точками — флаг оставался true и следующий turn видел wasInterruptedAtEntry=true → сразу short-circuit'ил без вызова LLM. Теперь всегда сбрасываем (compareAndSet атомарен, гарантирует следующий turn чистый). 2. **interrupt() отравлял следующий send.** Если вызывали interrupt() в пустоту (нет активного turn'а — флаг всё равно ставился → следующий send сразу short-circuit'ил, пользователь не получал ответа на своё 'Ок.' после явного cancel). Теперь interrupt() проверяет activeTurn?.isActive и при отсутствии активного turn'а — no-op. 3. **Bounded background scope для review/reflection/skill-mining.** OpenAiLlm.send() использует runBlocking — если запустить 30+ параллельных review (по одному на беседу), IO-thread pool голодает и ассистент висит. Вынес в отдельный scope с Dispatchers.IO.limitedParallelism(4) — не больше 4 sync LLM вызовов одновременно. Тест 27/27 (см. /tmp/test-interrupt.py и /tmp/run-manual-tests.py).
Description