## Context `main` da8f5c9, схема v5, агент v13. Устройство, на которое опираются правки: - **Зеркало** — `mirror.apply_state`: upsert `issue`, замена `issue_link` и `issue_comment` задачи присланными (поле `comments` необязательно — без него прежние комментарии остаются). Поля зеркала агент берёт одним `search` (`state_fields`), `attachment` в них нет. - **Хранилище файлов** — `files.py`: `<данные>/files/`, `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. **Адрес содержимого агент собирает сам** — `/secure/attachment//<имя>`, а не берёт `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//<имя>`.** Файл хранилища без имени и расширения Claude не откроет (анти-цель). Каталог по id — нет конфликтов имён; уборка `downloads/` — тем же `tick` хаба (30 дней). MCP читает файл через `GET /attachments/{id}/content` (граница «MCP — только через `client`»), а не из `files/` напрямую. 12. **MCP `attachment` ждёт сам** (опрос раз в 2 с, по умолчанию до 120 с, не больше 600): обычно агент успевает за секунды; не успел — понятный ответ, повторный вызов не ставит вторую команду. 13. **Веб скачивает через `Blob`.** Маршруты хаба требуют токен в заголовке, ссылка `` его не передаст; `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//<имя>` — выяснится на живой проверке, поправить сборку адреса. 5. 2026-10-22 — **копия для Claude — `<данные хаба>/downloads//<имя>`, уборка 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-`.** Обоснование: без этого «..» (разделители уже заменены) остаётся именем файла в каталоге копий; укрепление против обхода пути. Цена ошибки: нет. (R-8, решение исполнителя Task 5.) 22. 2026-10-22 — **опрос MCP `attachment` — раз в `POLL_SECONDS/2` (2,5 с), а не ровно 2 с; строка «взято самое новое» — только в ответе `ready`.** Обоснование: так сказано в плане, интервал в спеке приблизителен; на таймауте/ошибке важнее их текст. Цена ошибки: нет. (R-9.)