25 KiB
25 KiB
Context
main da8f5c9, схема v5, агент v13. Устройство, на которое опираются правки:
- Зеркало —
mirror.apply_state: upsertissue, замена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
- Список вложений — в
refresh, отдельной таблицейissue_attachment(схема v6). Полеattachmentвsearchприходит целиком (без постраничности, в отличие от комментариев), поэтому лишних вызовов Jira нет. Таблица — часть зеркала: пишет толькоapply_state, присланноеattachmentsзаменяет список задачи, без поля — список остаётся (агент ≤ v13). PK —attachment_id(id вложения в Jira глобален).created_at— UTC для сортировки, как у комментариев. - Скачанное — отдельная таблица
attachment_file, не колонка зеркала. Граница «зеркало пишет толькоapply_state» сохраняется; скачанный файл переживает замену списка вложений. Строка без файла в хранилище (убран уборкой) — «не скачано», без отдельной чистки таблицы. - Состояние вложения вычисляется при чтении (
ready/fetching/failed/none) изattachment_file, хранилища и последнейfetch_attachmentпо этому id. Отдельного статуса нет — нечему рассинхронизироваться. - Команда
fetch_attachment {key, attachment_id}в общей очереди. Скачивание — обычная команда: темп, одна команда в полёте, довыдача, журнал агента и восстановление работают как есть. Пакет из одной команды, без хвостовогоrefresh(в Jira ничего не меняется) и без проекта (вложение соседа тоже можно скачать). Пауза по умолчанию 2 с, как уrefresh(только чтение). Подтверждения по состоянию нет: потерянный отчёт закрывает довыдача — повтор безопасен. Код, перебирающий типы команд, SHALL знать новый тип:commands.check_confirmed(иначеNoneи 500 на отчёте послеrefresh), признак «пакет с записями» вqueue.claim(иначе копия базы на каждое скачивание), выборка команд задачи в карточке. Отвергнуто: отдельный канал «срочных» запросов мимо очереди — второй цикл в агенте, второй источник темпа к Jira. - Файл от агента к хабу —
POST /agent/files {content}(base64), в результате команды — только sha. Результат команды повторяется каждые 10 с до подтверждения и пишется в журнал агента; 14 МБ base64 там не нужны. Маршрут — зеркалоPOST /filesпод агентским токеном, с теми же проверками. Отчётdoneхаб принимает, только если файл с таким sha и размером лежит в хранилище. - Адрес содержимого агент собирает сам —
<base_url>/secure/attachment/<id>/<имя>, а не берётcontentиз ответа Jira: Basic-авторизация уходит только наbase_url(анти-цель). Перед скачиванием —GET /issue/{key}?fields=attachment: проверка, что вложение ещё есть, и размер из Jira до скачивания (больше 10 МБ — не качать). - Одна команда на вложение.
POST /attachments/{id}/fetchв одной транзакции записи проверяетfetching/readyи ставит пакет. Веб и MCP могут жать сколько угодно — команда одна. - Предел 10 МБ — в трёх местах: хаб отказывает в постановке по размеру из зеркала (413, без пакета), агент — по
размеру из Jira (на случай, если файл заменили), хранилище —
files.put(последний рубеж). - Старый агент — 409
agent_too_oldдо постановки. Агент v13 наfetch_attachmentответил бы «неизвестный тип команды»; хаб проверяетagent_status.agent_versionи сразу говорит, что нужен v14. Версия неизвестна (агент не выходил на связь) — ставим: команда дождётся агента.min_agent_versionне поднимается. - Ссылки на вложения ищет хаб, а не каждый интерфейс: MCP и веб получают готовые
attachment_ids. Разбор — регулярные выражения по описанию и тексту комментариев (!имя!,!имя|…!,[^имя],[текст|^имя]), имя — точное совпадение, при повторе имени — самое новое вложение (так делает Jira). Веб дополнительно превращает такие ссылки в отрендеренном тексте в кнопки скачивания по тем же правилам (список вложений у него есть). - MCP кладёт копию с исходным именем в
<данные хаба>/downloads/<id>/<имя>. Файл хранилища без имени и расширения Claude не откроет (анти-цель). Каталог по id — нет конфликтов имён; уборкаdownloads/— тем жеtickхаба (30 дней). MCP читает файл черезGET /attachments/{id}/content(граница «MCP — только черезclient»), а не изfiles/напрямую. - MCP
attachmentждёт сам (опрос раз в 2 с, по умолчанию до 120 с, не больше 600): обычно агент успевает за секунды; не успел — понятный ответ, повторный вызов не ставит вторую команду. - Веб скачивает через
Blob. Маршруты хаба требуют токен в заголовке, ссылка<a href>его не передаст;fetch→Blob→ временная ссылкаdownload. Файл до 10 МБ — память браузера не проблема. Пока агент везёт файл — опрос раз в 2 с, пока карточка открыта на этой задаче; файл сохраняется сам, без второго нажатия. - Уборка. Скачанные вложения от уборки не защищены (их можно скачать снова); выдача
GET …/contentобновляет mtime, так что файлы, которые смотрят, живут дольше. - Порядок выкатки — хаб первым. Хаб v6 со старым агентом: списков вложений нет (поле не приходит), скачивание
отвечает 409. Агент v14 со старым хабом: поле
attachmentsстарый хаб игнорирует (apply_stateберёт известные колонки),fetch_attachmentстарый хаб не выдаёт. - Скачивания не заслоняют записи. Пакеты «Скачать вложение …» не делают копию базы (в Jira не пишут) и не входят
в «последние 10 команд по задаче» карточки и MCP
issue: при разборе ошибки с десятком файлов там остаются правки моста в Jira. В общем списке пакетов они видны, как есть. - Перенаправления при скачивании.
urllibследует 3xx и переноситAuthorization; агент SHALL не выполнять перенаправление на другой хост (обработчик перенаправлений или проверка адреса), а на тот же — может. - Неизвестный размер и пустые файлы. Вложение без размера ставится, агент обрывает чтение после 10 МБ; пустое
вложение принимается от агента (
POST /agent/files), хотяPOST /filesпустой файл по-прежнему отвергает.
Risks / Trade-offs
- Ожидание в очереди. Скачивание стоит за командами, поставленными раньше (полный
refreshидёт минуты). Принимаем: MCP отвечает «ещё идёт», веб показывает «Загружается…». - Объём через общую папку. Файл 10 МБ — запрос ≈ 14 МБ JSON через реле. В пределах
MAX_BODY; если реле окажется медленным на таких файлах — снижать предел, а не менять транспорт. - Имя с запрещёнными символами (
:и др. — Jira банка их хранит, см. #43) — в копии MCP заменяются на_; в вебе браузер правит имя сам. - Ссылка по имени после переименования в Jira не находит вложение — показывается исходником. Редко.
- Удалённое в Jira вложение остаётся в зеркале до ближайшего
refreshзадачи; скачивание ответит понятной ошибкой.
Migration Plan
- 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. - PR агента: v14 (
attachmentsв состоянии,fetch_attachment). После merge —jira-hub agent publish ../jira-agent/src/agent.py;agent_statusпоказывает v14;min_agent_versionне поднимать. - Живая проверка: полный
refresh→ у задач с вложениями список вissue; MCPattachmentпо скриншоту и по Excel (кириллическое имя с пробелами) → путь, файл открывается; повторный вызов — без новой команды; кнопка «Скачать» в карточке → файл сохранён; вложение > 10 МБ, если найдётся, → отказ без команды.
Откат: агент — публикуется v15 без новых возможностей (только вверх); хаб — предыдущий коммит, таблицы v6 остаются и не
мешают, schema_version = 6 старый хаб не понизит.
Open Questions
Нет.
Decisions made without discussion
- 2026-10-22 — привязка вложения к комментарию — только по ссылке в тексте (
!имя!,[^имя]), без догадок по автору и времени. Обоснование: Jira сама пишет ссылку при перетаскивании файла в комментарий; эвристика даёт ложные привязки. Цена ошибки: файл, приложенный без ссылки, виден только в общем списке — добавить эвристику позже. - 2026-10-22 — скачивание — команда общей очереди (
fetch_attachment), а не отдельный канал. Обоснование: темп, довыдача, журнал агента — без нового кода. Цена ошибки: ожидание за длиннымrefresh; если мешает — приоритетfetch_attachmentв выдаче. - 2026-10-22 — файл от агента — отдельным маршрутом
POST /agent/files, в отчёте только sha. Обоснование: отчёт повторяется и пишется в журнал агента. Цена ошибки: нет. - 2026-10-22 — адрес содержимого собирает агент из
base_url. Обоснование: учётные данные не уходят на чужой адрес. Цена ошибки: если в Jira банка адрес вложений отличается от/secure/attachment/<id>/<имя>— выяснится на живой проверке, поправить сборку адреса. - 2026-10-22 — копия для Claude —
<данные хаба>/downloads/<id>/<имя>, уборка 30 дней. Обоснование: исходное имя и расширение нужны, чтобы Claude открыл файл; каталог хаба уже под0700. Цена ошибки: Claude в другом репозитории может спросить разрешение на чтение вне проекта — один раз на сессию. - 2026-10-22 — старый агент — 409
agent_too_oldдо постановки,min_agent_versionне поднимается. Обоснование: понятная ошибка вместо «неизвестный тип команды», старый агент продолжает работать. Цена ошибки: нет. - 2026-10-22 — MCP
attachmentждёт по умолчанию 120 с, максимум 600. Обоснование: обычно агент успевает за секунды, а чат не должен висеть дольше. Цена ошибки: при занятой очереди — повторный вызов. - 2026-10-22 — ссылки на вложения в тексте веба — кнопка со скрепкой, без картинки. Обоснование: решение владельца «превью не делаем»; ссылка в тексте ведёт к тому же скачиванию. Цена ошибки: нет.
- 2026-10-22 — пауза
fetch_attachmentпо умолчанию 2 с. Обоснование: только чтение, какrefresh. Цена ошибки: нет — меняется настройкойpacing. - 2026-10-22 — пакеты скачивания не делают копию базы и не входят в «последние 10 команд по задаче». Обоснование: находка рецензента — иначе серия скачиваний вытесняет копии перед записями и команды-записи из карточки. Цена ошибки: скачивания по задаче видны только в списке пакетов.
- 2026-10-22 — перенаправление на другой хост при скачивании —
failed, без перехода. Обоснование: находка рецензента —urllibунёс быAuthorizationна чужой хост. Цена ошибки: если Jira банка отдаёт вложения через другой хост — скачивание не работает до решения владельца, видно на живой проверке. - 2026-10-22 — вложение без размера ставится, предел 10 МБ проверяет агент по прочитанному. Обоснование: Jira может не отдать размер; отказывать в таком вложении — терять файл. Цена ошибки: агент читает до 10 МБ лишнего.
- 2026-10-22 — пустое вложение принимается от агента. Обоснование: в Jira бывают пустые файлы; иначе вечный
failed. Цена ошибки: нет. - 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.) - 2026-10-22 — при повторе имени «самое новое» вложение — максимум по
(created_at, число id, id). Обоснование: детерминированный выбор при равных временах (Jira отдаёт id по возрастанию). Цена ошибки: ссылка по имени укажет на соседнее вложение — исправляется порядком ключа. - 2026-10-22 — порядок отказов в
request_fetch: сначалаagent_too_old(409), потом размер (413). Обоснование: старый агент не скачает и маленький файл, а размер человек увидит и так; отказ по агенту не зависит от вложения. Цена ошибки: для большого файла при старом агенте покажем 409 вместо 413 — несущественно. (R-3, T3.1.) - 2026-10-22 —
TooLargeнаследуетCommandError(вPOST /batches— 422), в маршруте…/fetchловится раньше и даёт 413;attachment_idв команде — только цифры; оба отказаdone(нет файла, чужой размер) дают один текст «хаб: файл не найден в хранилище». Обоснование: одна проверка размера на постановку из любого входа; Jira-id — число. Цена ошибки: поменять класс исключения / текст. (R-4, решение исполнителя Task 3.) - 2026-10-22 —
pacing_from_configи запасной зазор:fetch_attachment= 2 с и там, и вDEFAULT_PACING. Обоснование: иначеinitиз config расходится с умолчанием и сохранённая настройка без нового типа ломает выдачу. Цена ошибки: нет. (R-5.) - 2026-10-22 —
sweep_downloadsсчитает возраст каталога вложения по самому новому из mtime каталога и его файлов; сбой уборкиdownloads/логируется и не мешает уборкеfiles/. Обоснование: копию, которую открывали недавно, не удалять; побочная уборка не должна ломать основную. Цена ошибки: преждевременное или запоздалое удаление копии (её MCP создаст заново). (R-6, решение исполнителя Task 4.) - 2026-10-22 — неизвестный id вложения в
GET /attachments/{id}— 404no_attachment;Content-Typeизmime_typeтолько при формате тип/подтип, иначеapplication/octet-stream. Обоснование: единые коды вложений; заголовок не берётся из недоверенных данных Jira как есть. Цена ошибки: нет. (R-7.) - 2026-10-22 —
safe_name: имя «.» и «..» заменяется наattachment-<id>. Обоснование: без этого «..» (разделители уже заменены) остаётся именем файла в каталоге копий; укрепление против обхода пути. Цена ошибки: нет. (R-8, решение исполнителя Task 5.) - 2026-10-22 — опрос MCP
attachment— раз вPOLL_SECONDS/2(2,5 с), а не ровно 2 с; строка «взято самое новое» — только в ответеready. Обоснование: так сказано в плане, интервал в спеке приблизителен; на таймауте/ошибке важнее их текст. Цена ошибки: нет. (R-9.)