# План правок по отчётам Опуса > 31 мая 2026 · ветка `opus-fixes` · порядок: по критичности + зависимостям --- ## Этап 1. Сервер (`elmer/`) — 4 правки ### 1.1 🔴 `api/db.py` — WAL + закрытие соединений + request_id **Файл**: `api/db.py` **Строки**: класс `Database`, методы `_init_schema()`, `save_session()` - [x] Добавить `PRAGMA journal_mode=WAL` и `busy_timeout=30000` - [x] Добавить колонку `request_id TEXT UNIQUE` в `sessions` - [x] Метод `close()` и контекстный менеджер (`__enter__`/`__exit__`) - [x] `save_session()` — проверять `request_id` на дубликат, возвращать кэшированный диагноз - [x] Индекс `idx_sessions_request_id` ### 1.2 🔴 `api/routes.py` — идемпотентность upload + /ping-llm без LLM **Файл**: `api/routes.py` **Строки**: `upload_session()`, `ping_llm()` - [x] `upload_session()` — принимать `request_id` из JSON, возвращать кэш при дубликате - [x] `upload_session()` — закрывать `db` через контекстный менеджер - [x] `upload_session()` — не отдавать `str(e)` наружу, логировать, клиенту — обобщённый текст - [x] `/ping-llm` — кэшировать результат на 60с, не вызывать LLM на каждый GET ### 1.3 🔴 `obd/protocol.py` — сброс буфера + не затирать ERROR **Файл**: `obd/protocol.py` **Строки**: `_write()`, `send()` - [x] `_write()` — `self._ser.reset_input_buffer()` перед записью - [x] `send()` — `if self._state == State.BUSY: self._state = State.READY` (не безусловно) ### 1.4 🟡 `brain/client.py` — таймаут из конфига + модель **Файл**: `brain/client.py` **Строки**: `Diagnoser.__init__()`, `Diagnoser.ask()` - [x] `DEFAULT_MODEL` → `"gpt-oss-120b"` - [x] `timeout` — параметр конструктора (по умолчанию 180) - [x] Комментарии: убрать «DeepSeek» --- ## Этап 2. Сервер (`elmer/`) — улучшения (без 🔴 но важные) ### 2.1 🟡 `api/routes.py` — импорты наверх + кэш конфига **Файл**: `api/routes.py`, `api/config.py` - [x] Поднять импорты (`from brain.client import Diagnoser` и др.) на уровень модуля - [x] `api/config.py` — `@lru_cache(maxsize=1)` на `load()` ### 2.2 🟡 `api/routes.py` — история /chat через роли **Файл**: `api/routes.py` **Строки**: `chat()` - [x] Передавать историю как массив `messages` с ролями, а не строкой «Водитель:/Автоэксперт:» ### 2.3 🟡 `brain/client.py` — обработка ошибок LLM **Файл**: `brain/client.py` - [x] Различать `Timeout`, `HTTPError(429)`, `HTTPError(5xx)`, `HTTPError(4xx)` - [x] Не отдавать детали исключения наружу --- ## Этап 3. Android (`elmer-android/`) — 6 правок ### 3.1 🔴 `ServerClient.kt` — request_id + идемпотентность **Файл**: `app/src/main/java/ru/elmer/client/server/ServerClient.kt` **Строки**: `uploadSession()` - [ ] Генерировать `UUID` один раз до цикла ретраев - [ ] Добавить `"request_id"` в JSON-тело - [ ] Добавить заголовок `Idempotency-Key` ### 3.2 🔴 `ServerClient.kt` + `build.gradle.kts` — X-Api-Key **Файлы**: `ServerClient.kt`, `app/build.gradle.kts` - [ ] `build.gradle.kts` — `buildConfigField("String", "API_KEY", ...)` - [ ] `ServerClient` — добавлять `X-Api-Key` во все запросы - [ ] `MainActivity.sendToLlm()` и `startTest()` — тоже `X-Api-Key` ### 3.3 🔴 `ElmProtocol.kt` — не затирать ERROR + дренаж буфера **Файл**: `app/src/main/java/ru/elmer/client/elm/ElmProtocol.kt` **Строки**: `sendCommand()`, `write()` - [ ] `sendCommand()` — `if (state == State.BUSY) state = State.READY` - [ ] `write()` — `while (input.available() > 0) input.read()` перед записью ### 3.4 🔴 `SessionDb.kt` — безопасная миграция + индекс **Файл**: `app/src/main/java/ru/elmer/client/db/SessionDb.kt` **Строки**: `onUpgrade()`, `onCreate()` - [ ] `onUpgrade()` — `ALTER TABLE` вместо `DROP TABLE` - [ ] Индекс `idx_resp_session ON responses(session_id)` ### 3.5 🔴 `MainActivity.kt` — /ping-llm без LLM + двойной receiver + chatHistory **Файл**: `app/src/main/java/ru/elmer/client/ui/MainActivity.kt` **Строки**: `startTest()`, `sendToLlm()`, receiver-регистрация - [ ] `startTest()` — троттлить `/ping-llm` (не чаще раза в 60с), предупреждать - [ ] Убрать дублирующий receiver `statusReceiver` (оставить `scriptStatusReceiver`) - [ ] `chatHistory` сохранять в `onSaveInstanceState` (JSON) ### 3.6 🔴 `ScriptRunnerService.kt` — null intent + мёртвый paused + try/finally **Файл**: `app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt` **Строки**: `onStartCommand()`, `executeScript()` - [ ] `onStartCommand()` — `if (intent == null) { stopSelf(); return START_NOT_STICKY }` - [ ] Убрать мёртвый флаг `paused` и `ACTION_RESUME` (или доделать паузу) - [ ] `executeScript()` — `try/finally` вокруг `progress.stop()` --- ## Этап 4. Android (`elmer-android/`) — улучшения ### 4.1 🟡 `ServerClient.kt` — exponential backoff **Файл**: `ServerClient.kt` - [ ] `(1 shl (attempt-1)) * 1000 + Random.nextLong(0, 500)` вместо фиксированных 2000 ### 4.2 🟡 `MainActivity.kt` — единый HTTP-клиент **Файл**: `MainActivity.kt` - [ ] `sendToLlm()` и `startTest()` перевести на OkHttp (через `ServerClient`) ### 4.3 🟡 `ScriptRunnerService.kt` — вынести хост в константу **Файл**: `ScriptRunnerService.kt`, `MainActivity.kt` - [ ] `obdai.ru` → `BuildConfig.SERVER_HOST` или константа --- ## Порядок выполнения ``` Этап 1 (сервер 🔴) → коммит Этап 2 (сервер 🟡) → коммит Этап 3 (Android 🔴) → коммит Этап 4 (Android 🟡) → коммит ``` После каждого этапа — проверка: `python run.py` (сервер), сборка APK (Android).