111 lines
16 KiB
Markdown
111 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`
|
||
не проверяется.
|
||
- `check_confirmed(…, reporter)` для `ensure_issue` при `reporter is None` не подтверждает; `reporter` берётся из настройки,
|
||
которая никогда не задаётся. Для `ensure_link` входящая связь любого типа из `link_types` — конфликт.
|
||
- Агент: `JiraClient.last_call_at` (monotonic) только в памяти; `Runner.wait_pause` без `last_call_at` не ждёт;
|
||
`recover` ставит `cmd["pause_seconds"] = 0` и вызывает `execute`, который пишет `claimed` ещё раз.
|
||
Агент приходит за новой командой (`claim`) только когда своей незавершённой нет: каждый путь `execute` кончается
|
||
подтверждённым отчётом или остановкой процесса, `recover` выполняется до первого `claim`.
|
||
- `symmetric_types()` агента уже определяет симметричные типы связей по `GET /issueLinkType` (`inward == outward`).
|
||
- `db.migrate` только создаёт таблицы (`CREATE TABLE IF NOT EXISTS`) и пишет `schema_version = 1`; миграций по версии нет.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
Goals: ни одна команда не застревает в `claimed` без пути восстановления, кроме явного предела выдач; `ensure_issue` и
|
||
`ensure_link` подтверждаются по состоянию; созданная задача не теряется при отмене; после перезапуска агента темп Jira
|
||
соблюдается; живая база мигрирует без потери данных; агент v2 продолжает работать с новым хабом.
|
||
Non-goals — см. proposal.md.
|
||
|
||
## Decisions
|
||
|
||
1. **Повторная выдача (решение владельца, вариант A).** Если при `claim` есть команды в `claimed`, хаб выдаёт самую
|
||
старую из них (по `claimed_at`, затем `id`) снова: тот же `id`, те же параметры, `pause_seconds` по текущей политике
|
||
темпа, новый `claimed_at`, `deliveries + 1`. Основание — инвариант агента «новый `claim` только без незавершённой
|
||
команды»: раз агент пришёл, выданную он не получил или потерял. Все команды идемпотентны («проверь, потом сделай»),
|
||
поэтому довыдача уже выполненной команды даёт `done` без записи. Это не повтор в смысле правила «хаб не повторяет
|
||
команды сам»: новая команда не создаётся, решение о повторе после `failed` по-прежнему за человеком.
|
||
2. **Предел выдач — 3.** Если у команды в `claimed` уже `deliveries >= 3` и агент снова пришёл за командой, хаб переводит
|
||
её в `failed` с `error = "агент не отчитался после 3 выдач"`, `finished_at = now`, и выбирает следующую по обычным
|
||
правилам (довыдача другой `claimed` или `_next`). Защищает очередь от команды, на которой агент падает. Число 3 —
|
||
константа кода, не настройка.
|
||
3. **Удаляются `_fresh_claimed_exists` и R-13.** При довыдаче `claimed` команда не может «зависнуть» без агента: если
|
||
агента нет, очередь всё равно стоит. Обход пакета с зависшим определителем (`_definer_stale`, `blocked_batches`) больше
|
||
не нужен — определитель довыдаётся. `maybe_auto_refresh` вместо «свежей claimed» проверяет «есть любая claimed».
|
||
`agent_view.stuck` заменяется на `redelivered` — команды в `claimed` с `deliveries > 1`.
|
||
4. **Миграция схемы v1 → v2.** `SCHEMA_VERSION = 2`; `migrate` по номеру из `setting.schema_version`: для v1 — `ALTER TABLE
|
||
command ADD COLUMN deliveries INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE agent_status ADD COLUMN jira_user TEXT`,
|
||
`ALTER TABLE agent_status ADD COLUMN symmetric_link_types TEXT` (JSON-список), затем `schema_version = 2`, всё в одной
|
||
транзакции. Существующие `claimed` команды получают `deliveries = 1` (их уже выдали один раз). Новая база создаётся сразу
|
||
в v2. Перед миграцией существующей базы хаб копирует файл в `backups/<дата-время>-schema-v1.sqlite`.
|
||
5. **Отчёты.** `report_result` по команде в `queued` — `ValueError`-подобная ошибка, API отвечает 409 «команда не выдана».
|
||
Агент на 409 уже считает команду закрытой (`send_result`), правка агента не нужна. По команде в `cancelled` типа
|
||
`ensure_issue` с `status = done` и `result.key`: результат и `finished_at` записываются, статус остаётся `cancelled`,
|
||
заметка `new:N` переименовывается на ключ. Для остальных терминальных статусов — без изменений, как сейчас.
|
||
6. **Дубль темы в пакете — 422.** `validate` пакета отвергает две и более `ensure_issue` с одинаковой `summary` (после
|
||
обрезки пробелов по краям), перечисляя темы. Регистр не нормализуется: поиск дубля агентом — точное сравнение.
|
||
7. **Факты агента о Jira — в claim.** Тело `claim` получает необязательные `jira_user` (строка, `name` из `GET /myself` — то же
|
||
поле, что агент кладёт в `reporter` зеркала) и `symmetric_link_types` (список имён типов Jira с `inward == outward`).
|
||
Хаб пишет непустые значения в `agent_status` (`COALESCE`, как остальные поля claim). Это сведения о среде агента, не
|
||
настройки: требование data-model «логин Jira в настройках MUST NOT» не затрагивается. Заготовка настройки `reporter`
|
||
удаляется из кода.
|
||
8. **Подтверждение по состоянию.** `ensure_issue` — задача с той же `summary`, `reporter = agent_status.jira_user`, созданная
|
||
не раньше 7 дней назад; при `jira_user IS NULL` — не подтверждается (как сейчас). `ensure_link` — связь есть, если в
|
||
зеркале есть `source→target` нужного типа **или** тип симметричен и есть `target→source` этого типа; конфликт —
|
||
связь другого типа из `link_types` в любом направлении или обратная связь несимметричного типа. Отчёт импорта
|
||
(`missing_links`) использует то же правило «есть» (общая функция в `commands`).
|
||
9. **Агент: сбор фактов.** После первой загрузки настроек и до первого `claim` агент вызывает `GET /myself` и
|
||
`GET /issueLinkType` (вызовы Jira, обновляют `last_call_at`; пауза следующей команды отсчитывается от них). Ошибка любого
|
||
из вызовов не останавливает агента: поле не передаётся, агент повторяет сбор перед следующим `claim` не чаще раза в
|
||
10 минут, пока не получит значение. `symmetric_types()` переиспользует тот же кэш.
|
||
10. **Агент: темп после перезапуска.** При старте `Runner` берёт максимальное `at` среди записей журнала (любого вида) и,
|
||
если оно не в будущем, ставит `last_call_at = monotonic() − (now − at)`. Запись `finished` пишется сразу после
|
||
последнего вызова Jira команды, поэтому оценка не раньше настоящего вызова — ждём не меньше нужного. Пустой журнал —
|
||
`last_call_at` пуст, как сейчас. `recover` не обнуляет `pause_seconds`: берёт значение из ответа `GET /agent/commands/{id}`.
|
||
11. **Агент: `claimed` при восстановлении не пишется повторно.** `execute(cmd, journal_claimed=True)`; `recover` вызывает с
|
||
`False` — запись `claimed`/`started` для этого id уже есть.
|
||
12. **Версия агента 3**, `min_agent_version` не поднимается. Хаб принимает claim без новых полей (агент v2).
|
||
13. **Признак `online`.** Спека data-model меняется под код: `agent_status` отдаёт `online: false`, если `last_seen_at`
|
||
старше 5 минут (решение без обсуждения, см. ниже).
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- Нарушение инварианта агента (например, будущая параллельность) превратит довыдачу в двойное выполнение — безопасно
|
||
из-за идемпотентности, но тратит вызовы Jira. Инвариант фиксируется в спеке agent.
|
||
- Довыдача после долгого простоя агента выполнит старую команду: параметры не устаревают сами — человек мог за это время
|
||
поменять задачу вручную, команда «вернёт» желаемое значение. Так же вела бы себя команда в очереди; для отмены есть
|
||
`cancel force`.
|
||
- Миграция живой базы — первая в проекте; страхуется копией перед миграцией и тестом на базе v1.
|
||
- Оценка паузы из журнала по `at` старше настоящего последнего вызова, если агент упал во время `refresh` между страницами
|
||
(последний вызов Jira позже последней записи). Пауза `refresh` — 2 с, ошибка мала.
|
||
|
||
## Migration Plan
|
||
|
||
1. Хаб: merge → перезапуск launchd (`launchctl kickstart -k`) → в логе миграция v1→v2 и копия базы; агент v2 продолжает
|
||
работу.
|
||
2. Агент v3: файл через общую папку на виртуалку, `check`, перезапуск `run`; `agent_status` показывает логин.
|
||
3. Проверка: повторный `jira-hub import admin/jira-sync` — `missing_links: 0`.
|
||
Откат: остановить хаб, вернуть копию `backups/*-schema-v1.sqlite`, предыдущий код хаба; агент v3 работает с хабом v1
|
||
(лишние поля claim игнорируются — проверить тестом API).
|
||
|
||
## Decisions made without discussion
|
||
|
||
| Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|
|
||
| 2026-10-13 | Спека data-model приводится к коду: признак `online` (bool), а не `offline` | порог 5 мин совпадает, `online` уже читают MCP и автообновление; правка кода дала бы лишнюю смену контракта | нет: клиентов с `offline` нет |
|
||
| 2026-10-13 | Предел довыдач — 3, константа кода | одной недоставки на сбой папки хватает с запасом; «ядовитая» команда не держит очередь | если сбоев папки больше 2 подряд на одной команде — ложный `failed`, лечится `command_retry` |
|
||
| 2026-10-13 | Миграция v1→v2 с копией базы перед ней; существующим `claimed` — `deliveries = 1` | первая миграция живой базы; честный счёт выдач | нет |
|
||
| 2026-10-13 | Дубль темы в пакете — после `strip`, без нормализации регистра | агент ищет дубль точным сравнением `summary` | темы, различающиеся только регистром, пройдут — так же их различит агент |
|
||
| 2026-10-13 | Повторный сбор фактов агента не чаще раза в 10 минут | Jira банка — лимиты; факты меняются редко | логин придёт до 10 минут позже после восстановления Jira |
|
||
| 2026-10-13 | Оценка последнего вызова Jira после перезапуска — максимальное `at` журнала | `finished` пишется сразу после последнего вызова; без новой записи на каждый вызов Jira | см. Risks: `refresh` между страницами, ошибка ≤ 2 с |
|
||
| 2026-10-13 | Поздний отчёт по отменённой `ensure_issue`: результат пишется, статус `cancelled` сохраняется | зависимые команды уже `skipped`, смена статуса задним числом запутала бы пакет; ключ виден в результате и в заметке | нет |
|
||
| 2026-10-13 | `report_result` по `queued` — 409 «команда не выдана» | агент уже трактует 409 как «закрыто», правка агента не нужна | нет |
|