WEB-450 · Задача · — · web
me-bridge теряет фото/документы из Telegram
Закрыт
P2
ведёт: —
Суть
# Фото/документы из Telegram не доходят до координатора (me-bridge теряет медиа)
📖 Правила обогащения: WEB-449.
## Симптом (боевой тест owner 30.08 22:31)
Owner прислал скрин двумя способами — как фото (сжатое) и как файл-документ, с текстом-сопровождением. Текст пришёл в мост; ОБЕ картинки — нет. Проверено 30.08 ~05:30 IST:
- MessageLog (Pi prod DB) за 25 мин: только type=text/voice, ни photo/image/document;
- Source (загрузки тетради): свежих строк с картинками нет (последний аплоад 29.08);
- fable-direct-inbox.log (M1): медиа-упоминаний нет.
## Гипотеза
me-mode-мост объявляет «голос и файлы идут в me-bridge» (баннер me-mode), но реально ingest-путь берёт только text+voice; photo/document Telegram-update дропаются (нет getFile→скачивание file_id). Скрипты: M1 ~/nc-c3-digest-wt/scripts/me-bridge-* (pipe-watcher — терминальный, не телеграм-ingest; сам телеграм-ingest найти и дополнить).
## Задача
Дополнить телеграм-ingest me-bridge: на update с photo[]/document — брать самый крупный file_id, getFile→download в общий путь, класть локально (labs/ или Downloads по owner-delivery-channels), в мост слать пометку «[картинка N: путь] + caption». Тогда координатор откроет файл Read-ом (видит изображения). Bulk/альбомы: media_group_id собирать пачкой. Негатив: не-owner chat_id игнорировать (как сейчас).
## Обходной путь (пока не починено)
Картинку залить как ИСТОЧНИК в тетрадь (обычный аплоад в ноутбук) → координатор смотрит браузером. Для быстрого обсуждения — описать словами.
Приоритет: P2 (удобство, не блокер). Owner заинтересован.
--- 31.08 ~06:30 IST (Фабл): ЗАПУЩЕНА НА M1 (указание owner 22:38). Уточнён диагноз: демон me-bridge-mac-daemon.ts УЖЕ качает вложения (parseAttachmentPayload→fetchAttachmentToLocal, стр ~672), проблема ВЫШЕ — upstream (webhook/getUpdates), пишущий meBridgeRequest, не формирует attachment-marker для photo[]/document. Волна mediabridge (codex M1): найти upstream, добавить photo(наибольший file_id)+document→getFile→short-link→marker+caption, альбомы media_group_id, юнит-тест на синтетическом update. Обходной путь остаётся: аплоад как источник.
--- 31.08 ~11:55 IST (Фабл): приёмка медиа-моста (M1) = NO-GO, «БЕЗОПАСНО АКТИВИРОВАТЬ: НЕТ» — и это правильный отлов, канал owner-а не рискуем. Функционал верен (10/10 attachment + 4/4 route воспроизведены), но ДВА P1 в очереди доставки: (A) одиночная запись создаётся status=pending с payload '__pending_attachment__' ДО записи storage/short-link/marker (meBridgeAttachments.ts:295-303 vs :357) → daemon может забрать раньше готовности → Claude получит задачу БЕЗ файла; (B) album-row переиспользуется только при status=deferred (:142-149), но каждая часть через 5с quiet-window ставит pending (:255-261) → daemon claim'ит немедленно → альбом рвётся на несколько задач. Круг 922-mediabridge2 запущен на M1 (атомарная видимость marker + claim-lock альбома + тесты гонки).
--- 31.08 ~13:15 IST (Фабл): круг 2 медиа-моста ГОТОВ — оба P1 очереди доставки исправлены (одиночное вложение не видно daemon'у до готовности marker; альбом не рвётся после quiet-window). Финальная приёмка на M1 (accmediabridge2) с тестами ГОНКИ + требованием точной процедуры активации и отката. Активирую только по «БЕЗОПАСНО АКТИВИРОВАТЬ: да».
--- 31.08 ~13:35 IST (Фабл): приёмка круга 2 = NO-GO, «АКТИВИРОВАТЬ: нет». P1-A ЗАКРЫТ ✅ (deferred+placeholder, атомарная публикация условным updateMany, guard в meBridgeQueue против claim placeholder/будущего claimableAt). P1-B ЖИВ ❌: часть альбома ПОСЛЕ перевода collector'а deferred→pending создаёт ВТОРОЙ collector → альбом уходит несколькими задачами. Круг 3 (M1): «догоняющее присоединение» к незаклеймленной pending-записи ИЛИ продлеваемое album-окно + тесты (части 0/3/12с → одна задача; гонка в момент клейма). Отдельно: приёмка не смогла route-level smoke (нет generated Prisma client) — попросил починить среду или честно назвать помеху.
--- 31.08 ~15:55 IST (Фабл): приёмка круга 3 = NO-GO, «активировать: нет». P1-A ✅, P1-B ✅ (поздняя часть получает continuation-связку). НОВЫЕ находки (security/operational): (1) лимит 10/час = count→create ⇒ ГОНКА, параллельные updates превышают лимит; (2) при отказе enqueue (>25MB/лимит) webhook ПАДАЕТ В ОБЫЧНЫЙ RAG ingest ⇒ обход hard-limit; (3) нет DB-backed конкурентного теста и live round-trip. Круг 4 (M1): атомарный лимит (единый conditional insert / advisory lock), терминальный отказ для me-mode с понятным ответом юзеру, DB-backed конкурентный тест 15 параллельных при лимите 10.
--- 31.08 ~17:45 IST (Фабл): круг 4 закрыл ОБА security-gap (атомарный лимит без гонки count→create; терминальный отказ вместо падения в RAG-ingest). Финальная приёмка на M1 (accmediabridge4) с обязательным конкурентным прогоном 15-при-лимите-10 на реальном Postgres. Активация — только по «БЕЗОПАСНО: да».
--- 31.08 ~18:35 IST (Фабл): приёмка круга 4 = «активировать: нет», но по НОВОЙ, отличной причине — приёмщик поднял РЕАЛЬНЫЙ PostgreSQL и прогнал 15 параллельных enqueue: при session timezone GB тест ПРОВАЛИЛСЯ, при UTC прошёл ⇒ hour-bucket квоты зависит от таймзоны сессии (на проде окно может считаться иначе). Отличный отлов — на моках это невидимо. Круг 5 (M1): UTC-независимый bucket + прогон на трёх таймзонах (GB/UTC/Europe-Kyiv) + регресс-тест + проверка соседних квот на ту же болезнь.
--- 31.08 ~21:05 IST (Фабл): круг 5 готов — timezone-зависимость устранена (MeBridgeAttachmentQuota.bucketStart → Timestamptz(3), окно считается в UTC независимо от session timezone). Финальная приёмка на M1 (accmediabridge5) с ОБЯЗАТЕЛЬНЫМ прогоном 15→10/5 на реальном PostgreSQL в ТРЁХ таймзонах (GB/UTC/Europe-Kyiv) + регресс-тест смены tz + проверка соседних квот. Активация — только по «БЕЗОПАСНО: да».
--- 31.08 ~22:30 IST (Фабл): приёмка круга 5: timezone-gap ЗАКРЫТ и подтверждён реальным PostgreSQL во ВСЕХ трёх session timezone (GB/UTC/Kyiv) ✅. Остался один пункт: поздняя часть альбома после claim → первые части уходят отдельной continuation-задачей, гарантии «0/3/12с → одна задача» нет. Круг 6 (M1): продлеваемое album-окно с жёстким максимумом ЛИБО дослать файлы в ту же задачу; тест на реальном Postgres + крайний случай 70с.
--- 01.09 ~00:40 IST (Фабл): приёмка круга 6: ФУНКЦИОНАЛ ПОДТВЕРЖДЁН — альбом 0/3/12с + late-70s PASS на РЕАЛЬНОМ PostgreSQL 17.8 (disposable-база с настоящими таблицами MeBridgeRequest/ShortLink/MeBridgeAttachmentQuota) ✅. Активация заблокирована ДВУМЯ операционными рисками: 🔴 P1 SECURITY — ЛИТЕРАЛЬНЫЙ пароль БД в docs/operator/ME_BRIDGE_README.md:189 и REBOOT_SURVIVAL.md:75 (в git-истории ⇒ потенциально скомпрометирован, вопрос ротации эскалирую owner-у после отчёта волны); 🟠 активация может перезапустить СТАРЫЙ checkout вместо нужного коммита (ME_BRIDGE_README.md:80 vs ExecStart живого процесса). Круг 7 (M1): вычистить литералы из ВСЕЙ docs/operator + процедура активации с проверкой запущенного sha. Функциональные победы сохраняются.
--- 01.09 ~02:35 IST (Фабл): 🔐 круг 7: scrub ВСЕЙ docs/operator выполнен — вместо литералов плейсхолдеры env/secret-store. Область оказалась ШИРЕ одного пароля БД: DB DSN/PGPASSWORD, cron-секрет, bearer, signal/telephony, Telnyx, embed-токены. Волна прямо заявляет: РОТАЦИЯ НУЖНА (значения были в файлах и в git-истории ⇒ считаются засвеченными), выполнить её должен владелец. ЭСКАЛИРОВАНО owner-у: предложен план — наши секреты (cron/bearer/пароль БД) меняю сам по его добру с обновлением всех мест использования, внешние (Telnyx/signal) меняет он в кабинетах. Активацию медиа-моста держу до ротации + приёмки круга 7.
=== 🔐 РЕЕСТР СЕКРЕТОВ ДЛЯ РОТАЦИИ (owner 01.09 08:10: «пока НЕ меняем, у нас всё тестовое» — фиксируем на будущее, ПЕРЕД боевым запуском ротация ОБЯЗАТЕЛЬНА) ===
Что засветилось (лежало литералами в docs/operator, попало в git-историю ⇒ считать скомпрометированным):
| Секрет | Где лежал (файлы docs/operator) | Где используется / что менять |
|---|---|---|
| DB DSN + PGPASSWORD (Postgres) | ME_BRIDGE_README.md, REBOOT_SURVIVAL.md, ENGINEERING_GUIDE.md | Pi: shared/.env (DATABASE_URL), docker noteclone-pgvector, все run-dir spend-runtime-*.env; менять паролем роли в PG + все .env |
| CRON_SECRET | ME_BRIDGE_README.md, ENGINEERING_GUIDE.md | Pi: shared/run/flip-cron.secret; вызовы /api/cron/* (reconcile, source-indexing) |
| Bearer / admin token | ENGINEERING_GUIDE.md, SWARM_ARCHITECTURE.md | админ-эндпоинты (/api/admin/*), внутренние вызовы |
| Signal / telephony ключи | SIGNAL_CALL_SA2_TODO_2026-04-27.md, MEETING_ROOMS_LIVE_PROOF_RUNBOOK | signal-cli-forked, ag-voice, SIP-конфиги (Asterisk на Pi) |
| Telnyx (PSTN/DID) | PSTN_DID_TELNYX_SETUP_2026-04-26.md | кабинет Telnyx (ВНЕШНИЙ провайдер — меняет owner), webhook-конфиг |
| Embed tokens | GDRIVE_AUDIT_FREE_MVP_HANDOFF, CLI_BRIDGE_HANDOFF | embed-виджеты, GDrive-интеграция |
СТАТУС: значения в документации ЗАМЕНЕНЫ плейсхолдерами (коммит 273f2458 «harden mediabridge activation and operator secrets»); сами секреты НЕ менялись по решению owner (тестовый контур, пользователей нет).
ЧЕК-ЛИСТ РОТАЦИИ (когда пойдём в бой): 1) PG-роль + DATABASE_URL во всех run-dir и .env → рестарт служб; 2) flip-cron.secret + обновить вызывающие; 3) admin/bearer; 4) signal/SIP; 5) Telnyx в кабинете (owner) + webhook; 6) embed/GDrive; 7) прогнать paid-гейт и смоки после каждой группы.
Связано: WEB-450 (медиа-мост), правила WEB-449.
--- 31.08 12:05Z — ПРИЁМКА КРУГА 7 (M1, sol xhigh, дерево wt-accmb7, изолированный HOME и отдельная disposable PostgreSQL): «БЕЗОПАСНО: нет; активировать: нет». Четыре блокера:
1. 🔴 FAIL-OPEN: scripts/me-bridge-mac-daemon.ts:223-241 и scripts/me-bridge-pipe-watcher.ts:92-110 проверяют sha и dirty ТОЛЬКО внутри `if (EXPECTED_GIT_SHA)`; scripts/claude-main-tmux-bootstrap.sh:33-47,73-85 запускает guard лишь при непустом sha. Достаточно удалить ~/.me-bridge-expected-sha — и защита молча выключается. Хуже: scripts/__tests__/me-bridge-activation-context.test.ts:76-86 ПРЯМО ЗАКРЕПЛЯЕТ это поведение (отсутствие expectation = успех). Это ровно тот класс, о котором память absence-of-errors-is-not-proof-of-life и guards-must-survive-process-boundary.
2. Квота текста: документация 20/ч, код 15/ч — нет единого источника значения.
3. Scrub неполный: проверялись изменённые строки, а не вся docs/operator и полный diff серии.
4. Процедура активации неисполнима на реальном хосте: расходятся installed и candidate plist, есть stale tmux-сессия; замена загруженных launchd jobs не описана.
ПОДТВЕРЖДЕНО ПРИЁМКОЙ (положительное): проверка шла через настоящие entrypoints с новым процессом на каждом шаге, боевые jobs и tmux не трогались; терминальный отказ вложения возвращается ДО ветки RAG-ingest; обе disposable БД удалены и отсутствие проверено запросом.
КРУГ 8 ЗАПУЩЕН на M1 (mediabridge8): fail-closed при отсутствующем/пустом expectation + переписать закрепляющий ошибку тест; единый источник значения квоты; полный scrub по классам паттернов, а не по списку имён; исполнимая процедура замены загруженных jobs и stale-сессии. Функционал WEB-450 обязан остаться зелёным на реальном PostgreSQL.
Секреты по решению owner НЕ ротируем (контур тестовый); реестр ротации — выше в этом тикете.
--- 31.08 16:05Z — ПРИЁМКА КРУГА 8 (M1, sol xhigh, новый приёмщик): «БЕЗОПАСНО: нет; активировать: нет»
ПРОШЛО: fail-closed из круга 8 подтверждён — без файла ожидаемого отпечатка запуск отказывает, в том числе после перезапуска процесса; переписанный тест больше не закрепляет fail-open.
НОВЫЕ НАХОДКИ:
1. 🔴 НОВЫЙ КЛАСС — утечка через общий /tmp: runtime prompts, вложения и содержимое журнала доступны другим локальным пользователям машины. Не наружу, но настоящая утечка «вбок».
2. Scrub пропустил 30 литеральных идентификаторов операторов и учётных записей — ТРЕТЬЯ подряд находка одного класса.
3. Runbook активации сверяется со старым canonical HEAD, нет шагов первого запуска, проверки после установки и отката.
РЕШЕНИЕ ПО ПРАВИЛУ n-same-class-findings-means-design-not-bugs: третья находка класса «scrub неполный» ⇒ меняем устройство. Круг 9 (mediabridge9) требует не «вычистить ещё раз», а СДЕЛАТЬ АВТОМАТИЧЕСКИЙ ГЕЙТ, прогоняемый в prebuild/CI по всему дереву, падающий на литералы по классам (пароли, DSN, токены, ключи, телефоны, e-mail операторов, base64/JWT-подобное, идентификаторы учёток), со списком исключений и обоснованиями. Негативный тест обязателен: подложенный тестовый литерал → гейт падает; убран → зелёный. Плюс приватный каталог с правами 700 вместо общего /tmp (проверять stat-ом, а не глазами) и приведение runbook к реальности.
--- 31.08 17:40-17:50Z — 🔴 ИНЦИДЕНТ: КАНАЛ OWNER→КООРДИНАТОР БЫЛ МЁРТВ 7 ЧАСОВ (моя вина, посадка l78)
СИМПТОМ: owner написал «есть мнение, что ты ни смс, ни голосовых больше не принимаешь, и они не расшифровываются». Проверка: последняя запись в MessageLog — 10:53, следующая только после починки. Семь часов входящих не было вообще.
ЧТО ВВОДИЛО В ЗАБЛУЖДЕНИЕ: getWebhookInfo показывал url на месте, pending_update_count=0 и старую ошибку 09:29 — снаружи всё выглядело здоровым. nginx отдавал 200 на каждый POST от Telegram. То есть провайдер считал доставку успешной.
КОРЕНЬ: в журнале приложения — `[telegram/webhook] Dedupe insert error (fail-safe): Invalid prisma.webhookDedupe.create() invocation: The column WebhookDedupe.fencingToken does not exist in the current database`. Обработчик падал на КАЖДОМ входящем и по fail-safe отвечал 200.
ПОЧЕМУ КОЛОНКИ НЕ БЫЛО: сравнение схем внутри релиза показало, что prisma/schema.prisma (исходник) НЕ содержит поля, а node_modules/.prisma/client/schema.prisma (сгенерированный клиент в сборке) содержит. Утекли ровно ТРИ поля одной таблицы: WebhookDedupe.fencingToken, .nextAttemptAt, .rawEvent — это поля из ветки wave/e3webhook (P1.6 fencing), которая в l78 НЕ входит. Механизм утечки: сборка шла на A1, где рядом в других деревьях работали волны, и через ОБЩИЙ pnpm store в сборку попал клиент, сгенерированный чужой веткой. Класс известен: «сборка ≠ исходники» (projection-hash-differs-build-vs-source), но здесь он ударил по схеме БД.
ЛЕЧЕНИЕ (выполнено): аддитивно добавлены три колонки — ALTER TABLE "WebhookDedupe" ADD COLUMN IF NOT EXISTS "fencingToken" TEXT / "nextAttemptAt" TIMESTAMP(3) / "rawEvent" JSONB. Ошибки в журнале прекратились, входящее голосовое owner-а прошло и расшифровалось (17:43:20).
МАСШТАБ ПРОВЕРЕН: сравнение всех моделей обеих схем — расхождений больше нет (3 поля в сборке против 0 в исходнике, обратных нет).
ЧЕГО НЕ ХВАТИЛО В ПОСАДКЕ: постпосадочная проверка платных путей делалась (paid gate, Совет, индексация), но КАНАЛ СВЯЗИ С OWNER не проверялся ни разу. Добавить в чек-лист посадки: после переключения отправить себе тестовое сообщение через боевой мессенджер и убедиться, что оно долетело в MessageLog.
СЛЕДУЮЩИЕ ШАГИ: (1) сборки перенесены на новую машину A2 (чистая, чужих деревьев нет); (2) нужен гейт перед упаковкой: сравнить набор полей prisma/schema.prisma и node_modules/.prisma/client/schema.prisma, при расхождении — не выпускать артефакт.
Лента
2026-09-03T16:16:10.054Z · coordinator03.09 21:45 IST. Взят в работу впервые: волна 1580-mebridgemedia идёт на A2 (старт 16:14:14Z). В бриф вошёл боевой тест owner-а от 30.08 22:31 (скриншот прислан двумя способами — сжатым фото и файлом-документом с текстом-сопровождением; текст дошёл, обе картинки нет; MessageLog за 25 минут только text и voice, свежих Source с картинками нет, в fable-direct-inbox.log медиа не упоминается) и гипотеза тикета о расхождении обещания баннера me-mode с реальным ingest-путём. Требования: найти точку потери по коду с файлами и строками на каждом шаге; реализовать приём фото и документов (скачивание, лимиты размера и MIME, запись в Source и MessageLog, связывание с подписью); сделать отказ ВИДИМЫМ — молчаливая потеря запрещена; тесты на фикстурах Telegram-апдейтов (фото с подписью, документ с подписью, документ без подписи, слишком большой файл, битая ссылка); если тип принципиально не поддерживается — править текст баннера в этом же круге через messages/*.json.
2026-09-03T17:53:19.156Z · coordinatorПервый круг сдан: коммит d087ed63 «Fix Telegram photo and document me-bridge intake». Приёмка = волна 1588-accmebridgedisp на A2: фото, документ и медиа-группа отдельными тестами с проверкой размера/имени/типа, сохранение подписи, честная ошибка вместо тихой потери.
2026-09-03T19:36:44.836Z · coordinator[Пост-QA l97, 03.09 ~20:35 IST] Релиз: прод = l97 9d69d2a02201f697d7fb3d04a0b58c4b2e9b1a02 (переключён 19:20Z). Живые проверки: /api/health 200, /api/ready/paid 200, /api/ready sourceCommit=9d69d2a0, SIP 4080 status=ready + stasis connected=true. Журнал с 19:20Z: новых классов ошибок НЕТ, только 2 записи TrustedIdentityError на неавторизованных запросах (граница безопасности, класс существовал и на l96). ERR_MODULE_NOT_FOUND (WEB-500): 0 записей — но окно наблюдения всего ~10 минут и в сравнительном окне l96 17:00-19:00Z тоже 0, так что это НЕ доказательство исправления, класс был привязан к конкретному трафику. Линия ПОСТАВЛЕНА в прод. Расписка по diff l96→l97: новый src/lib/messaging/telegramMeBridgeMedia.ts (241 строка), новый src/lib/messaging/meBridgeMediaLocale.ts (25 строк), правки src/lib/messaging/handlers/inboundFileHandler.ts, src/lib/me-bridge/webHandlers.ts, src/lib/telephony/meBridgeAttachments.ts и meBridgeAttachmentMarker.ts, тесты tests/unit/telegram-me-bridge-media.test.ts (193 строки) и tsconfig.mebridgemedia-scoped.json. Маршруты me-bridge в собранном релизе на месте (poll, recent, realtime-token, realtime-tool, web-upload, web-enqueue). ЧЕСТНОЕ ОГРАНИЧЕНИЕ: сквозная проверка «фото из Telegram доезжает» требует отправки реального сообщения в бот — это действие наружу, я его не выполнял. Нужен либо owner, либо отдельная волна с тестовым аккаунтом.
2026-09-08T11:16:55.944Z · coordinator[08.09 11:16Z координатор] **GO, ждёт независимой приёмки.** волна 2771: фото и документы из Telegram больше не теряются — добавлен getFile → скачивание → сохранение как Source; неподдерживаемый тип теперь ВИДИМЫЙ отказ, а не молчаливый дроп
Вершина кандидата `866ddabb165b`, база — боевая линия l113s `49bc58dcc508c612b337c92af7d7d5bb71c7a681`.
Приёмка поставлена волной из пачки 2787–2794 (A2). Посадка — только после её вердикта: вердикт авторской волны это самооценка, а не ворота.
2026-09-08T13:20:47.543Z · coordinator[08.09 13:20Z координатор] Статус → **review**. принято независимой приёмкой и вошло в собранный артефакт линии l113t `a45e820c` (сборка PACK_OK `375a98c6`, ждёт посадки на прод). Кандидат `866ddabb`, приёмка ACCWEB450 = GO.
2026-09-08T14:00:36.180Z · coordinator[08.09 14:00Z координатор] Статус → **done**. фото и документы из Telegram сохраняются; неподдерживаемый тип даёт видимый отказ вместо молчаливого дропа
Посажено на прод линией **l113t** `a45e820c7ec1043fe601974fe24fe5026eaaf118` (артефакт `me2-standalone-linux-arm64-a45e820c-20260908T131657Z`, sha256 `375a98c6…`), флип 08.09 13:59Z.
Проверено после флипа, а не по факту деплоя: оба бэкенда `ready=true` и `paidReady=true` с новым releaseId; корень витрины `https://app.sixbyy.com/` отдаёт 200 (проверяем отдельно — после l113j он 18 минут отдавал 500); вебхук Telegram отвечает 401, как и должен (после l113k все lease-пути отдавали 500); готовность SIP на 4080 = 200, строк `LEASE_LOST` после рестарта — ноль.
Откат при необходимости: `l113s` `49bc58dcc508c612b337c92af7d7d5bb71c7a681`, релиз `arm64-l113s-20260908T081706Z`.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-450","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-08T14:00:36.176Z