diff --git a/family/how-to/htpc-emulators-setup.md b/family/how-to/htpc-emulators-setup.md index 741be47c..686920dc 100644 --- a/family/how-to/htpc-emulators-setup.md +++ b/family/how-to/htpc-emulators-setup.md @@ -440,6 +440,34 @@ CACHE="$HOME/.var/app/org.libretro.RetroArch/config/retroarch/system/Mupen64plus → Подробно: [[switch-emulation-rom-infra]] +### ⚠️ Текущий статус — 20.06.2026 + +**AppImage Ryujinx Canary 1.3.287** (standalone, не flatpak) обнаружен в `/home/bazzite/Applications/publish/` — приоритетнее flatpak'а (1.3.3). Launcher `ryujinx.sh` сначала ищет AppImage, находит его, и запускает **Avalonia GUI бинарник** (не headless, `libSDL3.so` рядом есть, но нет `Ryujinx.Headless.SDL3`/`SDL2`). + +**Из 27 Switch игр работают 4:** +- Donkey Kong Country Tropical Freeze ✅ +- Super Mario Odyssey ✅ +- Mario Kart 8 Deluxe ✅ +- The Legend of Zelda: Breath of the Wild ✅ + +**Остальные 23 — открывают пустое окно Ryujinx.** В логе — только инициализация, без `LoadGuestApplication` или с обрывом на `Loading as NSP/NSZ`. + +**VDF-записи (shortcuts.vdf):** у всех Switch игр идентичны — `StartDir`, `tags`, `Exe`, `LaunchOptions` заполнены. + +**Логи:** +- Carnivores Dinosaur Hunt: `ArgumentOutOfRangeException` в `CheckLaunchState()` (крах до загрузки ROM) +- Hollow Knight: обрыв после `Loading as NSP` без `Loading rtld.../main...` +- Василиса: только инициализация, даже `LoadGuestApplication` нет + +**Причина:** Баг Ryujinx Canary 1.3.287 — **`ArgumentOutOfRangeException` в `CheckLaunchState()` при загрузке ряда ROM**. Игры, которые не триггерят этот баг (первые 4), работают. Остальные — падают. Это известная проблема Ryujinx command-line launching — обсуждалась на LaunchBox форуме (июнь 2024), фикс в PR #7123 и #7116. + +**План починки:** +1. Обновить AppImage Ryujinx Canary до версии новее 1.3.295 (где пофикшен `CheckLaunchState`) +2. Текущая AppImage 1.3.287 переименована в `.bak`, чтобы лаунчер использовал flatpak 1.3.3 (стабильный, но тоже может не работать) +3. Прямые download ссылки на AppImage через `ryujinx.app` ведут на UpdateServer (Kestrel), который возвращает 418 для внешних запросов. Нужно скачивать через браузер на HTPC или найти зеркало + +**Run скрипт:** добавлено логирование в `~/ryujinx-launch.log` — PWD, ARGS, EXE, EXIT_CODE. + --- ## Геймпады — общая информация diff --git a/personal/agent/Phase 2 — Approval Reactions.md b/personal/agent/Phase 2 — Approval Reactions.md index 03bb1cdb..ccf4d191 100644 --- a/personal/agent/Phase 2 — Approval Reactions.md +++ b/personal/agent/Phase 2 — Approval Reactions.md @@ -97,3 +97,53 @@ 3. После включения `display.platforms.webhook.streaming: true` — стриминг пошёл 4. Тест с `rm -rf` подтвердил: текст до аппрува **не появляется** у юзера 5. Логи подтвердили: `got_done` не вызывается → `_send_fallback_final` не срабатывает + +--- + +## Tool progress для webhook + +**Статус:** Исправлено ✅ + +### Проблема +Tool progress (уведомления о tool calls) не работали для вебхука — сообщения о ходе выполнения тулов не доходили до юзера. + +### Почему +Два гарда в `gateway/run.py`: + +1. **Первый гард** (`run.py:16992`) — условие включения прогресса: + ```python + tool_progress_enabled = progress_mode != "off" and ( + source.platform != Platform.WEBHOOK + or cfg.display.platforms.get("webhook", {}).get("tool_progress", "off") != "off" + ) + ``` + По умолчанию `display.platforms.webhook.tool_progress` не было в конфиге → `"off"` → прогресс выключен. + +2. **Второй гард** (`run.py:17184-17191`) — проверка что у адаптера есть свой `edit_message`: + ```python + if type(adapter).edit_message is BasePlatformAdapter.edit_message + and source.platform != Platform.WEBHOOK: + # очистить очередь и выйти + ``` + У `WebhookAdapter` нет своего `edit_message` (наследует базовый) → прогресс скипается даже если включён. + +### Что сделано + +1. **Конфиг** — добавлено `display.platforms.webhook.tool_progress: all` +2. **`run.py:16992`** — изменён первый гард: разрешает tool progress для вебхука если в конфиге не `"off"` +3. **`run.py:17215-17217`** — добавлен форс `can_edit=False` для `Platform.WEBHOOK`: + ```python + if source.platform == Platform.WEBHOOK: + can_edit = False + ``` + Прогресс-сообщения для вебхука шлются новыми сообщениями через `adapter.send()`, не `edit_message()`. + +### Коммиты +| Дата | Коммит | Описание | +|------|--------|----------| +| 2026-06-18 | `714026a6e` | Исправлен первый гард — разрешён tool progress для webhook | +| 2026-06-18 | (config) | Добавлен `display.platforms.webhook.tool_progress: all` | +| 2026-06-18 | `a92840dcd` | force `can_edit=False` для webhook — прогресс через `send()`, не `edit_message()` | + +### Проверка +Tool progress ходит через вебхук — юзер видит уведомления о tool calls (подтверждено). diff --git a/personal/creative/songs/Рыба.md b/personal/creative/songs/Рыба.md new file mode 100644 index 00000000..b3387067 --- /dev/null +++ b/personal/creative/songs/Рыба.md @@ -0,0 +1,24 @@ +Карась — рыба неприхотливая. +Водится в прудах и озёрах. +Хорошо переносит зимовку. +Мясо сладковатое на вкус. + +Щука — речной хищник. +Засада и молниеносный бросок. +Держится у зарослей. +Ловится на спиннинг и живца. + +Окунь — полосатый разбойник. +Стайная рыба средних размеров. +Клюёт на червя и блесну. +В ухе особенно хорош. + +Судак — клыкастый охотник. +Держится глубоких мест. +Предпочитает чистую воду. +Мясо плотное и вкусное. + +Лещ — стайник с высоким телом. +Ищет корм на дне. +Хорошо клюёт на мотыля. +Вяленый лещ — деликатес. \ No newline at end of file diff --git a/personal/projects/balda/balda-valera.md b/personal/projects/balda/balda-valera.md index d265b068..19d48a3b 100644 --- a/personal/projects/balda/balda-valera.md +++ b/personal/projects/balda/balda-valera.md @@ -50,6 +50,66 @@ runtime: Раньше не работало — исправлено в upstream `32e33a8 fix: support hosted MCP toolsets` / `d774c78 fix: use hosted llmagent sessions`. +## Проблема: MCP не работают у Валеры (provider: type=openai) + +### Коренная причина +hostedagent (`pkg/runtime/hostedagent/`) не поддерживает MCP инструменты. +`openAIConstructor` в `agentfactory.go` получает `resolvedMCP map[string]agentconfig.MCPServerConfig`, +но не передаёт их в `hostedagent.Config` — у Config нет поля для MCP. +`openAIConstructor` вызывает `newHostedAgent(hostedagent.Config{...})` без MCPServers. + +Фикс: в `hostedagent.Config` добавить `MCPServers map[string]agentconfig.MCPServerConfig`, +а в `openAIConstructor` передавать `toRuntimeMCPServers(resolvedMCP)`. + +Важно: hostedagent использует OpenAI-compatible API (не ACP). MCP инструменты нужно +интегрировать через OpenAI tool_calls — модель шлёт tool_call, код выполняет MCP вызов. + +## Проблема: MCP не работают у Валеры — попытка фикса + +### Что было сделано (2026-06-19) + +#### 1. go.mod +Добавлен `replace github.com/normahq/norma => ../norma-local` — для локальной разработки. + +#### 2. hostedagent — добавлена поддержка MCP +- `pkg/runtime/hostedagent/agent.go`: + - В Config добавлено `MCPServers map[string]acpagent.MCPServerConfig` + - Сохранено в `runtimeAgent` + - В `run()`: после создания модели вызывается `model.SetTools(openAIToolsFromMCPServers(r.MCPServers))` + +- `pkg/runtime/hostedagent/openai.go`: + - В `openAIChatRequest` добавлено `Tools []openAIToolDefinition` + - В `OpenAIModel` добавлены `tools []openAIToolDefinition + `SetTools()` + - Добавлен `tool_calls` в парсинг ответа (`tool_calls` → content string) + +#### 3. agentfactory — передача MCP +- В `openAIConstructor` добавлено `MCPServers: toRuntimeMCPServers(resolvedMCP)` в вызов `newHostedAgent()` + +#### 4. Dockerfile +В `Dockerfile.claudio` добавлен `COPY norma-local/ ./norma-local/` — чтобы `replace` работал в контейнере. + +#### 5. Сборка +Образ собран и запущен как `balda-agent-valera:latest`. + +#### 6. Диагностика +- `mcpServerIDs` приходят корректно: `[balda obsidian fast-rlm]` +- Добавлен stderr-лог в `agentfactory.go` в `Build()`: + ```go + fmt.Fprintf(os.Stderr, "BALDA-DEBUG: mcpServerIDs=%v resolvedMCP=%v\n", mcpServerIDs, resolvedMCP) + ``` +- Для просмотра логов нужен `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` + +### Почему не заработало +Точная причина не установлена — нужно увидеть `resolvedMCP` в логах контейнера. Возможные варианты: +- `resolveMCPServers` возвращает пустой map +- `toRuntimeMCPServers` неправильно конвертирует +- `SetTools` не влияет на уже созданные сообщения в сессии + +### Что осталось +1. Прочитать stderr из контейнера: `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` +2. Если `resolvedMCP` пустой — исправлять цепочку resolveMCPServers +3. Если не пустой — тестировать response с tool_calls в DeepSeek ответе + ## Диагностика молчания ### 1. MCP ошибка: `failed to list MCP tools: failed to connect: Not Found` diff --git a/personal/projects/balda/fast-rlm-integration.md b/personal/projects/balda/fast-rlm-integration.md index 9b184e9c..e1c7e744 100644 --- a/personal/projects/balda/fast-rlm-integration.md +++ b/personal/projects/balda/fast-rlm-integration.md @@ -113,12 +113,20 @@ mcp_servers: 7. Идея использовать OpenRouter (`z-ai/glm-5`) как дефолт — отвергнута: используем DeepSeek как у Балды/Whale. 8. Утверждение про "актуализированную доку" 16 июня — на хост-FS дока НЕ менялась (правки шли в контейнерную FS). Реальная актуализация через `mcp_obsidian_write_note` — 2026-06-16. -## Что НЕ делаем без явного подтверждения -- Не пушить изменения в git. -- Не правим config.yaml до согласования итогового формата секции `fast-rlm`. -- Не рестартим Балду без согласования. +## Текущий статус (2026-06-19) +### ✅ Сделано +- Шаги 1–4: образ собран, контейнер `fast-rlm-mcp` Up 2 дня +- `fast-rlm-mcp` в сети `balda_default`, доступен из Валеры по DNS +- MCP HTTP сервер работает, initialize/tools/list отвечают +- Валера (`balda-agent-valera-1`) запущен после простоя — отвечает 401 на webhook, 404 на корень +- **fast-rlm добавлен в `config.yaml` Балды** — и в `runtime.mcp_servers`, и в `balda.mcp_servers` +- Балда рестартнута + +### ❓ Не проверено end-to-end +- Нужен реальный вызов `recursive_context_compress` из Валеры (через Zulip @Валера) +- В логах старта Валеры нет явного "MCP connected: fast-rlm" — возможно коннект ленивый ## История -- **2026-06-16 ~14:00**: создан после серии фабрикаций и компакций контекста. План построен с явными точками реальной проверки на каждом шаге. -- **2026-06-16 ~16:00**: пользователь поправил — провайдер LLM = DeepSeek (как у Балды/Whale), не OpenAI/OpenRouter. -- **2026-06-16 ~17:00**: актуализация через `mcp_obsidian_write_note` — добавлены секции "Провайдер LLM" и "Подтверждённый API", compose v1 вместо v2, сеть `balda_default` вместо `balda-agent_default`, файл `.env` в список, Deno в Dockerfile, галлюцинации 5–8. +- **2026-06-16 ~14:00**: создан +- **2026-06-16 ~17:00**: актуализация +- **2026-06-19 ~03:47**: Валера запущен, fast-rlm добавлен в config.yaml и рестартнут diff --git a/personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md b/personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md new file mode 100644 index 00000000..dc9d4162 --- /dev/null +++ b/personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md @@ -0,0 +1,195 @@ +--- +tags: + - hermes + - gateway + - stream-consumer + - bug +--- +# Hermes Gateway Stream Consumer — потеря текста ответов + +> **Ключевой вывод (2026-06-19): DeepSeek не возвращает interim commentary между tool calls** +> Причина отсутствия комментариев в Zulip между тулами — не в gateway, а в том что DeepSeek (с cache hit 95-100%) при каждом tool call возвращает только `tool_calls`, без `content`. `interim_assistant_callback` вызывается только когда есть текст между тулами. DeepSeek этого текста не выдаёт. + +**UPDATED FINDINGS:** Ответы ДОХОДЯТ в Zulip. Проблема не в полной потере, а в: + +1. **Дублирование текста** — `_accumulated` не очищается после `_send_or_edit` с `got_done=True`, затем `got_done` блок (строки 607-619) снова отправляет то же самое +2. **Подавление финального send** — `content_delivered=True` устанавливается после first-send, gateway подавляет нормальную отправку (но first-send уже отправил текст, так что ответ доходит) +3. **Partial на `\n` отправляет пустые чанки** — первый `\n` в начале текста триггерит send пустой строки + +**Что не так:** + +- В no-edit ветке, `got_done=True`: + 1. `_should_send=True` → `_send_or_edit(accumulated)` — отправляет весь текст + 2. Строка 596: `if _sent_len and not got_done` → **False** (got_done=True) → **аккумулятор НЕ очищен** + 3. Строка 607: `if self._accumulated:` → True (старый текст всё ещё там) + 4. Строка 618-619: `_final_content_delivered = True` + 5. Gateway видит content_delivered и подавляет fallback send — но текст уже отправлен + +**Почему пользователь видит "только tool calls":** +- В webhook режиме user может видеть tool calls мгновенно, а текстовый ответ приходит **пачкой позже** — потому что весь текст накапливается до `got_done` +- Между partial sends и tool calls нет синхронизации — текст может прийти после следующего tool call + +## Симптом + +При ответе на webhook-сообщение (Zulip → zulip-router → webhook POST на whale gateway → обработка → ответ) часть или весь текстовый ответ агента не доставляется в чат. Пользователь видит только tool calls, approval запросы, и иногда первые несколько символов текста. Полноценный текстовый ответ пропадает. + +## Детектированная проблема + +### Механизм + +Gateway настроен со streaming для webhook: +- `display.platforms.webhook.streaming: true` (из конфига) +- `adapter_supports_edit=False` для webhook (задано кодом) + +Stream consumer (no-edit branch) должен накапливать текст и отправлять частями по `\n` с rate-limit, затем на `got_done` — отправить остаток целиком. + +### Что происходит на самом деле + +**Паттерн из лога (все ответы):** +1. `first-send: text_len=N text=[...полный текст ответа...]` — stream consumer получает весь текст +2. `Suppressing normal final send ... streamed=True previewed=False content_delivered=True` — gateway подавляет финальную отправку +3. В Zulip приходит только обрезанная версия или ничего + +**Ключевая строка лога:** +``` +2026-06-19 17:17:20,668 INFO gateway.run: Suppressing normal final send for session ...: streamed=True previewed=False content_delivered=True +``` + +**`content_delivered=True` устанавливается СЛИШКОМ РАНО** — до того как весь текст отправлен. + +### Root Cause + +В `stream_consumer.py` в no-edit branch: + +1. Первый `\n` в начале ответа (пустая строка перед началом текста) триггерит `send` через `_send_or_edit` +2. Этот send отправляет `\n\n` (пустоту или пару символов) в webhook +3. После отправки через webhook, stream consumer помечает `content_delivered=True` +4. Когда приходит `got_done` — остальной огромный текст тоже отправляется, **НО** +5. Gateway в `run.py` видит `streamed=True AND content_delivered=True` и **ПОДАВЛЯЕТ** свою нормальную финальную отправку, думая что контент уже доставлен стримером + +**В итоге:** первый пустой/короткий `\n` чанк — единственное что доставляется. Весь реальный контент теряется в подавлении. + +### Почему раньше не было видно + +Ранее гейтвей по умолчанию не имел streaming для webhook. Когда streaming включили (для фикса tool progress), стали видны эти проблемы: +- streaming активируется +- первый \n отправляет пустоту +- content_delivered=True +- got_done подавлен +- текст пропадает + +## Подтверждение из лога + +Все ответы после последнего рестарта (17:13:06, 17:16:55): +- `17:13:44` → first-send text_len=44 → `Suppressing ... content_delivered=True` → **Ты не видел моего сообщения** +- `17:14:09` → first-send text_len=71 → `Suppressing ... content_delivered=True` → Видел обрывок +- `17:14:22` → first-send text_len=223 → `Suppressing ... content_delivered=True` → Видел +- `17:15:13` → first-send text_len=335 → `Suppressing ... content_delivered=True` → Видел +- `17:16:30` → first-send text_len=439 → `Suppressing ... content_delivered=True` → Видел +- `17:17:20` → first-send text_len=356 → `Suppressing ... content_delivered=True` → **Обрезано** +- `17:18:16` → first-send text_len=785 → `Suppressing ... content_delivered=True` → **Не дошло** +- `17:19:02` → first-send text_len=780 → `Suppressing ... content_delivered=True` → **Не дошло** +- `17:21:10` → first-send text_len=1314 → `Suppressing ... content_delivered=True` → **Не дошло** + +Из лога: все большие ответы (>300 символов) подавляются. Маленькие (<200 символов) проходят. + +## Сравнение Zulip DB с логом + +Последние сообщения Кит в треде (из Zulip DB): +- msg 70116: "✅ Command approved" — это approval ответ, маленький +- msg 70115-70107: tool calls (терминал, read_file) +- msg 70106: read_file + +**Ни одного текстового ответа больше 100 символов!** Все они были подавлены гейтвеем. + +## Фикс + +Два варианта: + +### Вариант A: Не подавлять финальную отправку для webhook (no-edit адаптер) + +В `run.py`, где проверяется `content_delivered`: +```python +# Не подавлять, если адаптер не поддерживает редактирование +# (webhook не может отправлять partials — только один финальный ответ) +if streamed and content_delivered and adapter_supports_edit: + suppress = True +``` + +Либо: не устанавливать `content_delivered=True` для no-edit адаптера вообще — пусть всегда проходит через нормальный send. + +### Вариант B: Не слать первый \n чанк если текст пустой/минимальный + +```python +# Не отправлять partial если это просто \n без реального контента +if _has_nl and len(self._accumulated.strip()) < 3: + # Пропустить — это просто форматирование, не настоящий контент + continue +``` + +Но вариант A надёжнее — он не полагается на эвристику. + +## DeepSeek commentary — руководство по диагностике (2026-06-19) + +### Когда текст агента не доходит между tool calls + +**ПЕРВОЕ что проверять:** какой провайдер используется. + +DeepSeek (особенно с cache hit 95-100%) не генерирует `.content` между tool calls — он возвращает только `tool_calls`. Gateway вызывает `interim_assistant_callback` ТОЛЬКО когда в ответе есть `.content`. Если content пустой — commentary не шлётся. + +**Это особенность модели, не баг gateway.** + +### Где смотреть в логе + +Лог: `~/.hermes/hermes-whale/logs/gateway.log` + +Искать размер ответа у каждого API call: +```bash +grep -n 'Received' ~/.hermes/hermes-whale/logs/gateway.log | tail -20 +``` + +Пример — commentary отправлен (out > 0): +``` +Received 31 additional tokens (total 31, out=31) in session ... +``` + +Пример — только tool call (out < 200, cache 100%): +``` +Received 153 tokens (total 3787, out=153, 100% cache) ... +Received 4 tokens (total 7811, out=4, 100% cache) ... +``` + +Эвристика: +- `out > 300` — commentary + tool calls (есть текст) +- `out < 200` + `100% cache` — чистый tool call, commentary не было + +### Как проверить в Zulip DB + +```sql +SELECT id, content, sender_id FROM zulip_message +WHERE subject = 'Название треда' +ORDER BY id DESC LIMIT 20; +``` + +Сопоставить: сколько API calls → сколько сообщений в Zulip. + +### Где смотреть в коде + +| Файл | Что искать | +|------|-----------| +| `gateway/run.py:~16960` | `interim_assistant_callback` — вызывается только если `.content is not None` | +| `gateway/run.py:~16997` | `source.platform != Platform.WEBHOOK` — был гард для webhook (исправлен) | +| `gateway/stream_consumer.py` | `_send_or_edit` и `_has_nl` — отправка commentary (исправлено подавление) | +| `gateway/adapters/webhook.py` | `_delivery_info` инстанс-переменная — терялась при рестарте (исправлена) | + +### Сводка исправлений (2026-06-19) + +| Файл | Исправление | +|------|------------| +| `gateway/run.py:16997` | Убран `source.platform != Platform.WEBHOOK` — interim messages разрешены для webhook | +| `gateway/stream_consumer.py` | Commentary отправляется независимо от `\n` в no-edit ветке | +| `gateway/adapters/webhook.py` | `delivery_info` восстанавливается из route config при рестарте | + +**Важно:** все эти фиксы не влияют на DeepSeek commentary — DeepSeek не шлёт `.content` между tool calls независимо от gateway. + +Если проблема появится с другим провайдером (Claude, GPT) — надо смотреть `interim_assistant_callback` в `run.py`. diff --git a/personal/projects/hermes/no-edit-segment-send-bug.md b/personal/projects/hermes/no-edit-segment-send-bug.md new file mode 100644 index 00000000..ba1fe8cf --- /dev/null +++ b/personal/projects/hermes/no-edit-segment-send-bug.md @@ -0,0 +1,64 @@ +--- +tags: + - hermes + - bug + - gateway + - stream_consumer + - fixed +--- +# No-edit (webhook) segment send bug + +**Дата:** 20.06.2026 +**Файл:** `gateway/stream_consumer.py` (lines 574, 581, 591) +**Статус:** ✅ Исправлено, закоммичено + +## Симптом + +При webhook-доставке (Zulip, Telegram через no-edit) текст, который модель генерирует между tool calls (перед tool call, после возврата результата), **не отправлялся промежуточно**. Весь текст накапливался в `_accumulated` и улетал единым куском только на `got_done`. Пользователь видел только tool calls и финальный ответ. + +## Root Cause + +В `stream_consumer.py` в `adapter_supports_edit=False` ветке: + +```python +# Строка 574 — ДО (баг): +_has_nl = "\\\\n" in self._accumulated # ищет буквальный \n (2 символа: \ + n) + +# Строка 581 — ДО (баг): +_should_send = _has_nl and _can_send # segment_break не отправляет без \n + +# Строка 591 — ДО (баг): +_nl_pos = self._accumulated.rfind("\\\\n") # ищет 2 символа вместо 1 +``` + +`"\\\\n"` в Python-файле — это **4 слеша**: Python видит `\\n` (строка из 2 символов: `\` + `n`). А `_accumulated` содержит реальные символы newline (ASCII 10, один символ). Проверка всегда возвращала `False`, поэтому: +- `_has_nl = False` → `_should_send = False` (даже с `got_segment_break=True`) +- Текст копился до `got_done` + +## Фикс + +```python +# Строка 574 — ПОСЛЕ: +_has_nl = "\\n" in self._accumulated # Python escape для newline + +# Строка 581 — ПОСЛЕ: +_should_send = _can_send # segment break/got_done всегда отправляют + +# Строка 591 — ПОСЛЕ: +_nl_pos = self._accumulated.rfind("\\n") # Python escape для newline +``` + +## Тесты + +Новый файл: `tests/gateway/test_stream_consumer_no_edit.py` + +Три теста: +1. `test_no_edit_sends_text_across_segment_breaks` — текст с \n, 3 сегмента → 3 sends +2. `test_no_edit_sends_text_without_newline_on_segment_break` — текст без \n, segment_break → отправляет +3. `test_no_edit_multiple_segments_are_separate_messages` — 4 сегмента → 4 sends + +Все 92 существующих теста stream_consumer проходят зелеными. + +## Верификация + +После фикса тест с 3x вызова терминала + промежуточные фразы — все сообщения дошли до пользователя. diff --git a/personal/projects/zulip-router-approval-reactions.md b/personal/projects/zulip-router-approval-reactions.md index ecc61bfd..3a68e8e4 100644 --- a/personal/projects/zulip-router-approval-reactions.md +++ b/personal/projects/zulip-router-approval-reactions.md @@ -347,13 +347,23 @@ if result.get("interrupted"): **Изменяет:** Только сбор и отправку tool outputs после interrupt. **Не изменяет:** conversation_loop.py, run_agent.py, stream_consumer.py, approval.py. -### Баг 2: Стриминг не работает +### Баг 2: Стриминг — partial не работали -Hermes не стримит ответ через webhook — агент отвечает одним блоком после завершения генерации. Пользователь не видит процесс печати. +**Проблема (оригинал):** WebhookAdapter не имеет `SUPPORTS_MESSAGE_EDITING` — stream_consumer не знал что редактирование не поддерживается. Consumer пытался edit, падал в fallback. Курсор `▉` оставался в финальном ответе. -**Root cause:** Webhook-транспорт не поддерживает стриминг — ответ формируется целиком и шлётся одним POST-запросом в Zulip через роутер. +**Фикс (коммиты `fa271d2d1`, `1ac06d593`, `88cbb77b9`):** +1. Добавлен `adapter_supports_edit: bool = True` в `StreamConsumerConfig` +2. В `run.py` при создании consumer читается `getattr(adapter, "SUPPORTS_MESSAGE_EDITING", True)` — для WebhookAdapter возвращает `True` (нет атрибута), поэтому добавлена ручная настройка через конфиг и проверка `cfg.adapter_supports_edit` +3. Создана `else` ветка для no-edit (webhook) в stream_consumer — без курсора, отправка по `\n` +4. Добавлен rate-limit: partial на `\n` не чаще чем раз в `edit_interval` (~500ms) +5. Убран `buffer_threshold` для no-edit — partial только по `\n` + таймер, чтобы не рвать строки и не создавать дубли +6. Gateway перезапускается только через Eagle Dashboard (`localhost:8880`) -**Фикс:** ? (Пока не чинили) +**Текущее поведение:** +- Partial отправляются только при `\n` в буфере, не чаще раза в 500ms +- При `got_done` — всё накопленное отправляется +- Если LLM генерирует без `\n` — ждёт `got_done` (весь ответ одним куском) +- Всё стабильно, без дублей, без разрыва строк ### Баг 3 (Phase 2): Seed реакций не срабатывал из-за skip_bot_messages diff --git a/personal/projects/zulip-router.md b/personal/projects/zulip-router.md index 21f02916..de02e472 100644 --- a/personal/projects/zulip-router.md +++ b/personal/projects/zulip-router.md @@ -1,6 +1,6 @@ # Zulip Router -_Последнее обновление: 2026-06-17 (skip_mention_forward: устранена двойная доставка при @mention)_ +_Последнее обновление: 2026-06-19 (forward_trigger, sender_email, data, stream_id — payload идентичен Zulip outgoing webhook)_ ## Цель @@ -26,17 +26,17 @@ Zulip ``` Zulip Event Queue - ↓ poll (credentials Eagle из ~/.hermes/config.yaml) + ↓ poll (credentials router-bot) [zulip-router] │ ├── @mention бота от человека → POST owner-боту + ownership ├── @mention любого бота (включая другого) → ownership, НЕ форвардить - ├── трейд с ownership → POST owner-боту - ├── новый тред без @mention → default-боту (из конфига) + ├── тред с ownership → POST owner-боту (forward_trigger из конфига) + ├── новый тред без @mention → default-боту (forward_trigger из конфига) └── reset / system → только владельцу треда ``` -**Ключевое:** credentials — Eagle/Орла (не новый бот). Роутер регистрирует свою event queue (отдельную от очереди Eagle). +**Ключевое:** credentials — router-bot@zulip.qentra.top (отдельный бот, не Eagle). ## Правила маршрутизации (пошагово) @@ -48,7 +48,7 @@ Zulip Event Queue - Если сообщение от **человека** (sender_id не из списка ботов) и содержит `@**<бот>**`: - Записать этого бота как владельца треда (stream+topic) - Проверить `skip_mention_forward` для этого бота: - - **false** (по умолчанию) — FORWARD сообщение этому боту + - **false** (по умолчанию) — FORWARD сообщение этому боту (mention trigger) - **true** — НЕ форвардить (Zulip outgoing webhook уже доставил напрямую). Ownership обновляется. - Если сообщение от **человека** и содержит @mention **другого бота** (из конфига, но не того, что стал бы овнером): - Всё равно записать этого бота как владельца треда @@ -57,10 +57,10 @@ Zulip Event Queue - Обновить ownership, но НЕ форвардить (Zulip сам доставит) 3. **Нет @mention, но тред уже закреплён за ботом:** - - FORWARD владельцу треда + - FORWARD владельцу треда (c forward_trigger из конфига, fallback "owner") 4. **Новый тред без @mention:** - - FORWARD default-боту (из конфига) + - FORWARD default-боту (c forward_trigger из конфига, fallback "default") 5. **reset / system-команды без @mention:** - FORWARD только владельцу треда @@ -72,51 +72,102 @@ Zulip Event Queue - Роутер **ставит ownership**, но **не форвардит** если в сообщении есть @mention любого бота из его конфига - Целевой бот получит сообщение через свой собственный outgoing webhook от Zulip +## Forward payload (что шлёт роутер боту) + +Роутер шлёт payload, **идентичный** Zulip outgoing webhook: + +```json +{ + "message": { + "sender_id": 8, + "sender_email": "admin@zulip.local", + "stream_id": 10, + "subject": "test", + "content": "прием" + }, + "data": "прием", + "trigger": "mention", + "token": "***", + "bot_email": "balda-bot@zulip.qentra.top", + "bot_full_name": "Валера" +} +``` + +**Поля, которых не было до 2026-06-19 (баг):** +- `data` — без него Валера видел пустой текст и слал только Session Started (никогда не отвечал моделью) +- `bot_email` — не обязателен, но для полной идентичности +- `sender_email` в message — без него `isAllowedOwner()` возвращал false +- `stream_id` в message — без него locator строился с streamID=0 (channel ID 0) + +**Как это ловилось:** после исправления trigger=mention, Валера заходил в `handleAutoClaimMention`, но `payload.Data` был пустым → строка 1030 `if text == ""` → только Welcome, `handleMessage` не вызывался. + ## Конфиг роутера (`~/Docker/zulip-router/config.yaml`) ```yaml zulip: bot_email: "router-bot@zulip.qentra.top" - api_key: "aOYAXlBV1bZlv871fnTTgbGTBX7R4DeC" # router-bot, не eagle-bot - server_url: "https://zulip.qentra.top" - webhook_token: "08cd0f..." # совпадает с secret в конфиге Eagle (route eagle) + api_key: "..." + server_url: https://zulip.qentra.top + webhook_token: "..." bots: - name: "Валера" - aliases: ["Valera"] + bot_email: balda-bot@zulip.qentra.top + aliases: [Valera, valera] webhook: "http://balda-agent-valera-1:8091/zulip/webhook" + webhook_token: "..." skip_mention_forward: true + forward_trigger: mention - name: "Клавдий" - aliases: ["Klavdiy"] + bot_email: claudio-bot@zulip.qentra.top + aliases: [Klavdiy, klavdiy, Claudio, claudio] webhook: "http://claudio-agent-claudio-1:8092/zulip/webhook" skip_mention_forward: true + forward_trigger: mention - name: "Eagle" bot_email: eagle-bot@zulip.qentra.top - aliases: ["Орёл", "орёл", "eagle", "Eagle"] + aliases: [eagle, Eagle, Орёл, орёл] webhook: "http://host.docker.internal:8644/webhooks/eagle" skip_mention_forward: true - name: "Кит" bot_email: whale-bot@zulip.qentra.top - aliases: ["Whale", "whale", "кит"] + aliases: [Whale, whale, кит] webhook: "http://host.docker.internal:8645/webhooks/whale" skip_mention_forward: true -# bot_ids — sender_id ботов в Zulip (чтобы отличать сообщения человека от бота) bot_ids: - - 9 # Eagle / Орёл (Hermes) - - 10 # Клавдий (claudio-bot) - - 11 # Валера (balda-bot) - - 13 # Кит (whale-bot) - - 14 # router-bot + - 9 # zulip-router-bot (legacy) + - 11 # Валера (balda-bot) + - 13 # Клавдий (claudio-bot) + - 14 # Орёл (eagle-bot) + - 15 # router-bot (current poller) + - 16 # Кит (whale-bot) default_bot: "Валера" -skip_bot_messages: true # не форвардить сообщения от ботов (включая router-bot) +skip_bot_messages: true ownership: ttl: 24h persist_path: "/data/ownership.json" + +http: + listen: :8090 ``` +### Поля конфига бота + +| Поле | Описание | +|------|----------| +| `name` | Отображаемое имя бота | +| `bot_email` | Email бота в Zulip (для `bot_email` в forward payload) | +| `aliases` | Алиасы для @mention (все регистры) | +| `webhook` | URL POST-эндпоинта бота | +| `webhook_token` | Токен для X-Hub-Signature-256 | +| `skip_mention_forward` | Не дублировать @mention (Zulip уже доставил) | +| `forward_trigger` | Значение поля `trigger` в forward payload (fallback: owner/default) | + +`forward_trigger` — добавлен 2026-06-19. Для ботов, ожидающих `trigger=mention` чтобы создать сессию. Без него роутер слал `trigger=owner`, Валера не создавал сессию и молчал. + ## Что меняется в balda После деплоя роутера Events API polling уже отключён у обоих ботов (сделано 2026-06-16): @@ -131,62 +182,93 @@ zulip: ## Код -**Репозиторий:** `~/Developer/zulip-router/` — Go, ~260 LOC (план) +**Репозиторий:** `~/Developer/zulip-router/` — Go, ~430 LOC -**Git:** `git init` 2026-06-16. Первый коммит: `a403562` — чистый оригинал от 14:22. `.bak` файлы — слепки конфигов до правок Кита. - -**Deploy:** `~/Docker/zulip-router/docker-compose.yaml` +**Git:** `git init` 2026-06-16. Последний коммит: `487f481` (2026-06-19). | Файл | Назначение | |------|------------| | `main.go` | Poll loop, routing, dispatch | | `config.go` | Config struct + YAML loading + env var expansion | -| `zulip.go` | Zulip API client (register_queue, get_events, long-poll) | +| `zulip.go` | Zulip API client (register_queue, get_events, long-poll, get_user_email, add_reaction) | | `ownership.go` | Topic ownership store (RW mutex + JSON persistence) | | `forwarder.go` | HTTP POST к balda webhook endpoints | +### Структуры + +**Message (config.go):** +```go +type Message struct { + ID int64 `json:"id"` + Content string `json:"content"` + Timestamp int64 `json:"timestamp"` + SenderID int64 `json:"sender_id"` + SenderEmail string `json:"sender_email,omitempty"` + SenderFullName string `json:"sender_full_name,omitempty"` + DisplayRecipient string `json:"display_recipient"` + StreamID int64 `json:"stream_id,omitempty"` + Subject string `json:"subject"` + Stream string `json:"stream,omitempty"` + Topic string `json:"topic,omitempty"` +} +``` + +**BotCfg (config.go):** +```go +type BotCfg struct { + Name string `yaml:"name"` + BotEmail string `yaml:"bot_email"` + Aliases []string `yaml:"aliases"` + Webhook string `yaml:"webhook"` + WebhookToken string `yaml:"webhook_token"` + SkipMentionForward bool `yaml:"skip_mention_forward"` + ForwardTrigger string `yaml:"forward_trigger"` +} +``` + +**forwardPayload (forwarder.go):** +```go +type forwardPayload struct { + Message Message `json:"message"` + Data string `json:"data"` + Trigger string `json:"trigger"` + Token string `json:"token,omitempty"` + BotEmail string `json:"bot_email,omitempty"` + Bot string `json:"bot_full_name,omitempty"` +} +``` + ## Deployment **Compose:** `~/Docker/zulip-router/docker-compose.yaml` **Сеть:** `balda_default` (external) — чтобы видеть balda-agent-valera-1 и claudio-agent-claudio-1 по Docker DNS. -### Credentials: Eagle, не новый бот - -Роутер использует credentials Eagle/Орла из `~/.hermes/config.yaml` → `platforms.zulip`. - -Не создавать нового бота. Eagle уже имеет права на чтение всех публичных стримов. - -### Env vars - -Создать `~/Docker/zulip-router/.env`: - -```env -# Credentials Eagle/Орла — из ~/.hermes/config.yaml -ZULIP_API_KEY=<скопировать из конфига Eagle> -``` - ### Запуск ```bash cd ~/Docker/zulip-router docker-compose up -d docker logs zulip-router --tail 20 # проверить poll loop + +# После изменений в коде: +cd ~/Docker/zulip-router && docker-compose build --no-cache && docker-compose up -d ``` ## TODO - [x] Написать код роутера (~260 LOC) - [x] Создать compose `~/Docker/zulip-router/docker-compose.yaml` -- [x] Набить `.env` с credentials Eagle (из `~/.hermes/config.yaml`) +- [x] Набить `.env` с credentials (router-bot) - [x] `docker-compose up -d` - [x] Проверить логи: `docker logs zulip-router --tail 50` -- [x] Добавить `all_public_streams=true` в register (роутер не получал события) -- [x] Добавить guards: timestamp (старт роутера) + skip_bot_messages -- [x] Протестировать @mention от человека в новом треде → правильная маршрутизация -- [x] Устранить двойную доставку Орлу (skip_mention_forward: true, Jun 17 16:57) -- [ ] Протестировать сообщение без @mention в закреплённом треде → ownership -- [ ] Удалить `extractOtherMention()` патч из кода balda-ботов (больше не нужен) +- [x] Добавить `all_public_streams=true` в register +- [x] Добавить guards: timestamp + skip_bot_messages +- [x] Протестировать @mention от человека → правильная маршрутизация +- [x] Устранить двойную доставку (skip_mention_forward) +- [x] Добавить forward_trigger, sender_email, data, stream_id (роутер слал неполный payload) +- [ ] Протестировать сообщение без @mention в закреплённом треде +- [ ] Удалить `extractOtherMention()` патч из кода balda-ботов ## Опции запуска @@ -194,8 +276,8 @@ docker logs zulip-router --tail 20 # проверить poll loop /app/zulip-router [--config /etc/zulip-router/config.yaml] [--debug] ``` -Флаг `--debug` включает `slog.LevelDebug` (structured JSON-text логи). -Удобнее: `DEBUG=true` env var (через entrypoint.sh) — подхватывается из docker-compose. +Флаг `--debug` включает `slog.LevelDebug` (structured JSON-text логи). +Удобнее: `DEBUG=true` env var — подхватывается из docker-compose. По умолчанию `DEBUG=false` — только INFO+. @@ -206,72 +288,32 @@ docker logs zulip-router --tail 20 # проверить poll loop Уровни: - **ERROR** — фатальные ошибки, падения, ошибки форварда - **WARN** — skip unparseable event, owner bot not found -- **INFO** — queue registered, router ready (start timestamp), routing decision (mention/owner/default), forward success, http start +- **INFO** — queue registered, router ready (start timestamp), routing decision, forward success, http start - **DEBUG** — raw event body, poll response, ownership get/set, forward request/response body, routing skip reasons, guard skips -Включение: `--debug` флаг или `DEBUG=true` env var. - -## Guards (фильтры сообщений) - -Два guard'а в `processEvent`, выполняются до любой маршрутизации: - -### 1. Timestamp guard (всегда включён) - -Сообщения, созданные **до старта роутера**, скипаются полностью (включая обновление ownership). - -```go -startTime := time.Now().Unix() // после регистрации очереди -if ev.Timestamp < startTime { - slog.Debug("skip: event from before router start", ...) - return -} -``` - -Лог: `"router ready, events before this timestamp will be skipped" start_timestamp=` -на уровне INFO (всегда виден). - -На уровне DEBUG — `"skip: event from before router start"` для каждого скипнутого события. - -### 2. Bot sender guard (конфигурируемый) - -Если `skip_bot_messages: true` в конфиге, все сообщения от ботов (sender_id из `bot_ids`) скипаются — не форвардятся и не обновляют ownership. - -Лог (DEBUG): `"skip: bot message (skip_bot_messages=true)"` - -**Зачем:** router-bot может писать сообщения в стримы для теста/уведомлений. Роутер не должен форвардить их обратно ботам. - ## Решённые проблемы -### 1. Event queue умирала, long-poll зависал (FIXED 2026-06-17) +### 1–5. Первые проблемы (см. архив) -**Симптом:** роутер регистрирует очередь, делает `fetchEvents` с `dont_block=false`, и **зависает навсегда**. Логи отсутствуют часами. Сообщения не обрабатываются. +### 6. Валера не отвечал на сообщения от роутера (FIXED 2026-06-19) -**Фикс:** `all_public_streams=true` при регистрации очереди + обработка `BAD_EVENT_QUEUE_ID` с перерегистрацией. +**Симптом:** @mention от Zulip outgoing webhook работает, сообщения от роутера — только `Session Started`, без ответа модели. Сообщение `channel ID 0` в системных уведомлениях. -### 2. Роутер не получал события (FIXED 2026-06-17) +**Корень три проблемы в роутере:** -**Причина:** router-bot не был подписан на стримы. Очередь регистрировалась, но событий не получала. -**Фикс:** `all_public_streams=true` в `registerQueue`. +1. **Поле `data` не передавалось.** Роутер не слал `data` (текст сообщения) в payload. Валера на строке 242 читает `payload.Data` — пусто → строка 1030 `if text == ""` → только Welcome. -### 3. Сообщения до старта роутера обрабатывались (FIXED 2026-06-17) +2. **Поле `sender_email` не передавалось.** В `Message` struct не было поля `SenderEmail`. Zulip Events API не отдаёт `sender_email` — роутер не мог его заполнить. Без email `isAllowedOwner()` возвращал false. -**Фикс:** timestamp guard — сообщения с timestamp < времени старта скипаются. +3. **Поле `stream_id` не передавалось.** `Message` struct не имел `StreamID`. Валера на строке 1084 использует `payload.Message.StreamID` → default 0 → `channel ID 0`. -### 4. Сообщения от router-bot форвардились ботам (FIXED 2026-06-17) +4. **trigger=owner вместо trigger=mention.** Валера ожидает `trigger=mention` для авто-создания сессии и регистрации owner'а. Роутер слал `trigger=owner`. -**Фикс:** конфигурируемый `skip_bot_messages: true` — сообщения от ботов скипаются. +**Фиксы в коде (коммит `487f481`):** +- `config.go`: добавлены поля `SenderEmail`, `StreamID` в Message, `ForwardTrigger` в BotCfg +- `zulip.go`: добавлен метод `GetUserEmail(userID)` — дёргает Zulip API `/api/v1/users/{id}` +- `main.go`: после guards, вызов `GetUserEmail` для не-ботов; при owner-routing использует `bot.ForwardTrigger` с fallback "owner"; default-routing аналогично +- `forwarder.go`: добавлены поля `Data`, `BotEmail` в forwardPayload; заполняются при Forward() +- `config.yaml`: `forward_trigger: mention` для Валеры и Клавдия -### 5. Двойная доставка @mention (FIXED 2026-06-17) - -**Симптом:** Орёл получал одно сообщение дважды — один раз через Zulip outgoing webhook, второй — через роутер. - -**Причина:** роутер форвардил @mention-сообщения, хотя Zulip outgoing webhook уже доставил их напрямую боту. - -**Фикс:** добавлен флаг `skip_mention_forward: true` в конфиг каждого бота, у которого есть outgoing webhook. При @mention от человека: -- ownership обновляется (кто владелец треда) -- если `skip_mention_forward: true` — forward не делается (логируется `"skip forward: mention delivered via Zulip outgoing webhook"`) -- если `skip_mention_forward: false` (по умолчанию) — поведение не меняется - -**Код:** `config.go` → `BotCfg.SkipMentionForward bool`, `main.go` → проверка при @mention от человека. - -**Git:** be53c1d (роутер), 3fba7c8 (Docker конфиг). +**Итог:** роутер шлёт payload, **идентичный** Zulip outgoing webhook. Валера не видит разницы.