docs(agents): A60 — причина разрыва установлена живым прогоном

Поставил v0.1.2-b7 в изолированную песочницу (свои HOME и HERMES_HOME, живая
установка не тронута) и запустил обновление на v0.1.3-b1. Хаб умер через шесть
секунд и за 150 секунд не поднялся. Установка при этом не произошла вовсе:
манифест и код остались на 0.1.2.

В задании было написано, что установщик доживает сиротой и файлы обновляет.
Прогон это опроверг. Настоящая цепочка: хаб зовёт установщик через
capture_output=True, то есть читателем его вывода становится сам хаб; установщик
на шаге [0/6] снимает хаб; у трубы не остаётся читателя; следующий echo даёт
SIGPIPE, и установщик умирает на шаге [1/6], не поставив ничего. Проверено
контрольным опытом: тот же скрипт с выводом в файл доходит до конца, с трубой
умирает. Отсюда следует, что перестановкой schedule_restart делу не помочь.

Тем же прогоном найдено второе: deployed_at пишется в момент установки, а не
сборки, и сравнивается с датой публикации релиза. Хаб на b7 при живом b1 ответил
«установлена сборка новее опубликованного релиза» и обновление не предложил.
Переустановил старую сборку — выпал из обновлений молча. Вынесено в P0-5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
This commit is contained in:
ochenstarik-ui 2026-09-02 19:41:20 +07:00
parent c0485a3721
commit c6981921da

View file

