115 lines
16 KiB
Markdown
115 lines
16 KiB
Markdown
## 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 | Довыдача не делает резервную копию базы | копия перед первой командой пакета уже сделана при первой выдаче | нет |
|