[2026-09-29] eagle: personal/plans/mm-tool-progress-thread-anchor.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-29 14:07:01 +06:00
1 parent b4f7ff4a30
commit 6011f9d69a
1 file changed
+120
@@ -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/<ts>-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`) |