From 7f6c941ad9acf9a7c38acc944565e5c0af08e734 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 21 Jun 2026 11:26:20 +0600 Subject: [PATCH] [2026-06-21] eagle: family/how-to/htpc-emulators-setup.md personal/projects/balda/balda-valera.md personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md personal/projects/hermes/no-edit-segment-send-bug.md --- family/how-to/htpc-emulators-setup.md | 28 +++ personal/projects/balda/balda-valera.md | 60 ++++++ ...ermes-gateway-stream-consumer-text-loss.md | 195 ++++++++++++++++++ .../hermes/no-edit-segment-send-bug.md | 64 ++++++ 4 files changed, 347 insertions(+) create mode 100644 personal/projects/hermes/hermes-gateway-stream-consumer-text-loss.md create mode 100644 personal/projects/hermes/no-edit-segment-send-bug.md 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/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/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 вызова терминала + промежуточные фразы — все сообщения дошли до пользователя.