12 KiB
12 KiB
Context
Хаб main 8229bba (web-app и agent-v9-hardening слиты). Issue aep/jira-hub#32. Источники: #25 (M-1…M-3, M-5…M-7),
#16 (T3.1, T3.2, T5.2, M-8, ре-ревью minor-1, minor-3, O-9, O-10, O-11), #4 (M-9/O-4, M-10). Сверка с кодом — 15.10.2026.
Найдено при чтении кода:
release._write_atomic—tmp.write_bytes+os.replace, безfsyncи без очистки.tmpпри ошибке;publishпишетagent.py, затем манифест.cliловит толькоPublishError. Если манифест не записался, новыйagent.pyне сходится со старым манифестом по sha →load→None→ 404.release.current:int(m["version"])(6.9 → 6,true→ 1),str(m["sha256"]);log.warningна каждый вызов (каждыйclaimиGET /agent).release.load:data.decode("utf-8")после сверки sha,UnicodeDecodeErrorне ловится → 500.Hub.agent_update = {"update_state": None, "launcher_version": None}— в памяти; после перезапуска хаба до первогоclaimlauncher_version = Noneнеотличимо от «агент без запускателя» (release.hint,mcp_server.fmt_agent:115).h_settings_putотвечает{"settings_version": version}.queue._next: при неразрешённом временном ключе —skipped«задача new:N не создана», независимо от того,failedилиcancelledкоманда-создатель (_definer_status).commands.check_confirmed:(issue.get(field) or None) != (p[field] or None)—\r\nв зеркале против\nв параметре даёт «не совпало».relay._cleaned: set[Path], проверкаself.root not in self._cleaned— по написанию пути.- Тесты: 249 passed; 87
ResourceWarning: unclosed database(test_queue, test_relay, test_commands, test_importer).
Goals / Non-Goals
Goals: публикация не оставляет хаб без годной версии при сбое записи и не роняет команду трейсбеком; статус агента
не утверждает неизвестное; подтверждение по состоянию работает на задачах с CRLF; ответы и тексты называют то, что
произошло; тесты закрывают найденные пробелы и чисты от ResourceWarning.
Non-Goals: см. proposal.
Decisions
Публикация агента
- Запись файла (
_write_atomic): временный файл.<имя>.tmpв том же каталоге,write+flush+os.fsync,os.replace; приOSErrorвременный файл удаляется (ошибки удаления игнорируются), исключение пробрасывается. - Порядок публикации:
agent.py→agent.py.prev(os.replace, еслиagent.pyесть); новый файл через_write_atomicвagent.py; манифест через_write_atomic.OSErrorпосле переименования —agent.py.prevвозвращается на местоagent.py(os.replace; если и это не удалось — в лог), иPublishError("не удалось записать публикацию: <ошибка>"). После успешной записи манифестаagent.py.prevудаляется. Итог: при любом сбое опубликована прежняя версия (манифест иagent.pyсогласованы),release/без временных файлов. cliпечатает текстPublishErrorв stderr и возвращает 1 — как сейчас; новых веток нет,OSErrorдоcliне доходит.- Годный манифест (
current): объект;version—intи неbool, ≥ 1;sha256— строка,re.fullmatch(r"[0-9a-f]{64}"). Иначе — «публикации нет». Совпадает с проверкой агента v9 (design agent-v9-hardening 10). - Предупреждение о битом манифесте — один раз на каждый новый текст ошибки: модульная переменная с последним текстом; успешное чтение или отсутствие манифеста её сбрасывает. Отсутствие манифеста — не ошибка, без лога.
load:UnicodeDecodeError→log.error«agent.py не в UTF-8 — публикации нет» иNone(404no_release).
Статус агента
agent_reported:Hub.agent_updateполучает ключreported: False;note_agent_update(вызывается на каждомclaim) ставитTrue.GET /agentотдаётagent_reported.release.hint(...)получает признак и не выдаёт «агент запущен без run.py…», покаagent_reported = false; подсказки об откате и отказе — поupdate_stateкак сейчас (до первогоclaimонnull).- MCP
agent_status: строка запускателя — «запускатель: v», «без запускателя» (агент сообщилnull) или «запускатель: неизвестно (агент ещё не выходил на связь после перезапуска хаба)» (agent_reported = false). Если поляagent_reportedв ответе нет (старый хаб) — прежнее поведение.
Настройки, очередь, подтверждение, реле
PUT /settingsотвечаетsettings.get_all(conn)(токены скрыты) после записи — те же поля, чтоGET /settings, включаяsettings_version. Клиенты, читающиеsettings_versionиз ответа (client,mcp_server.settings_set, web), не меняются.- Пропуск зависимой команды: если среди неразрешённых ключей есть ключ, чья
ensure_issueвcancelled, текст — «создание задачи new:N отменено» (по первому такому ключу); иначе прежний «задача new:N не создана». - Сравнение строк в
check_confirmed: для полейensure_fieldsсо строковыми значениями сравнение после замены\r\nна\nс обеих сторон;Noneи пустая строка по-прежнему равны. Метки и прочие поля — без изменений. - Реле:
_cleanedхранитPath.resolve(); проверка — поself.root.resolve(). Ошибкаresolve(папки нет) — сравнение по исходному пути, как сейчас.
Тесты и чистота
- Новые тесты: bool в строковом параметре и
[]в списочном (test_commands);sync_run.finished_atне ставится приfailedне-refresh по пределу выдач (test_queue); 501 наDELETE/PATCH(test_api);epics.csvбез «Ключ» (test_importer); «подготовлена заново» вtest_vanished_tree_keeps_work(test_relay); предупреждение «обновите агента» приagent_version < min_agent_version, офлайн-агент (нет контакта дольше порога) вGET /agent,tick— проверка эффекта (test_api/test_queue); публикация — сбой записи манифеста возвращает прежнийagent.py,.tmpнет; битый манифест (6.9,true, короткий sha) — «публикации нет», предупреждение один раз; не-UTF-8 → 404;agent_reportedдо и послеclaim;PUT /settingsотдаётrelay_dir; текст «отменено»; CRLF-подтверждение; реле с симлинком на ту же папку. ResourceWarning: фикстуры и хелперы тестов закрывают соединения (conn.close()/contextlib.closing); критерий —uv run pytest -W error::ResourceWarningбез предупрежденийunclosed database(pytestfilterwarnings = error::ResourceWarningвpyproject.toml, если не ломает сторонние пакеты; иначе — проверка в плане командой).- T3.1:
_type_errorпереписывается без тяжёлой строки сandи кортежем — отдельные ветки по типу.
Risks / Trade-offs
- [
agent.py.prevвrelease/] лишний файл живёт только между переименованием и записью манифеста; при падении процесса посреди — остаётся; следующая публикация его перезапишет. → не влияет наload(читается толькоagent.py). - [«неизвестно» до первого claim] до 30 с после перезапуска хаба статус неполный. → честнее ложного «без запускателя».
- [
PUT /settingsотдаёт больше полей] клиенты, ожидавшие ровно{settings_version}, получают надмножество. → поле на месте. - [Конфликт с #29] текстовый в
tests/test_api.py(одна строка у #29). → кто сливается вторым, разрешает.
Decisions made without discussion
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| R-1 (design #7) | 2026-10-15 | Признак agent_reported в памяти, а не launcher_version в БД |
без записи на каждый claim; значение из БД после перезапуска могло устареть | до 30 с «неизвестно» после перезапуска хаба |
| R-2 (design #2) | 2026-10-15 | Откат публикации через agent.py.prev (переименования, без копии) |
переименование не требует места на диске — откат работает и при полном диске | лишний файл в release/ при падении процесса посреди публикации |
| R-3 (design #9) | 2026-10-15 | PUT /settings отвечает всеми настройками, а не {settings_version, relay_dir} |
один формат с GET /settings; O-11 — «не видно, что записалось» относится к любой настройке |
надмножество полей в ответе |
| R-4 (design #10) | 2026-10-15 | «Отменено» — только при cancelled создателе; при failed текст прежний |
O-10 описывает только force-отмену; failed — задача действительно не создана |
нет |
| R-5 (design #14) | 2026-10-15 | Цель — ноль ResourceWarning от sqlite в тестах, проверка -W error::ResourceWarning |
M-9/O-4: 87 предупреждений прячут настоящие утечки | время на правку фикстур |