From 6011f9d69a3afe8fcc3235bbaa62f9f7175134ae Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 29 Sep 2026 14:07:01 +0600 Subject: [PATCH] [2026-09-29] eagle: personal/plans/mm-tool-progress-thread-anchor.md --- .../plans/mm-tool-progress-thread-anchor.md | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 personal/plans/mm-tool-progress-thread-anchor.md diff --git a/personal/plans/mm-tool-progress-thread-anchor.md b/personal/plans/mm-tool-progress-thread-anchor.md new file mode 100644 index 00000000..5b0f4ffb --- /dev/null +++ b/personal/plans/mm-tool-progress-thread-anchor.md @@ -0,0 +1,120 @@ + +# План: tool-progress бабблы в тред (Mattermost) + +> Задача Alex 2026-09-29: «надо починить MM интеграцию: tool calls сыпятся +> в основной канал а не в тред». Статус: **ПЛАН, ждёт подтверждения**. + +## Симптом + +Alex пишет `@whale` в треде → **финальный ответ приходит в тред правильно**, +а **бабблы tool-progress (🔍/⚙️/emoji + имя тула) падают в корень канала**. + +Доказательство из `~/.hermes/hermes-whale/logs/gateway.log` (2026-09-29): + +``` +13:34:11 inbound message: platform=mattermost user=admin chat=a6nyh77eyjbn5c1owh1i8eebee msg='Почему' +13:35:09 response ready: platform=mattermost chat=a6nyh77eyjbn5c1owh1i8eebee ... response=805 chars +``` + +Session key: `agent:main:mattermost:channel:a6nyh77eyjbn5c1owh1i8eebee:pcoiddzrqty3ipqtozgcs9ttte` +→ **тред-часть в session_key есть** (`pcoiddzrqty3ipqtozgcs9ttte`), значит финал +адресуется верно. Проблема только в progress-канале. + +## Причина (найдена в коде) + +`gateway/run.py`, строки **17153-17170** — расчёт анкора для progress-бабблов: + +```python +if source.platform in (Platform.SLACK, Platform.MATTERMOST): + _progress_thread_id = source.thread_id or event_message_id +else: + _progress_thread_id = source.thread_id +_progress_metadata = (...) if _progress_thread_id else None +_progress_reply_to = ( + event_message_id + if source.platform in (Platform.FEISHU, Platform.MATTERMOST) + and source.thread_id and event_message_id + else None +) +``` + +🔴 **Ключевая строка — `_progress_reply_to`:** требуется `source.thread_id`. +Но `source.thread_id` заполняется **только когда триггер-сообщение само внутри треда**. +Когда `@whale` написан в корне канала (`root_id` пустой) — `source.thread_id` = `None` +→ `_progress_reply_to = None` → `adapter.send(..., reply_to=None)` → **пост уходит +новым корневым постом канала**. + +При этом `_progress_metadata` считается по другой ветке (`thread_id or event_message_id`) +и **тоже может получать якорь** — то есть два поля рассинхронизированы: +metadata иногда есть, reply_to нет. + +`_send_progress_text()` (строка 17273) передаёт оба: +```python +result = await adapter.send( + chat_id=source.chat_id, + content=text, + reply_to=_progress_reply_to, # ← здесь None + metadata=_progress_metadata, +) +``` + +MM-адаптер в `send()` (строка 290) уважает анкор только так: +```python +if reply_to and self._reply_mode == "thread": + resolved_root = await self._resolve_root_id(reply_to) + payload["root_id"] = resolved_root +``` +→ при `reply_to=None` ветка не срабатывает вообще. **`MATTERMOST_REPLY_MODE=thread` +уже стоит** (`.env`), т.е. адаптер готов — до него не доезжает анкор. + +## Сравнение с остальными платформами + +| Платформа | Правило анкора progress | Ок? | +|---|---|---| +| Slack | `thread_id or event_message_id` для metadata | так же как MM | +| Feishu | `reply_to` только при `thread_id` | тот же дефект | +| **Mattermost** | metadata ← `thread_id or event_message_id`, **reply_to ← только `thread_id`** | 🔴 рассинхрон | + +## Предлагаемый фикс (1 строка + симметрия) + +Выровнять `_progress_reply_to` под ту же логику, что metadata: +опираться на уже вычисленный `_progress_thread_id`, а не на сырой `source.thread_id`. + +```python +_progress_reply_to = ( + event_message_id + if source.platform in (Platform.FEISHU, Platform.MATTERMOST) + and _progress_thread_id and event_message_id + else None +) +``` + +**Что меняется:** при посте в корне канала `_progress_thread_id = event_message_id` +→ `_progress_reply_to = event_message_id` → адаптер резолвит root и кладёт +progress-баббл **в тред, созданный от этого поста**. При посте внутри треда +поведение не меняется (`source.thread_id` уже непустой). + +⚠️ **Не трогаю:** `reply_mode`, `REQUIRE_MENTION`, логику меншн-гейта (патч `9b46ba791a`), +`edit_message`-путь (там metadata передаётся отдельно, строка 17243). + +## Шаги + +1. Бэкап `gateway/run.py` → `backups/-mm-thread-progress/run.py` (+ md5). +2. Правка строки 17166-17170 в `run.py` (скриптом в файле, не sed/inline). +3. `pytest tests/gateway/ -k mattermost` + `tests/gateway/test_stream_events.py` если есть. +4. Прогон шире: `pytest tests/gateway/ -q -o 'addopts='`. +5. Коммит `fix(mattermost): anchor tool-progress bubbles to thread`. +6. **Alex рестартует gateway** (`gateway run --replace` из моей сессии убьёт её же). + +## Приёмка (Alex) + +Пишу `@whale` в корне канала `hermes-maintenance` → **и финальный ответ, +и бабблы tool-progress видны в треде** под моим постом, в корне — ничего лишнего. + +## Риски + +| Риск | Оценка | +|---|---| +| Прогресс уедет в тред там, где Alex ждал в корне | низкий — при `reply_mode=thread` это и есть желаемое | +| Поломка Slack/Feishu | низкий — правка в общей ветке, для них `_progress_thread_id` считается иначе; проверить тестами | +| Регресс в approval-промптах | нет — у них свой путь (`send_exec_approval`) |