2026-10-13 22:30:25 +04:00

115 lines
16 KiB
Markdown
Raw 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.

## Context
Мост в работе с 13.10.2026: хаб на Mac mini (launchd, база `~/.local/share/jira-hub/hub.sqlite` с живыми данными:
193 задачи, 139 заметок, пакеты #1–#3), агент v2 на виртуалке, обмен через общую папку (реле в `serve`).
Как устроено сейчас (по коду `main` на 799b0e2 / d2cb529):
- `queue.claim` в одной транзакции: обновляет `agent_status`, при наличии `claimed` моложе 10 минут
(`_fresh_claimed_exists`, `STALE_MINUTES`) отдаёт пусто; иначе `_next` выбирает `queued` команду, ставит `claimed`.
`_next` обходит пакет, чей определитель `new:N` висит в `claimed` дольше 10 минут (R-13).
- `report_result` для терминального статуса возвращает команду без изменений (результат отбрасывается); статус `queued`
не проверяется.
- `reconcile` после отчёта `refresh` подтверждает `claimed` команды по зеркалу (`check_confirmed`); для `ensure_issue`
нужен `reporter` из настройки, которая никогда не задаётся, — не подтверждается никогда.
- Отчёт импорта: связь из `links.csv` «есть», только если в зеркале `source→target` того же типа.
- Агент: `JiraClient.last_call_at` (monotonic) только в памяти; `Runner.wait_pause` без `last_call_at` не ждёт;
`recover` ставит `cmd["pause_seconds"] = 0` и вызывает `execute`, который пишет `claimed` ещё раз. Агент приходит за
новой командой (`claim`) только когда своей незавершённой нет: каждый путь `execute` кончается подтверждённым отчётом
или остановкой процесса, `recover` выполняется до первого `claim`. Единственное место, где агент сам теряет выданную
команду, — `reload_settings_before` при недоступном хабе без остановки (команда не пишется в журнал, цикл идёт на
новый `claim`); довыдача закрывает и его.
- `db.migrate` только создаёт таблицы (`CREATE TABLE IF NOT EXISTS`) и пишет `schema_version = 1`; миграций по версии нет.
`db.connect` вызывается на каждый запрос API и при старте.
## Goals / Non-Goals
Goals: ни одна команда не застревает в `claimed` без пути восстановления, кроме явного предела выдач; созданная задача
не теряется при отмене; после перезапуска агента темп Jira соблюдается; живая база мигрирует без потери данных; агент v2
продолжает работать с новым хабом; отчёт импорта не даёт ложных `missing_links` для связей в обратную сторону.
Non-goals — см. proposal.md.
## Decisions
1. **Повторная выдача (решение владельца, вариант A).** Если при `claim` есть команды в `claimed`, хаб выдаёт самую
старую из них (по `claimed_at`, затем `id`) снова: тот же `id`, те же (уже подставленные) параметры, `pause_seconds` по
текущей политике темпа, новый `claimed_at`, `deliveries + 1`, `agent_status.current_command_id` = её `id`. Основание —
инвариант агента «новый `claim` только без незавершённой команды»: раз агент пришёл, выданную он не получил или
потерял. Все команды идемпотентны («проверь, потом сделай»), поэтому довыдача уже выполненной команды даёт `done` без
записи; для `ensure_issue` агент находит свою задачу поиском дубля (`reporter = currentUser()`, тема, 7 дней). Это не
повтор в смысле правила «хаб не повторяет команды сам»: новая команда не создаётся, решение о повторе после `failed`
по-прежнему за человеком. Резервная копия перед первой командой пакета при довыдаче не делается (уже сделана).
2. **Предел выдач — 3.** Если у самой старой `claimed` уже `deliveries >= 3`, хаб переводит её в `failed` с
`error = "агент не отчитался после 3 выдач"`, `finished_at = now`, снимает `current_command_id` и выбирает дальше по тем
же правилам (следующая `claimed` или `_next`). Защищает очередь от команды, на которой агент падает. Число 3 —
константа `MAX_DELIVERIES`, не настройка.
3. **Удаляются `_fresh_claimed_exists`, `_definer_stale`, `STALE_MINUTES` и обход R-13.** При довыдаче команда не может
«зависнуть» без агента: если агента нет, очередь всё равно стоит. `maybe_auto_refresh` вместо «свежей claimed»
проверяет «есть любая claimed». `agent_view.stuck` заменяется на `redelivered` — команды в `claimed` с
`deliveries > 1` (`id`, `type`, `claimed_at`, `deliveries`).
4. **R-11 закрывается довыдачей.** Подтверждение по состоянию остаётся (`reconcile` после `refresh`) для команд, которые
окажутся в `claimed` одновременно с выполненным `refresh` (две `claimed` после миграции живой базы, гонка двух `claim`),
но для `ensure_issue` оно не делается: `check_confirmed` для неё всегда «нет», параметр `reporter` и чтение настройки
`reporter` удаляются. Совпадение темы с задачей неизвестного автора — не доказательство.
5. **Миграция схемы v1 → v2.** `SCHEMA_VERSION = 2`; новая база создаётся сразу в v2 (`deliveries` в `CREATE TABLE`).
Для существующей базы `migrate` читает `setting.schema_version`; если 1 — копия файла базы через
`backup.make_backup(conn, <каталог базы>/backups, "schema-v1", keep=0)` (без ротации), затем в `transaction`: повторная
проверка версии (параллельный `connect` мог успеть), `ALTER TABLE command ADD COLUMN deliveries INTEGER NOT NULL
DEFAULT 0`, `UPDATE command SET deliveries = 1 WHERE status = 'claimed'`, `schema_version = 2`. Для `:memory:` копия не
делается. `<каталог базы>/backups` совпадает с `paths.backups_dir()` при штатном `JIRA_HUB_HOME`.
6. **Отчёты.** `report_result` по команде в `queued` — `NotClaimedError` (подкласс `ValueError` не годится: API уже
отдаёт 422 на `ValueError`), API отвечает 409 `not_claimed`. Агент на 409 уже считает команду закрытой
(`send_result`), правка агента не нужна. По команде в `cancelled` типа `ensure_issue` с `status = done` и
`result.key`, если `result` у неё ещё пуст: `result`, `jira_calls`, `rate_limited` записываются, `finished_at` и статус
не меняются, заметка `new:N` переименовывается на ключ. Для остальных терминальных статусов — без изменений, как сейчас.
7. **Дубль темы в пакете — 422.** `validate` пакета отвергает две и более `ensure_issue` с одинаковой `summary` после
`strip()`, перечисляя повторяющиеся темы. Регистр не нормализуется: поиск дубля агентом — точное сравнение.
8. **Отчёт импорта: `reversed_links`.** Связь из `links.csv`, которой нет как `source→target` того же типа, но есть
`target→source` того же типа, попадает в `reversed_links`, а не в `missing_links`. Симметричность типа хабу не нужна:
человек видит, что связь в Jira есть, и решает сам. CLI выводит новое поле в том же JSON.
9. **Агент: темп после перезапуска.** При старте `Runner` берёт максимальное `at` среди записей журнала (любого вида) и,
если оно не в будущем, ставит `JiraClient.last_call_at = monotonic() − (now − at)`, `last_call_iso = at`. Запись
`finished` пишется сразу после последнего вызова Jira команды, поэтому оценка не раньше настоящего вызова — ждём не
меньше нужного. Пустой журнал или нечитаемое `at` — `last_call_at` пуст, как сейчас. `recover` не обнуляет
`pause_seconds`: берёт значение из ответа `GET /agent/commands/{id}`.
10. **Агент: `claimed` при восстановлении не пишется повторно.** `execute(cmd, journal_claimed=True)`; `recover` вызывает с
`False` — запись `claimed`/`started` для этого id уже есть.
11. **Версия агента 3**, `min_agent_version` не поднимается. Тела запросов агента не меняются.
12. **Признак `online`.** Спека data-model меняется под код: `agent_status` отдаёт `online: false`, если `last_seen_at`
старше 5 минут (решение без обсуждения, см. ниже).
## Risks / Trade-offs
- Нарушение инварианта агента (например, будущая параллельность) превратит довыдачу в двойное выполнение — безопасно
из-за идемпотентности, но тратит вызовы Jira. Инвариант фиксируется в спеке agent.
- Довыдача после долгого простоя агента выполнит старую команду: если человек за это время поменял задачу вручную,
команда «вернёт» желаемое значение. Так же вела бы себя команда в очереди; для отмены есть `cancel force`.
- Гонка двух `claim` через папку (старый долгий запрос ещё ждёт в хабе, агент прислал новый): старый берёт команду, ответ
никто не читает, новый её довыдаёт — `deliveries = 2` без реального сбоя. Предел 3 оставляет запас на один такой
случай плюс одну настоящую недоставку.
- Миграция живой базы — первая в проекте; страхуется копией перед миграцией и тестом на базе v1.
- Оценка паузы из журнала по `at` старше настоящего последнего вызова, если агент упал во время `refresh` между страницами
(последний вызов Jira позже последней записи). Пауза `refresh` — 2 с, ошибка мала.
## Migration Plan
1. Хаб: merge → `git pull` в основном checkout → перезапуск launchd (`launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`)
→ в `backups/` копия `*-schema-v1.sqlite`, `schema_version = 2`; агент v2 продолжает работу.
2. Агент v3: файл через общую папку на виртуалку, `check`, перезапуск `run`.
3. Проверка: повторный `jira-hub import admin/jira-sync` — три связи в `reversed_links`, `missing_links: 0`.
Откат: остановить хаб, вернуть копию `backups/*-schema-v1.sqlite` на место `hub.sqlite`, предыдущий код хаба; агент v3
работает с хабом v1 (тела запросов не менялись).
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-13 | Спека data-model приводится к коду: признак `online` (bool), а не `offline` | порог 5 мин совпадает, `online` уже читают MCP и автообновление; правка кода дала бы лишнюю смену контракта | нет: клиентов с `offline` нет |
| 2026-10-13 | Предел довыдач — 3, константа кода | одной недоставки на сбой папки хватает с запасом; «ядовитая» команда не держит очередь | при 3 сбоях подряд на одной команде — ложный `failed`, лечится `command_retry` |
| 2026-10-13 | Миграция v1→v2 с копией базы перед ней; существующим `claimed` — `deliveries = 1` | первая миграция живой базы; честный счёт выдач | нет |
| 2026-10-13 | Дубль темы в пакете — после `strip`, без нормализации регистра | агент ищет дубль точным сравнением `summary` | темы, различающиеся только регистром, пройдут — так же их различит агент |
| 2026-10-13 | Оценка последнего вызова Jira после перезапуска — максимальное `at` журнала | `finished` пишется сразу после последнего вызова; без новой записи на каждый вызов Jira | см. Risks: `refresh` между страницами, ошибка ≤ 2 с |
| 2026-10-13 | Поздний отчёт по отменённой `ensure_issue`: результат пишется, статус `cancelled` и `finished_at` сохраняются | зависимые команды уже `skipped`, смена статуса задним числом запутала бы пакет; ключ виден в результате и в заметке | нет |
| 2026-10-13 | `report_result` по `queued` — 409 `not_claimed` | агент уже трактует 409 как «закрыто», правка агента не нужна | нет |
| 2026-10-13 | Связи «в обратную сторону» в отчёте импорта — отдельный список `reversed_links` без учёта симметричности типа | хаб не знает симметричность; человек видит, что связь есть | несимметричная связь, поставленная наоборот, не попадёт в `missing_links` — видна в `reversed_links` |
| 2026-10-13 | Довыдача не делает резервную копию базы | копия перед первой командой пакета уже сделана при первой выдаче | нет |