16 KiB
16 KiB
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
- Повторная выдача (решение владельца, вариант A). Если при
claimесть команды вclaimed, хаб выдаёт самую старую из них (поclaimed_at, затемid) снова: тот жеid, те же параметры,pause_secondsпо текущей политике темпа, новыйclaimed_at,deliveries + 1. Основание — инвариант агента «новыйclaimтолько без незавершённой команды»: раз агент пришёл, выданную он не получил или потерял. Все команды идемпотентны («проверь, потом сделай»), поэтому довыдача уже выполненной команды даётdoneбез записи. Это не повтор в смысле правила «хаб не повторяет команды сам»: новая команда не создаётся, решение о повторе послеfailedпо-прежнему за человеком. - Предел выдач — 3. Если у команды в
claimedужеdeliveries >= 3и агент снова пришёл за командой, хаб переводит её вfailedсerror = "агент не отчитался после 3 выдач",finished_at = now, и выбирает следующую по обычным правилам (довыдача другойclaimedили_next). Защищает очередь от команды, на которой агент падает. Число 3 — константа кода, не настройка. - Удаляются
_fresh_claimed_existsи R-13. При довыдачеclaimedкоманда не может «зависнуть» без агента: если агента нет, очередь всё равно стоит. Обход пакета с зависшим определителем (_definer_stale,blocked_batches) больше не нужен — определитель довыдаётся.maybe_auto_refreshвместо «свежей claimed» проверяет «есть любая claimed».agent_view.stuckзаменяется наredelivered— команды вclaimedсdeliveries > 1. - Миграция схемы 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. - Отчёты.
report_resultпо команде вqueued—ValueError-подобная ошибка, API отвечает 409 «команда не выдана». Агент на 409 уже считает команду закрытой (send_result), правка агента не нужна. По команде вcancelledтипаensure_issueсstatus = doneиresult.key: результат иfinished_atзаписываются, статус остаётсяcancelled, заметкаnew:Nпереименовывается на ключ. Для остальных терминальных статусов — без изменений, как сейчас. - Дубль темы в пакете — 422.
validateпакета отвергает две и болееensure_issueс одинаковойsummary(после обрезки пробелов по краям), перечисляя темы. Регистр не нормализуется: поиск дубля агентом — точное сравнение. - Факты агента о 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удаляется из кода. - Подтверждение по состоянию.
ensure_issue— задача с той жеsummary,reporter = agent_status.jira_user, созданная не раньше 7 дней назад; приjira_user IS NULL— не подтверждается (как сейчас).ensure_link— связь есть, если в зеркале естьsource→targetнужного типа или тип симметричен и естьtarget→sourceэтого типа; конфликт — связь другого типа изlink_typesв любом направлении или обратная связь несимметричного типа. Отчёт импорта (missing_links) использует то же правило «есть» (общая функция вcommands). - Агент: сбор фактов. После первой загрузки настроек и до первого
claimагент вызываетGET /myselfиGET /issueLinkType(вызовы Jira, обновляютlast_call_at; пауза следующей команды отсчитывается от них). Ошибка любого из вызовов не останавливает агента: поле не передаётся, агент повторяет сбор перед следующимclaimне чаще раза в 10 минут, пока не получит значение.symmetric_types()переиспользует тот же кэш. - Агент: темп после перезапуска. При старте
Runnerберёт максимальноеatсреди записей журнала (любого вида) и, если оно не в будущем, ставитlast_call_at = monotonic() − (now − at). Записьfinishedпишется сразу после последнего вызова Jira команды, поэтому оценка не раньше настоящего вызова — ждём не меньше нужного. Пустой журнал —last_call_atпуст, как сейчас.recoverне обнуляетpause_seconds: берёт значение из ответаGET /agent/commands/{id}. - Агент:
claimedпри восстановлении не пишется повторно.execute(cmd, journal_claimed=True);recoverвызывает сFalse— записьclaimed/startedдля этого id уже есть. - Версия агента 3,
min_agent_versionне поднимается. Хаб принимает claim без новых полей (агент v2). - Признак
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
- Хаб: merge → перезапуск launchd (
launchctl kickstart -k) → в логе миграция v1→v2 и копия базы; агент v2 продолжает работу. - Агент v3: файл через общую папку на виртуалку,
check, перезапускrun;agent_statusпоказывает логин. - Проверка: повторный
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 как «закрыто», правка агента не нужна | нет |