16 KiB
Raw Blame History

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 как «закрыто», правка агента не нужна нет