Files
SDR-Rover/docs/handoff/RF001_LAUNCHER_HEALTH_DIAGNOSTIC.md
2026-09-30 11:42:19 +03:00

158 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RF001 launcher/health diagnostic
Дата проверки: 29 сентября 2026 года.
## Ограничения проверки
- Производственный RX-агент не останавливался и не изменялся.
- Задание Планировщика Windows не изменялось.
- Диагностический процесс не обращался к HTTP-очереди.
- Доступ к SDR не выполнялся: `hardware_accessed=false`, `RF RX=0`, `RF TX=0`.
- Bootstrap `rf001-bootstrap-2026.09.29.5` не создавался; `.4` повторно не запускался.
- Clean release A `rf001-agent-2026.09.29.1` использовался только для чтения кода health writer и проверки `READY.json`/manifest.
## Безопасное состояние после проверки
- Активный производственный агент: `2026.09.25.1`; heartbeat на MAIN свежий, active task отсутствует.
- Незавершённых задач: 0. Незавершённых аппаратных задач: 0.
- Задание `SDR Rover RX Agent`: `Running`; его действие по-прежнему запускает legacy agent, а не release A.
- Производственная цепочка legacy agent на RX: venv wrapper PID 7980 -> базовый Python PID 17568.
- `hackrf_transfer.exe`: 0 процессов. Процессов RX capture: 0.
- Диагностических процессов: 0.
- Clean release A: 10 файлов из 10 совпадают с `versions/rf001-agent-2026.09.29.1` внутри подписанного bootstrap `.4` по относительному пути, размеру и SHA256.
- Conflict quarantine сохранён. SHA256 его diagnostic manifest: `b92f15dd4b7cffef375fd108f454a41623ed7328220c73c3f2b847dc59e956bc`.
- `rf001-bootstrap-2026.09.29.5.zip` отсутствует.
## Доказанная первопричина
Два последовательных изолированных запуска через настоящий `start_release()` дали одинаковый результат. Поля `release_id`, `agent_version`, `launch_nonce` и `state` совпали. Единственным несовпавшим полем стало `health.pid == Popen.pid`.
На проверенном Windows/venv способе запуска `Popen.pid` принадлежит процессу `venv\\Scripts\\pythonw.exe`, а фактический интерпретатор, который исполняет health writer, является его единственным прямым дочерним процессом с образом базового `Python313\\python.exe`. Связь установлена не по именам: сверены PID, PPID, время создания, полный путь образа, командная строка и одноразовый `launch_nonce`.
### Запуск 1
- Launcher процесса диагностики: PID 7848.
- `Popen.pid`: 10284, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:44.696889Z`.
- Health writer: PID 8452, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 10284, создан `2026-09-29T13:04:44.702070Z`.
- Health обновлён `2026-09-29T16:04:44.866156+03:00`.
- Wrapper уже наблюдался 0,167845 с и отсутствовал к завершению прогона через 2,589228 с после создания.
- Agent child уже наблюдался 0,164086 с и отсутствовал к завершению прогона через 2,585469 с после создания.
- Точный момент завершения каждого процесса не опрашивался, поэтому приведены проверенные нижняя и верхняя границы, а не предполагаемая длительность.
### Запуск 2
- Launcher процесса диагностики: PID 7848.
- `Popen.pid`: 3504, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:47.290751Z`.
- Health writer: PID 14616, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 3504, создан `2026-09-29T13:04:47.295668Z`.
- Health обновлён `2026-09-29T16:04:47.461326+03:00`.
- Wrapper уже наблюдался 0,169277 с и отсутствовал к завершению прогона через 2,574777 с после создания.
- Agent child уже наблюдался 0,165658 с и отсутствовал к завершению прогона через 2,571158 с после создания.
- Точный момент завершения каждого процесса не опрашивался.
Оба `launch_nonce` различались:
- `96acc3af17150f42d393238bcbdad3ff`;
- `e4a618547e4bcda4b6914bf81bbd7963`.
В обоих случаях штатный `start_release()` завершился fail-closed:
```text
RuntimeError: health file does not match the launched process
```
После каждого прогона процессов, принадлежащих диагностическому запуску, не осталось. Исходные health-файлы и снимки процессов сохранены на RX в:
```text
C:\Users\Professional\Downloads\SDR_Rover_Diagnostics\launcher_health_20260929T130000Z\evidence
```
## Сравнение полей health
| Поле | Запуск 1 | Запуск 2 |
|---|---:|---:|
| `release_id == rf001-agent-2026.09.29.1` | PASS | PASS |
| `agent_version == 2026.09.29.1` | PASS | PASS |
| `launch_nonce` | PASS | PASS |
| `state == RUNNING` | PASS | PASS |
| `health.pid == Popen.pid` | FAIL: 8452 != 10284 | FAIL: 14616 != 3504 |
Следовательно, `Popen.pid` соответствует варианту C: это родитель фактического процесса агента. Он не является произвольным или посторонним процессом; это venv-обёртка с ожидаемой командной строкой, которая существует одновременно с прямым дочерним health writer.
## Строгая модель идентичности
Основным вариантом по-прежнему должен быть запуск, при котором `Popen.pid` принадлежит фактическому долгоживущему агенту. Для проверенной Windows/venv топологии, где это условие не выполняется, допустима только следующая строгая альтернатива:
1. Сохранить точные проверки `release_id`, `agent_version`, одноразового `launch_nonce` и `state=RUNNING`.
2. Снять идентичность процесса `Popen` сразу после запуска: PID, путь образа, командную строку и время создания.
3. Если health PID отличается, принять только один живой прямой дочерний процесс этого конкретного wrapper.
4. Для дочернего процесса проверить время создания после начала данного запуска, точную команду с тем же agent entrypoint и nonce, ожидаемый путь интерпретатора и наличие процесса в момент принятия health.
5. Не принимать внука, произвольного потомка, самый новый `python.exe`, совпадение только по имени или один числовой PID.
6. При завершении или откате повторно сверить PID и время создания перед остановкой wrapper и строго связанных с ним потомков. Это закрывает повторное использование PID.
Минимальное производственное исправление можно ограничить `rf001_agent_launcher.py`: добавить проверку уникального прямого потомка и такую же строгую очистку потомков при ошибке. Формат health менять не требуется, поскольку в нём уже есть nonce и собственный PID. Если получение надёжного времени создания и пути образа без дополнительного компонента окажется невозможно, health writer следует расширить полями process identity; отключать проверку PID нельзя.
## Конечный автомат launcher
Названия ниже описывают фактические фазы кода. Не все они сейчас записываются как отдельное значение в `launcher_status.json`.
| Фаза | Допустимый вход | Процессы | Canonical release / active pointer | Scheduled Task | Health | Действие при ошибке |
|---|---|---|---|---|---|---|
| `LEGACY_ACTIVE` | Действующий агент здоров и idle | Legacy agent запущен | Указатель на legacy | Launcher не меняет определение задания | Legacy `RUNNING` | Запрос обновления принимается только при `safe_to_switch=true`, совпадающем `from_release_id` и idle health |
| `TARGET_VERIFIED` | Подписанный target прошёл `validate_ready_release()` | Новый процесс ещё не создан | Target уже существует в canonical releases; указатель ещё legacy | Без изменений | Legacy health | Отказ до остановки legacy |
| `STARTING_TARGET` | Target проверен | Legacy останавливается; создаётся wrapper target | Указатель атомарно меняется на target; status `STARTING_UPDATE` | Без изменений | Старый health больше не считается подтверждением | Переход в `TARGET_FAILED` |
| `WAITING_FOR_HEALTH` | `Popen` создан, nonce уникален | Wrapper и возможный прямой child | Указатель target | Без изменений | Ожидаются свежие точные поля и строгая process identity | Timeout, exit или mismatch завершают target fail-closed |
| `TARGET_HEALTHY` | Все health-поля и identity прошли | Target продолжает работу | Указатель target; status `RUNNING` | Без изменений | Target `RUNNING` | Дальнейший выход ведёт в `RESTARTING` |
| `TARGET_FAILED` | Startup exit, timeout или mismatch | Target должен быть остановлен без orphan | Target ещё был активным указателем | Без изменений | Ошибочный health не принимается | Начать rollback |
| `ROLLING_BACK` | Target не прошёл health gate | Target очищается, запускается legacy | Указатель возвращается на legacy | Без изменений | Для legacy требуется новый nonce и новый health | Ошибка rollback должна оставаться явной, а не считаться успехом |
| `LEGACY_RESTORED` | Legacy снова прошёл health gate | Legacy работает | Указатель legacy; status `ROLLED_BACK` | Без изменений | Свежий legacy `RUNNING` | Архивировать запрос и продолжить legacy |
Существенный пробел текущего production-кода: `TARGET_FAILED` останавливает только объект `Popen`; общая строгая очистка потомка в launcher пока не реализована. В двух диагностических запусках orphan не осталось, но это наблюдение не заменяет будущую явную очистку по identity.
## Проверенные гонки и регрессии
Локальный набор `pytest` проверяет:
- точное совпадение процесса: PASS;
- неверные nonce, release, version и state: FAIL;
- stale health: FAIL;
- посторонний PID: FAIL;
- единственный ожидаемый прямой child: PASS только со строгой lineage, nonce, командой и временем создания;
- неожиданный grandchild: FAIL;
- повторное использование PID с другим creation time: FAIL;
- target умер до health: FAIL;
- target записал health, но умер: FAIL;
- health в состоянии `STARTING`, до полной готовности: FAIL;
- rollback не принимает health с nonce прошлого запуска: FAIL;
- выбор процессов для cleanup не затрагивает посторонний процесс;
- cleanup останавливает только строго принадлежащие wrapper и child;
- два успешных логических запуска детерминированы и имеют разные nonce;
- health JSON заменяется атомарно.
Команда проверки:
```text
python -m pytest tests/test_rf001_launcher_health_diagnostic.py tests/test_rf001_agent_launcher.py tests/test_rf001_rx_http_agent.py tests/test_rf001_agent_update.py -q
```
Результат: `48 passed`, предупреждение только о недоступности `.pytest_cache` в текущей песочнице.
Windows integration test использовал реальный RX venv, установленный launcher source, canonical release A и настоящий agent health writer. Выполнено два последовательных изолированных запуска. HTTP polling, SDR и аппаратные команды в этом режиме отсутствовали.
## Файлы и следующий этап
В этой диагностической фазе добавлены только:
- `tools/rf001_launcher_health_diagnostic.py`;
- `tests/test_rf001_launcher_health_diagnostic.py`;
- этот отчёт.
Для будущего производственного исправления потребуются как минимум:
- `tools/rf001_agent_launcher.py`;
- `tests/test_rf001_agent_launcher.py`;
- тесты упаковки, подтверждающие неизменность подписанного состава и fail-closed rollback.
`tools/rf001_rx_http_agent.py` потребуется менять только при выборе расширенного health process identity; доказанная минимальная схема может обойтись без изменения агента.
После отдельного согласования исправления понадобится новый уникальный bootstrap, поскольку launcher на RX должен быть заменён проверенным пакетом. Bootstrap `.5` в рамках этой фазы не создавался.