Andrey Epifancev d3de2d6670 design.md: решения 21–22 по Task 5 (#83)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:45:36 +04:00

25 KiB
Raw Blame History

Context

main da8f5c9, схема v5, агент v13. Устройство, на которое опираются правки:

  • Зеркало — mirror.apply_state: upsert issue, замена issue_link и issue_comment задачи присланными (поле comments необязательно — без него прежние комментарии остаются). Поля зеркала агент берёт одним search (state_fields), attachment в них нет.
  • Хранилище файлов — files.py: <данные>/files/<sha256>, put (атомарно, предел 10 МБ, повтор обновляет mtime), sweep раз в сутки из tick (старше 30 дней, кроме файлов ensure_attachment в queued/claimed). Маршруты POST /files (клиент) и GET /agent/files/{sha} (агент, base64). Тело запроса — до 16 МБ (MAX_BODY): файл 10 МБ в base64 ≈ 14 МБ проходит, в том числе через реле папки.
  • Очередь — пакеты, одна команда в полёте, пауза по pacing типа, хвостовой refresh по ключам пакета (кроме пакетов только из refresh), довыдача до 3 раз, подтверждение по состоянию.
  • Агент — Executor.do_<тип>; неизвестный тип — failed «неизвестный тип команды …». JiraClient умеет JSON и multipart-загрузку (upload), считает вызовы и повторяет 429/503.
  • MCP — ходит в хаб только через client; из files берёт только MAX_SIZE; HubClient.from_home знает каталог данных хаба (JIRA_HUB_HOME).
  • Веб — карточка IssueCard.tsx, рендер вики-разметки web/src/issues/wiki/ (!картинка! сейчас — текст как есть), адрес Jira — константа JIRA_URL в web/src/board/constants.ts.

Goals / Non-Goals

Goals: список вложений задачи в зеркале без содержимого; привязка вложений к описанию и комментариям по ссылкам в тексте; скачивание одного вложения по запросу из MCP (путь к локальной копии с исходным именем) и из веба (сохранение файлом); одна команда на вложение при параллельных запросах.

Non-Goals и Anti-Goals: см. proposal.md.

Decisions

  1. Список вложений — в refresh, отдельной таблицей issue_attachment (схема v6). Поле attachment в search приходит целиком (без постраничности, в отличие от комментариев), поэтому лишних вызовов Jira нет. Таблица — часть зеркала: пишет только apply_state, присланное attachments заменяет список задачи, без поля — список остаётся (агент ≤ v13). PK — attachment_id (id вложения в Jira глобален). created_at — UTC для сортировки, как у комментариев.
  2. Скачанное — отдельная таблица attachment_file, не колонка зеркала. Граница «зеркало пишет только apply_state» сохраняется; скачанный файл переживает замену списка вложений. Строка без файла в хранилище (убран уборкой) — «не скачано», без отдельной чистки таблицы.
  3. Состояние вложения вычисляется при чтении (ready / fetching / failed / none) из attachment_file, хранилища и последней fetch_attachment по этому id. Отдельного статуса нет — нечему рассинхронизироваться.
  4. Команда fetch_attachment {key, attachment_id} в общей очереди. Скачивание — обычная команда: темп, одна команда в полёте, довыдача, журнал агента и восстановление работают как есть. Пакет из одной команды, без хвостового refresh (в Jira ничего не меняется) и без проекта (вложение соседа тоже можно скачать). Пауза по умолчанию 2 с, как у refresh (только чтение). Подтверждения по состоянию нет: потерянный отчёт закрывает довыдача — повтор безопасен. Код, перебирающий типы команд, SHALL знать новый тип: commands.check_confirmed (иначе None и 500 на отчёте после refresh), признак «пакет с записями» в queue.claim (иначе копия базы на каждое скачивание), выборка команд задачи в карточке. Отвергнуто: отдельный канал «срочных» запросов мимо очереди — второй цикл в агенте, второй источник темпа к Jira.
  5. Файл от агента к хабу — POST /agent/files {content} (base64), в результате команды — только sha. Результат команды повторяется каждые 10 с до подтверждения и пишется в журнал агента; 14 МБ base64 там не нужны. Маршрут — зеркало POST /files под агентским токеном, с теми же проверками. Отчёт done хаб принимает, только если файл с таким sha и размером лежит в хранилище.
  6. Адрес содержимого агент собирает сам — <base_url>/secure/attachment/<id>/<имя>, а не берёт content из ответа Jira: Basic-авторизация уходит только на base_url (анти-цель). Перед скачиванием — GET /issue/{key}?fields=attachment: проверка, что вложение ещё есть, и размер из Jira до скачивания (больше 10 МБ — не качать).
  7. Одна команда на вложение. POST /attachments/{id}/fetch в одной транзакции записи проверяет fetching/ready и ставит пакет. Веб и MCP могут жать сколько угодно — команда одна.
  8. Предел 10 МБ — в трёх местах: хаб отказывает в постановке по размеру из зеркала (413, без пакета), агент — по размеру из Jira (на случай, если файл заменили), хранилище — files.put (последний рубеж).
  9. Старый агент — 409 agent_too_old до постановки. Агент v13 на fetch_attachment ответил бы «неизвестный тип команды»; хаб проверяет agent_status.agent_version и сразу говорит, что нужен v14. Версия неизвестна (агент не выходил на связь) — ставим: команда дождётся агента. min_agent_version не поднимается.
  10. Ссылки на вложения ищет хаб, а не каждый интерфейс: MCP и веб получают готовые attachment_ids. Разбор — регулярные выражения по описанию и тексту комментариев (!имя!, !имя|…!, [^имя], [текст|^имя]), имя — точное совпадение, при повторе имени — самое новое вложение (так делает Jira). Веб дополнительно превращает такие ссылки в отрендеренном тексте в кнопки скачивания по тем же правилам (список вложений у него есть).
  11. MCP кладёт копию с исходным именем в <данные хаба>/downloads/<id>/<имя>. Файл хранилища без имени и расширения Claude не откроет (анти-цель). Каталог по id — нет конфликтов имён; уборка downloads/ — тем же tick хаба (30 дней). MCP читает файл через GET /attachments/{id}/content (граница «MCP — только через client»), а не из files/ напрямую.
  12. MCP attachment ждёт сам (опрос раз в 2 с, по умолчанию до 120 с, не больше 600): обычно агент успевает за секунды; не успел — понятный ответ, повторный вызов не ставит вторую команду.
  13. Веб скачивает через Blob. Маршруты хаба требуют токен в заголовке, ссылка <a href> его не передаст; fetch → Blob → временная ссылка download. Файл до 10 МБ — память браузера не проблема. Пока агент везёт файл — опрос раз в 2 с, пока карточка открыта на этой задаче; файл сохраняется сам, без второго нажатия.
  14. Уборка. Скачанные вложения от уборки не защищены (их можно скачать снова); выдача GET …/content обновляет mtime, так что файлы, которые смотрят, живут дольше.
  15. Порядок выкатки — хаб первым. Хаб v6 со старым агентом: списков вложений нет (поле не приходит), скачивание отвечает 409. Агент v14 со старым хабом: поле attachments старый хаб игнорирует (apply_state берёт известные колонки), fetch_attachment старый хаб не выдаёт.
  16. Скачивания не заслоняют записи. Пакеты «Скачать вложение …» не делают копию базы (в Jira не пишут) и не входят в «последние 10 команд по задаче» карточки и MCP issue: при разборе ошибки с десятком файлов там остаются правки моста в Jira. В общем списке пакетов они видны, как есть.
  17. Перенаправления при скачивании. urllib следует 3xx и переносит Authorization; агент SHALL не выполнять перенаправление на другой хост (обработчик перенаправлений или проверка адреса), а на тот же — может.
  18. Неизвестный размер и пустые файлы. Вложение без размера ставится, агент обрывает чтение после 10 МБ; пустое вложение принимается от агента (POST /agent/files), хотя POST /files пустой файл по-прежнему отвергает.

Risks / Trade-offs

  • Ожидание в очереди. Скачивание стоит за командами, поставленными раньше (полный refresh идёт минуты). Принимаем: MCP отвечает «ещё идёт», веб показывает «Загружается…».
  • Объём через общую папку. Файл 10 МБ — запрос ≈ 14 МБ JSON через реле. В пределах MAX_BODY; если реле окажется медленным на таких файлах — снижать предел, а не менять транспорт.
  • Имя с запрещёнными символами (: и др. — Jira банка их хранит, см. #43) — в копии MCP заменяются на _; в вебе браузер правит имя сам.
  • Ссылка по имени после переименования в Jira не находит вложение — показывается исходником. Редко.
  • Удалённое в Jira вложение остаётся в зеркале до ближайшего refresh задачи; скачивание ответит понятной ошибкой.

Migration Plan

  1. PR хаба: схема v6, attachments в apply_state, POST /agent/files, fetch_attachment в контракте и очереди, маршруты /attachments/…, ссылки в GET /issues/{key}, MCP (issue, comment, attachment), веб. После merge — git pull, make web, перезапуск хаба владельцем, /mcp → reconnect.
  2. PR агента: v14 (attachments в состоянии, fetch_attachment). После merge — jira-hub agent publish ../jira-agent/src/agent.py; agent_status показывает v14; min_agent_version не поднимать.
  3. Живая проверка: полный refresh → у задач с вложениями список в issue; MCP attachment по скриншоту и по Excel (кириллическое имя с пробелами) → путь, файл открывается; повторный вызов — без новой команды; кнопка «Скачать» в карточке → файл сохранён; вложение > 10 МБ, если найдётся, → отказ без команды.

Откат: агент — публикуется v15 без новых возможностей (только вверх); хаб — предыдущий коммит, таблицы v6 остаются и не мешают, schema_version = 6 старый хаб не понизит.

Open Questions

Нет.

Decisions made without discussion

  1. 2026-10-22 — привязка вложения к комментарию — только по ссылке в тексте (!имя!, [^имя]), без догадок по автору и времени. Обоснование: Jira сама пишет ссылку при перетаскивании файла в комментарий; эвристика даёт ложные привязки. Цена ошибки: файл, приложенный без ссылки, виден только в общем списке — добавить эвристику позже.
  2. 2026-10-22 — скачивание — команда общей очереди (fetch_attachment), а не отдельный канал. Обоснование: темп, довыдача, журнал агента — без нового кода. Цена ошибки: ожидание за длинным refresh; если мешает — приоритет fetch_attachment в выдаче.
  3. 2026-10-22 — файл от агента — отдельным маршрутом POST /agent/files, в отчёте только sha. Обоснование: отчёт повторяется и пишется в журнал агента. Цена ошибки: нет.
  4. 2026-10-22 — адрес содержимого собирает агент из base_url. Обоснование: учётные данные не уходят на чужой адрес. Цена ошибки: если в Jira банка адрес вложений отличается от /secure/attachment/<id>/<имя> — выяснится на живой проверке, поправить сборку адреса.
  5. 2026-10-22 — копия для Claude — <данные хаба>/downloads/<id>/<имя>, уборка 30 дней. Обоснование: исходное имя и расширение нужны, чтобы Claude открыл файл; каталог хаба уже под 0700. Цена ошибки: Claude в другом репозитории может спросить разрешение на чтение вне проекта — один раз на сессию.
  6. 2026-10-22 — старый агент — 409 agent_too_old до постановки, min_agent_version не поднимается. Обоснование: понятная ошибка вместо «неизвестный тип команды», старый агент продолжает работать. Цена ошибки: нет.
  7. 2026-10-22 — MCP attachment ждёт по умолчанию 120 с, максимум 600. Обоснование: обычно агент успевает за секунды, а чат не должен висеть дольше. Цена ошибки: при занятой очереди — повторный вызов.
  8. 2026-10-22 — ссылки на вложения в тексте веба — кнопка со скрепкой, без картинки. Обоснование: решение владельца «превью не делаем»; ссылка в тексте ведёт к тому же скачиванию. Цена ошибки: нет.
  9. 2026-10-22 — пауза fetch_attachment по умолчанию 2 с. Обоснование: только чтение, как refresh. Цена ошибки: нет — меняется настройкой pacing.
  10. 2026-10-22 — пакеты скачивания не делают копию базы и не входят в «последние 10 команд по задаче». Обоснование: находка рецензента — иначе серия скачиваний вытесняет копии перед записями и команды-записи из карточки. Цена ошибки: скачивания по задаче видны только в списке пакетов.
  11. 2026-10-22 — перенаправление на другой хост при скачивании — failed, без перехода. Обоснование: находка рецензента — urllib унёс бы Authorization на чужой хост. Цена ошибки: если Jira банка отдаёт вложения через другой хост — скачивание не работает до решения владельца, видно на живой проверке.
  12. 2026-10-22 — вложение без размера ставится, предел 10 МБ проверяет агент по прочитанному. Обоснование: Jira может не отдать размер; отказывать в таком вложении — терять файл. Цена ошибки: агент читает до 10 МБ лишнего.
  13. 2026-10-22 — пустое вложение принимается от агента. Обоснование: в Jira бывают пустые файлы; иначе вечный failed. Цена ошибки: нет.
  14. 2026-10-22 — command_id/batch_id в состоянии вложения: при fetching — у живой (queued/claimed) команды, иначе у последней fetch_attachment по этому id; ready требует и строку attachment_file, и файл в хранилище. Обоснование: повторный POST …/fetch должен вернуть ту же команду, что идёт, а не историческую; строка без файла — «не скачано» (решение 2). Цена ошибки: интерфейс покажет чужой command_id в нейтральных состояниях — правка одной функции. (R-1, правило исполнителя Task 2.)
  15. 2026-10-22 — при повторе имени «самое новое» вложение — максимум по (created_at, число id, id). Обоснование: детерминированный выбор при равных временах (Jira отдаёт id по возрастанию). Цена ошибки: ссылка по имени укажет на соседнее вложение — исправляется порядком ключа.
  16. 2026-10-22 — порядок отказов в request_fetch: сначала agent_too_old (409), потом размер (413). Обоснование: старый агент не скачает и маленький файл, а размер человек увидит и так; отказ по агенту не зависит от вложения. Цена ошибки: для большого файла при старом агенте покажем 409 вместо 413 — несущественно. (R-3, T3.1.)
  17. 2026-10-22 — TooLarge наследует CommandError (в POST /batches — 422), в маршруте …/fetch ловится раньше и даёт 413; attachment_id в команде — только цифры; оба отказа done (нет файла, чужой размер) дают один текст «хаб: файл не найден в хранилище». Обоснование: одна проверка размера на постановку из любого входа; Jira-id — число. Цена ошибки: поменять класс исключения / текст. (R-4, решение исполнителя Task 3.)
  18. 2026-10-22 — pacing_from_config и запасной зазор: fetch_attachment = 2 с и там, и в DEFAULT_PACING. Обоснование: иначе init из config расходится с умолчанием и сохранённая настройка без нового типа ломает выдачу. Цена ошибки: нет. (R-5.)
  19. 2026-10-22 — sweep_downloads считает возраст каталога вложения по самому новому из mtime каталога и его файлов; сбой уборки downloads/ логируется и не мешает уборке files/. Обоснование: копию, которую открывали недавно, не удалять; побочная уборка не должна ломать основную. Цена ошибки: преждевременное или запоздалое удаление копии (её MCP создаст заново). (R-6, решение исполнителя Task 4.)
  20. 2026-10-22 — неизвестный id вложения в GET /attachments/{id} — 404 no_attachment; Content-Type из mime_type только при формате тип/подтип, иначе application/octet-stream. Обоснование: единые коды вложений; заголовок не берётся из недоверенных данных Jira как есть. Цена ошибки: нет. (R-7.)
  21. 2026-10-22 — safe_name: имя «.» и «..» заменяется на attachment-<id>. Обоснование: без этого «..» (разделители уже заменены) остаётся именем файла в каталоге копий; укрепление против обхода пути. Цена ошибки: нет. (R-8, решение исполнителя Task 5.)
  22. 2026-10-22 — опрос MCP attachment — раз в POLL_SECONDS/2 (2,5 с), а не ровно 2 с; строка «взято самое новое» — только в ответе ready. Обоснование: так сказано в плане, интервал в спеке приблизителен; на таймауте/ошибке важнее их текст. Цена ошибки: нет. (R-9.)