@ -18,7 +18,7 @@ git checkout -b antigravity/a60-update-completes origin/main
## Порядок исполнения ## Порядок исполнения
Два прохода: **Flash** реализует, **Pro** проводит аудит. Пункт **P0-5** написан Два прохода: **Flash** реализует, **Pro** проводит аудит. Пункт **P0-6** написан
для аудитора. для аудитора.
Зона: применение обновления и перезапуск. Показа и загрузки не касается — там всё Зона: применение обновления и перезапуск. Показа и загрузки не касается — там всё
@ -63,20 +63,35 @@ subprocess.run(["bash", installer], timeout=600) ЖДЁТ здесь
schedule_restart() сюда управление не доходит schedule_restart() сюда управление не доходит
``` ```
**Linux.** `installer/install-linux.sh:53` в своём `stop_running_hub` берёт **Linux — проверено исполнением, цепочка целиком.**
`pgrep -u $(id -u) -f "antigravity_provider.router.web|hermes_hub_web_entry"` и не
исключает никого. Под шаблон попадает тот самый питон, который висит на
`subprocess.run`. Установщик снимает его, `schedule_restart` не выполняется
никогда, окно прогресса умирает вместе с процессом. Скрипт при этом доживает
сиротой и файлы обновляет — поэтому со стороны выглядит как «поставилось, но не
запустилось».
**Windows.** `installer/HermesHubSetup.cs:280` (`StopOwnedRuntime`) выглядит ```
аккуратнее: строит `$protected` — цепочку собственных предков, чтобы не снять 1. хаб: subprocess.run(["bash", installer], capture_output=True)
stdout установщика — труба, единственный читатель которой сам хаб
2. install-linux.sh, шаг [0/6]: stop_running_hub снимает хаб
pgrep -u $(id -u) -f "antigravity_provider.router.web|hermes_hub_web_entry"
никого не исключает, под шаблон попадает тот, кто запустил установщик
3. хаб мёртв -> у трубы не осталось читателя
4. следующий echo установщика -> SIGPIPE -> установщик умирает на шаге [1/6]
5. не установлено ничего; перезапускать нечего
```
**Установщик не доживает до конца — он умирает раньше, чем что-либо поставит.**
Это не «поставилось, но не запустилось»: манифест и код остаются на прежней
сборке. Проверено контрольным опытом — тот же скрипт, та же смерть родителя: с
выводом в файл доходит до конца, с `capture_output=True` умирает.
Отсюда следует, что одной перестановкой `schedule_restart` делу не помочь: пока
установщик пишет в трубу убитого им процесса, он не доживёт до установки при
любом порядке вызовов.
**Windows — прочитано по коду, живьём не проверялось.**
`installer/HermesHubSetup.cs:280` (`StopOwnedRuntime`) выглядит аккуратнее: строит `$protected` — цепочку собственных предков, чтобы не снять
того, кто его запустил. Но проверка `-notin $protected` стоит **только на того, кто его запустил. Но проверка `-notin $protected` стоит **только на
дочерних** процессах внутри `Stop-HubBranch`. Сам процесс-цель снимается дочерних** процессах внутри `Stop-HubBranch`. Сам процесс-цель снимается
безусловно, а хаб под шаблон `antigravity_provider\.router\.web` подходит. безусловно, а хаб под шаблон `antigravity_provider\.router\.web` подходит.
Прочитано по коду — **подтвердить исполнением, а не поверить на слово.** Труба там та же: `proc.wait(timeout=600)` при `Popen` без перенаправления вывода.
**Подтвердить исполнением, а не поверить на слово.**
Отката на путях установщика нет вообще: он есть только для `.zip` Отката на путях установщика нет вообще: он есть только для `.zip`
(`apply_update_sync`). Ни `.sh`, ни `.exe` при срыве на середине ничего не (`apply_update_sync`). Ни `.sh`, ни `.exe` при срыве на середине ничего не
@ -96,9 +111,13 @@ schedule_restart() сюда управлени
**выходит сам**, освободив порт. **выходит сам**, освободив порт.
2. **Помощник**: дожидается освобождения порта → запускает установщик → поднимает 2. **Помощник**: дожидается освобождения порта → запускает установщик → поднимает
хаб → завершается. хаб → завершается.
3. **Помощник не наследует** ни stdout хаба, ни его рабочий каталог: хаб исчезнет 3. **Ни одна труба помощника не должна вести в хаб.** Это то самое место, где всё
ломается сейчас: `capture_output=True` делает читателем вывода тот процесс,
который установщик собирается снять. Вывод установщика — сразу в файл, а не в
`PIPE`, и не через процесс, которому предстоит умереть.
4. **Помощник не наследует** ни stdout хаба, ни его рабочий каталог: хаб исчезнет
раньше, чем помощник закончит. раньше, чем помощник закончит.
4. **Помощник пишет свой ход в файл** `~/.hermes/updates/apply-<время>.log`, чтобы 5. **Помощник пишет свой ход в файл** `~/.hermes/updates/apply-<время>.log`, чтобы
после неудачи было что показать. Пустой отказ без причины — уже было в A59, после неудачи было что показать. Пустой отказ без причины — уже было в A59,
второй раз не проходит. второй раз не проходит.
@ -138,7 +157,28 @@ schedule_restart() сюда управлени
что хаб жив. что хаб жив.
6. **Проверить, что старый процесс не остался** и порт занят новым. 6. **Проверить, что старый процесс не остался** и порт занят новым.
## P0-5. Аудит вторым проходом ## P0-5. Обновление вообще предлагается
Найдено тем же прогоном, до того как дело дошло до установки.
Хаб на `v0.1.2-b7` при живом релизе `v0.1.3-b1` ответил:
`«Установлена сборка новее опубликованного релиза (4fa9939 от 2026-09-02)»`,
`update_available: false`. Обновиться было нельзя вообще.
Причина: `deployed_at` в `deployment_manifest.json` пишется установщиком в момент
**установки**, а не сборки, и сравнивается с `published_at` релиза. Поставил
старую сборку сегодня — она «новее» любого релиза, и обновление не предложат
больше никогда. Владелец, переставивший сборку руками, выпадает из обновлений
молча.
1. **Сравнивать сборки, а не дату установки.** Дата установки не говорит о том,
какой код внутри.
2. **Если сравнить нечем — предложить обновление, а не промолчать.** Молчание
здесь дороже лишнего окна: владелец не узнает, что отстал.
3. **Причину решения показывать.** «Установлена сборка новее релиза» — вывод, а
не факт; рядом должно стоять, из чего он сделан.
## P0-6. Аудит вторым проходом
1. **Пройти обновление целиком на обеих системах.** 1. **Пройти обновление целиком на обеих системах.**
2. **Проверить, что помощник не снимает чужого** — только процессы хаба своего 2. **Проверить, что помощник не снимает чужого** — только процессы хаба своего
@ -173,8 +213,10 @@ schedule_restart() сюда управлени
4. Сорвавшаяся установка откатывается, хаб остаётся работоспособным. 4. Сорвавшаяся установка откатывается, хаб остаётся работоспособным.
5. Старый процесс не остался, порт занят новым. 5. Старый процесс не остался, порт занят новым.
6. Журнал помощника пишется, и при отказе на него указывают. 6. Журнал помощника пишется, и при отказе на него указывают.
7. `ruff check .` чисто; релизный гейт пройден; тестов не меньше **740**. 7. Обновление предлагается по сравнению сборок, а не по дате установки;
8. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, переустановка старой сборки не выключает обновления навсегда.
8. `ruff check .` чисто; релизный гейт пройден; тестов не меньше **740**.
9. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`,
`X passed / Y skipped / Z failed`, тайминги, скриншоты. `X passed / Y skipped / Z failed`, тайминги, скриншоты.
## Главное ## Главное