Список проблем, выявленных в ходе аудита 2026-05-16. Решаем по одной, сверху вниз. После исправления каждого пункта — отмечать галочкой и обновлять этот файл.
navi/core/agent.py ✅Severity: Critical Файл: navi/core/agent.py (1349 → ~410 строк) Проблема: Класс Agent одновременно управляет тремя режимами (run, run_stream, run_ephemeral), компрессией контекста (3 уровня fallback), планированием, анти-столлингом, адаптивным репланингом, подсчётом токенов, stall-детекцией, обработкой изображений и под-агентами. Почему блокер: Любое изменение в одной подсистеме требует правки одного файла. Параллельная работа нескольких разработчиков невозможна без конфликтов. Unit-тесты вынуждены инициализировать весь агент даже для проверки одного метода. Направление: Выделить PlanningOrchestrator, ContextCompressor, SubAgentRunner, AntiStallMonitor в отдельные сервисы. Agent должен остаться только координатором.
Решение 2026-05-16:
ContextCompressor → navi/core/compressor.pyAntiStallMonitor → navi/core/anti_stall.pySubAgentRunner → navi/core/subagent_runner.pyAgentTurnContext / StreamState → navi/core/agent_run_context.py_iter_stream_guarded → navi/core/stream_guard.pybuild_tool_list / load_user_enabled_tools → navi/core/tool_utils.pyrun() — тонкая обёртка вокруг run_stream()run_stream() делегирует _compression_events_preturn, _compression_events_midturn, _consume_stream, _execute_tools_with_sinkrun_ephemeral() делегирует SubAgentRunner.run()navi/api/deps.pySeverity: Critical Файл: navi/api/deps.py (строки 46–170) Проблема: _memory_store, _registries, _mcp_manager, _scheduler, _kv_store, _session_store, _workers — создаются при первом обращении и живут до перезапуска процесса. Нет явного lifecycle management (shutdown pools, close connections). Почему блокер: Невозможно подменить реализацию в тестах без monkeypatch на уровне модуля. Горизонтальное масштабирование (несколько процессов) невозможно, потому что состояние привязано к процессу. Направление: Заменить на явный AppContainer или asynccontextmanager-зависимости FastAPI с yield.
Severity: High Файл: navi/api/websocket.py (443 строки) Проблема: WebSocket handler занимается: heartbeat, replay/reconnect, оркестрацией запуска агента, аутентификацией, валидацией изображений, управлением глобальным состоянием сессий (_runs, _busy_sessions, _session_sockets), созданием Agent напрямую. Почему блокер: При добавлении нового транспорта (SSE, gRPC) придётся дублировать всю оркестрацию. Бизнес-логика просочилась в слой сериализации. Направление: Ввести AgentSessionOrchestrator между WebSocket и Agent. WebSocket должен заниматься только сериализацией/десериализацией.
settingsSeverity: High Файл: navi/config.py Проблема: settings = Settings() доступен из любого модуля. Поля не readonly. extra="ignore" означает, что опечатки в .env игнорируются без предупреждения. Почему блокер: Любой модуль может изменить settings.ollama_num_ctx во время выполнения, что приведёт к race condition при параллельных запросах. Направление: Сделать Settings immutable (frozen=True). Передавать экземпляр в конструкторы вместо глобального импорта.
Решение 2026-05-18:
frozen=True добавлено в SettingsConfigDict в navi/config.pymodel_validator(mode="after") конвертирован в mode="before" для загрузки navi_persona_file, т.к. frozen-инстанс нельзя мутировать после созданияsettings.field = value, переведены на замену целого объекта: monkeypatch.setattr(module, "settings", Settings(...))session_dir / ensure_session_dir из navi.session_files, настройки заменяются согласованно в обоих модуляхSeverity: High Файлы: navi/memory/store.py, navi/store/__init__.py, navi/core/pg_session_store.py Проблема: Каждый стор создаёт свой asyncpg.create_pool(self._dsn) с одинаковым DSN. У каждого своя asyncio.Lock для lazy-инициализации. Почему блокер: При росте нагрузки количество соединений к PostgreSQL утраивается. Нет единого менеджера пула. Направление: Вынести asyncpg.Pool в отдельный Database сервис. Передавать pool конструктором в сторы.
Решение 2026-05-18:
navi/db.py::Database — единый менеджер пула с lazy-инициализациейKvStore, PgSessionStore, MemoryStore, RecallScheduler теперь принимают pool в __init__ вместо dsnAppContainer хранит database: Database, shutdown() закрывает один пулcreate_container() создаёт один пул и передаёт его всем сторам_initialized + _lock) сохранён для изоляции схемregistry.pySeverity: High Файл: navi/core/registry.py (строки 164–254) Проблема: build_default_registries() создаёт все реестры, а затем вручную прописывает кросс-ссылки (list_tool._profile_registry = profiles, spawn_tool._backend_registry = backends). Почему блокер: Циклические зависимости разрешаются через "патч после создания". Добавление нового инструмента требует редактирования фабрики. Направление: Внедрить двухфазную инициализацию: create() → wire().
Решение 2026-05-18:
build_default_registries: сначала создаются backends, profiles, cp_registry (нет зависимостей на tools), затем создаются ВСЕ инструменты с полными зависимостямиNone и последующего патчинга: ListToolsTool(registry=tools, profile_registry=profiles, mcp_manager=mcp_manager) SpawnAgentTool(backend_registry=backends, ...) ReloadToolsTool(registry=tools, cp_registry=cp_registry, mcp_manager=mcp_manager)_profile_registry = profiles, _backend_registry = backends, _cp_registry = cp_registrymcp_manager в build_default_registries (раньше передавался через патч в create_container())builtins — никакого патчинга не требуетсяtool_executor.pySeverity: Medium Файл: navi/core/tool_executor.py (строки 60–187) Проблема: Три метода (_run_single_tool, _execute_tool_calls, _execute_tool_calls_streaming) содержат идентичную логику: resolve → middleware → execute → image extraction → build message. Почему блокер: Любой баг в middleware или image-обработке нужно править в трёх местах. Направление: Единый метод _execute_one(tc, tool_map) -> (event, msg, image_msg), используемый всеми тремя путями.
Решение 2026-05-18:
_execute_one(tc, tool_map) — единственный канонический путь resolve → middleware → execute → image extraction → build message_run_single_tool, _execute_tool_calls, _execute_tool_calls_streaming делегируют _execute_oneSeverity: Medium Файл: navi/tools/_internal/base.py (строки 19–42) Проблема: 7 глобальных ContextVar (current_session_id, current_event_sink, current_stop_event, current_model, current_user_id, current_user_role, current_user_info). Инструменты читают их неявно. Почему блокер: Инструмент нельзя вызвать вне контекста агента (из CLI, фоновой задачи, теста) без установки всех ContextVar. Направление: Передавать контекст выполнения явным параметром в execute(). ContextVar оставить как optional fallback.
Решение 2026-05-24:
ToolContext dataclass в navi/tools/_internal/base.py — явный контейнер для всех 7 значенийTool.execute() теперь принимает ctx: ToolContext | None = NoneToolExecutor._execute_one() собирает ToolContext и передаёт его инструментуAgent._execute_tools_with_sink() и SubAgentRunner строят ToolContext из значений в scope и передают в цепочкуctx, остальные получили только новый параметрctx=ToolContext(...) — больше никаких фикстур с current_session_id.set()ai_helper.py, context_builder.py, planning.py)Severity: Medium Файл: navi/api/websocket.py (строки 80–86, 403–406) Проблема: _runs, _busy_sessions, _session_sockets — глобальные mutable dict без явной синхронизации. При горизонтальном масштабировании (несколько процессов) состояние запуска не реплицируется. Почему блокер: Сессия может быть запущена на инстансе A, а WebSocket подключён к инстансу B. Направление: Вынести состояние запуска в SessionStore (PostgreSQL) или Redis. _session_sockets заменить на pub/sub.
Severity: Medium Файлы: navi/mcp/manager.py, navi/mcp/client.py Проблема: resolve_group() и get_instructions() вызывают load_mcp_servers() при каждом обращении. Нет кэширования. McpClient делает reconnect без backoff. Почему блокер: При частых запросах — лишний дисковый I/O. Если MCP-сервер упал, каждый запрос порождает две попытки подключения без задержки. Направление: Закэшировать конфигурацию в McpManager. Добавить exponential backoff на reconnect в McpClient.
Решение 2026-05-18:
McpManager теперь хранит _configs: dict[str, McpServerConfig], заполняемый при load_all()resolve_group() и get_instructions() читают из кэша; fallback к диску если кэш пуст (тесты / первый вызов без load_all())reload_all() сбрасывает кэш (self._configs = None) и перечитывает конфигMcpClient получил exponential backoff: base 1s, max 30s, ±20% jitter_ensure_connected() блокирует reconnect в пределах backoff-окна; backoff сбрасывается при успешном подключении, удваивается при неудачеagent.py — Step 1 complete: ContextCompressor extracteddeps.py — replaced with AppContainer + lifespansettings — frozen Settings + mode="before" validatortool_executor.py — unified _execute_one, three methods delegate