0.167.45

Fixed

  • Транзакционная multi-key запись настроек ( — P3, аудит 2026-08-context). Закрыты два окна целостности из аудита #305: (1) partial-write — AdvancedHandler.Update (фаза 2) писал ключи циклом SetSetting, сбой середины оставлял частичный апдейт (для anti-loop пары agent_loop_warn/stop_threshold — потенциально невалидную пару в Store); (2) TOCTOU в pair-валидации — два параллельных Update, каждый со своей половиной пары, оба проходили проверку против старого значения другой стороны и сохраняли невалидную пару. Новый метод store.Store.SetSettings(ctx, map[string]string) — multi-key upsert одной транзакцией (SQLite: BeginTx + prepared upsert; PostgreSQL: pool.Begin, симметрично; DualStore — делегирование; батч-кап maxSettingsBatchSize=256, пустая map — no-op, детерминированный порядок ключей). Update пишет весь батч одним вызовом SetSettings (сбой → 500, rollback всего батча) и сериализован sync.RWMutex per-handler: критическая секция «pair-валидация → валидация → запись» под Lock (второй конкурентный Update валидируется против закоммиченного первого), Get — под RLock (заодно закрыт torn read пары: ранее два независимых GetSetting могли увидеть новый warn + старый stop). Отмена клиента в фазе записи — тихий выход без ERROR-лога и 500 (симметрично валидационной ветке). Read-side defense-in-depth (newToolLoopDetector нормализует пару на каждом Run) сохранён. Тесты (+8): SQLite-транзакционность (upsert всех ключей, перезапись, пустая map no-op, mid-tx сбой через per-instance test-hook → полный rollback, отменённый ctx, oversized-батч отклонён); handler — сбой SetSettings → 500 без частичных записей, отмена ctx → тихий возврат, конкурентный TOCTOU-тест (25 итераций параллельных half-pair Update: ровно один победитель на раунд, инвариант stop > warn в Store не нарушен ни разу; валидирован на pre-fix коде — падает без мьютекса).