> [!WARNING] Историческая схема Balda > Не использовать пути, контейнеры, provider или MCP transport из этой заметки для текущего запуска. > Канонический текущий runbook: [[personal/projects/balda/setup|Balda — актуальный локальный запуск]]. > Актуально: `balda-mm-mac`, `/Users/admin/Docker/balda-mm-mac/config/config.yaml`, Mattermost, Codex ACP, Obsidian `type: http` → `http://host.docker.internal:3101/mcp`. # Балда / Валера — эксплуатация & MCP debug > ## ⛔ СНЕСЁН 2026-09-29 — агент удалён на Mac > > По команде Alex: «Валеру снеси на маке (он на старой балде работает, а мы нового готовим), > только не папку с кодом, а агента». > > | Объект | Действие | > |---|---| > | Контейнер `balda-agent-valera-1` | ✅ `docker-compose -f docker-compose.yaml down` → Removed | > | Образ `balda-agent-valera:latest` (`640cb18aaeeb`, 71.6 МБ) | ✅ `docker rmi` → Deleted | > | Порт **8091** | ✅ освобождён | > | Папка кода `/Users/admin/Developer/balda` | ✅ **СОХРАНЕНА** (не тронута) | > | Compose+конфиг `/Users/admin/Docker/balda-agent/` | ✅ **СОХРАНЕНЫ** (`.env`, `.config/balda/`, `docker-compose.yaml`) | > | `balda-executor` (2.48 ГБ образ) | ⚠️ **НЕ тронут** — это не Валера | > > 🔴 **Питфолл: `restart: unless-stopped`** — Валера поднялся бы сам при старте Docker. > Поэтому одного `docker stop` мало: нужен именно `compose down`. > > 🔴 **Питфолл: на этом хосте НЕТ плагина `docker compose`.** > `docker compose -f` → `unknown shorthand flag: 'f'`. Работает только отдельный бинарь > **`docker-compose`** (v5.1.3, `/opt/homebrew/bin/docker-compose`). Docker Engine 29.2.1. > > 📌 **Как восстановить, если понадобится** (образ придётся пересобрать — он удалён): > ```bash > cd ~/Docker/claudio-agent > docker build --no-cache -f Dockerfile.claudio -t balda-agent-valera:latest /Users/admin/Developer > cd ~/Docker/balda-agent && docker-compose up -d > ``` > Раздел §«Сборка» ниже описывает полный цикл (сборка из `~/Developer/norma-local`). > > **Замена:** новый стек Балды — `balda-mm:*` на Kraken со стороны Mattermost, > см. [[personal/projects/balda/balda-mm-delegation-plan]]. > ## ⚠️ УСТАРЕЛО (2026-09-25) — читать с оговоркой > Этот док описывает **старый форк-стек**: свой патч `norma-local` (`hostedagent/openai.go`), > `go.mod replace` на локальную норму, DeepSeek через OpenAI-совместимый рантайм. > > **Апстрим ушёл далеко вперёд.** Актуально: `normahq/balda` → **`baldaworks/balda`**, > архитектура переписана (`handlers/*` → `handlersfx/*`, Zulip → `channel/zulip/*`), > провайдеры теперь **ACP** (`claude_code_acp`, `codex_acp`, ...) вместо hostedagent/openai. > > Планируется поднять **чистую Балду без наших коммитов** → см. > [[personal/projects/balda/balda-mm-delegation-plan|balda-mm-delegation-plan]]. > > Что здесь всё ещё верно: грабли эксплуатации (NATS, owner, `DOCKER_OPTS`, `stream_only_with_session`), > бранчи/стэши, диагностика молчания. Что мертво: раздел про `openai.go`/`norma-local` replace как «текущее». > > --- > ### 🔴 ВАЖНАЯ ПОПРАВКА (2026-09-26): раздел про `openai.go` снова АКТУАЛЕН > > Вывод «патч `openai.go` мёртв» **опровергнут**. Апстрим переписал > `hostedagent/openai.go` под `runtime v2` и **потерял** фикс tool_calls целиком > (246 строк против наших 490) — то есть **та же дыра снова открыта**, и закрыть её > можно только тем же патчем. > > На Kraken это и есть причина «Балда не исполняет MCP-инструменты»: серверы подключены, > инструменты не вызываются. Разбор → [[personal/plans/balda-mm-phase4-agents]] §15. > Питфоллы патча (двойная сериализация аргументов DeepSeek, ключ `output`) → > [[personal/tech/balda-go-fx-pitfalls]] §«НАСТОЯТЫЙ корень». > > --- > ### ✅ ПАТЧ СДЕЛАН И ОТПРАВЛЕН В АПСТРИМ (2026-09-27) > > План выполнен целиком. Патч живёт в **новом месте** — `runtime v2`, не `norma`: > > | Что | Значение | > |---|---| > | Форк | `mallexxx/runtime`, ветка `feat/openai-tool-calls-v2` от тега `v2.0.11` | > | Коммит | `733b32d` | > | **PR в апстрим** | **https://github.com/normahq/runtime/pull/4** | > | Патч | `hostedagent/openai.go` +324, `openai_test.go` +315 (17 тестов) | > | Подключение в Балде | `replace github.com/normahq/runtime/v2 => ./runtime-local` в `~/Developer/balda/go.mod` | > | Клон форка | `~/Developer/runtime-local/` (отдельный git); **копия** для сборки — `~/Developer/balda/runtime-local/` (без `.git`) | > | `Dockerfile.release` | `COPY runtime-local/` **до** `go mod download` | > | Образ | `balda-mm:0.1.12` | > | `adk` API | форк на `adk/v2 v2.0.0` + `genai v1.57.0`; поля `FunctionCall{ID,Args,Name}` совпали со старым патчем — перенос 1:1 | > > 🔴 **Главный новый факт: патч вскрыл ВТОРОЙ баг.** На `0.1.12` каждый ход падает с > `duplicate tool: "balda.control.shutdown"`. Причина: апстримный `openai.go` жёстко ставил > `TurnComplete: true` — агент завершался после первого ответа, инструменты **не пакались вообще**. > Патч включил второй раунд → началась паковка → обнажилась двойная регистрация инструмента > (`Tools` + `Toolset` в `llmagent.Config`). Полный разбор → [[personal/plans/balda-mm-phase4-agents]] §16. > > 📌 **Как доказать, что код патча попал в образ** (образ `scratch`, shell нет): распаковать слои > `docker save` из `blobs/sha256/*` и грепнуть бинарник — > `strings …/usr/local/bin/balda | grep tool_call_id`. На OCI-образе `tar -tf` даёт ложный 0. > > --- > ### ✅ ВТОРОЙ PR — ФИКС MCP-ТУЛИНГА ОТПРАВЛЕН (2026-09-27) > > Чтобы закрыть обнажившийся баг `duplicate tool`, потребовалась правка **в `runtime`**, а не > в Балде. Отправлено **отдельным PR** — тема независима от #4. > > | Что | Значение | > |---|---| > | Форк | `mallexxx/runtime`, ветка `fix/hosted-toolsets-duplicate-endpoint` | > | **PR в апстрим** | **https://github.com/normahq/runtime/pull/5** | > | База | `main` (`12f1210`), `0` позади / `1` впереди — rebase не нужен | > | Коммит | `b01bdc6` | > | Объём | **2 файла, +263 / −0** | > | Патч | `agentfactory/agentfactory.go` +74; `agentfactory/duplicate_toolset_test.go` new (6 тестов) | > > **Суть фикса.** `hostedToolsets` схлопывает MCP-конфиги, различающиеся **только query/fragment**. > Балда регистрирует bundled-сервер дважды: `balda` (`…/mcp/balda`) и > `balda-session-memory-` (`…/mcp/balda?ctx=`). Оба отдают один список инструментов, > ADK пакует всё в одну карту → `duplicate tool`. > > 🔴 **Ключевое решение: выживает SCOPED-запись, не первая по алфавиту.** Её URL несёт > контекст-токен сессии — это строгое надмножество plain-эндпоинта. Наивный дедуп «первый победил» > отбросил бы токен и сломал session-scoped MCP-вызовы. Новые функции: `mcpEndpointKey` > (stdlib `net/url`, срезает `RawQuery` + `Fragment`) и `mcpURLIsScoped`. > `stdio`-транспорты возвращают `""` → **никогда не схлопываются**. > > Скип логируется — видно в проде: > `[mcp] skipping duplicate toolset "balda": endpoint already served by "balda-session-memory-"` > > **Гейты (локально, все зелёные на закоммиченном дереве):** > > ``` > go build ./... → exit 0 > go vet ./... → exit 0 > go tool golangci-lint run ./... → 0 issues > go test -race ./... → все пакеты ok > ``` > > ⚠️ **Питфолл линтеров на тесте** (`duplicate_toolset_test.go:187`): `staticcheck ST1023` > требует убрать явный тип (`var x tool.Toolset = ts`), а `unconvert` — убрать конверсию > (`tool.Toolset(ts)`). Оба правы → правильное решение — **не вводить переменную вообще**: > `if _, ok := any(ts).(requestProcessor); ok {`. После этого импорт `adk/v2/tool` стал > неиспользуемым — его тоже надо убрать. > > 🔴 **CI на PR НЕ ЗАПУСТИЛСЯ** — `mergeStateStatus: UNSTABLE`, `check-runs: total_count 0`. > Причина: PR из **форка**, GitHub требует **ручного подтверждения** workflows владельцем репо > («Approve and run workflows»). Это НЕ поломка PR. Локальные гейты пройдены полностью. > Требуется действие Alex: открыть PR#5 и подтвердить запуск CI. > > 📌 **Развилка репозиториев (важно не путать):** фикс MCP-тулинга → `normahq/runtime` (PR#5), > Mattermost-транспорт → `baldaworks/balda` (PR#8). Это **разные репозитории**, у них разные > форки (`mallexxx/runtime` / `mallexxx/balda`) и разные клоны на диске > (`~/Developer/runtime-local/` — git-клон; `~/Developer/balda/` — репо Балды). > > ⚠️ **`runtime-local` существует в ДВУХ местах** и их легко перепутать: > | Путь | Что это | > |---|---| > | `~/Developer/runtime-local/` | **git-клон форка** `mallexxx/runtime` — здесь делаются PR | > | `~/Developer/balda/runtime-local/` | **копия без `.git`** для сборки образа Балды | ## Расположение - **Валера** = `balda-agent-valera-1`, конфиг: `~/Docker/balda-agent/.config/balda/config.yaml` - **Клавдий** = `claudio-agent-claudio-1`, конфиг: `~/Docker/claudio-agent/.config/balda/config.yaml` - **Общий образ**: собирается из `~/Docker/claudio-agent/Dockerfile.claudio`, контекст `/Users/admin/Developer` - **Norma-local** (форк с MCP tools): `~/Developer/norma-local/` - `go.mod` replace: `github.com/normahq/norma => ./norma-local` (в `~/Developer/balda/go.mod`) - Модифицирован `pkg/runtime/hostedagent/openai.go` + `pkg/runtime/agentfactory/agentfactory.go` - **Dockerfile**: `~/Docker/claudio-agent/Dockerfile.claudio` - Копирует `norma-local/` в `/src/norma-local/` - `go mod edit -replace github.com/normahq/norma=./norma-local` перед `go mod download` ## Архитектура - **Валера** = DeepSeek (`provider: deepseek`, модель `deepseek-chat`) - **Клавдий** = Claude через прокси (`provider: claude`, `claude-sonnet-4-6`) - Валера использует **Balda runtime** — OpenAIModel из openai.go вызывается (hostedagent провайдер). - `agentfactory.go` используется обоими — там лог версии на старте. ## Версионный лог (norma-tools) При старте Валеры в логах: ``` norma-tools version=v0.0.10 build=whale-YYYYMMDD-N ``` Где: - `version` — номер версии нормы (из `openAIVersion`) - `build` — тег сборки (из `buildTag`) **Перед каждым билдом апать `buildTag`** в `openai.go`: ```go const openAIVersion = "v0.0.10" const buildTag = "whale-YYYYMMDD-N" // ← менять! ``` Файлы где апать: - `~/Developer/norma-local/pkg/runtime/hostedagent/openai.go` — `buildTag` константа ## Статус: DeepSeek MCP tools — РАБОТАЕТ ### Что сделано (openai.go) Добавлена полная поддержка OpenAI tool_calls: - `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры - `Tools []openAIToolDefinition` в `openAIChatRequest` - `openAIToolsFromConfig()` — конвертация genai.Tool[] → OpenAI definitions - `parseChatResponse()` — парсинг tool_calls из ответа DeepSeek (ID сохраняется) - `contentToOpenAI()` — конвертация FunctionCall/FunctionResponse в историю - `generate()` — TurnComplete=false при tool_calls (ADK делает второй раунд) ### Исправленные проблемы #### 1. DeepSeek возвращает аргументы с двойной сериализацией ✓ 22.06.2026 **Симптом:** `read_note`/`write_note` падали — Obsidian MCP возвращал `"Cannot read properties of undefined (reading 'replace')"`. **Корень:** DeepSeek возвращает `function.arguments` как JSON-строку (экранированную), а не как JSON-объект. **Фикс в `parseChatResponse`:** сперва пробуем распарсить Arguments как строку (`json.Unmarshal(&argsStr)`), потом эту строку как объект (`json.Unmarshal([]byte(argsStr), &args)`). #### 2. Obsidian MCP: get_vault_stats работает, read/write/delete нет ✓ 22.06.2026 **Корень:** двойная сериализация аргументов (см. проблему 1). После её фикса всё работает. #### 3. Key `output` не проверялся в contentToOpenAI ✓ 22.06.2026 Obsidian MCP возвращает response как `{"output":"..."}` — добавлена проверка на ключ `output`. ### Текущий билд - buildTag: `whale-20260622-7` - Все фиксы закоммичены в `master` (норма-локаль) - OPENAI-DEBUG стэш дропнут — логи в рабочей копии (не коммитятся) - Replace на норму работает через `go.mod` + Dockerfile - `norma-tools` лог подтверждён: `time=2026-06-22T13:00:37.170Z level=INFO msg=norma-tools version=v0.0.10 build=whale-20260622-7` ## Промежуточные статусы — ПОЧИНЕНО (23.06.2026) **Статус:** работает. - `RunSessionTurnPayload` (zulip_handler.go) — публикует промежуточный текст и ⚙️ function call статусы для `!ev.TurnComplete` ивентов через `sendPlain`. - `balda.go` (Telegram) — те же промежуточные публикации. - `handleAutoClaimMention` / `handleMessage` / `enqueueTurn` — передают `messageID` для точного Zulip threading. - Коммит: `05159d1` (main), запушен. ## Стэши debug-логов (23.06.2026) Дебаг логи не коммитятся, хранятся в стэшах. **balda** (`~/Developer/balda`): - `stash@{0}`: `debug: whale-20260623 — TASK-ACTOR logging in swarm_task_actor, event count & response_len debug in zulip_handler` - `internal/apps/balda/actors/swarm_task_actor.go` — TASK-ACTOR: dispatching/dispatch OK/FAILED - `internal/apps/balda/handlers/zulip_handler.go` — eventCount, response_len, running session turn log - `stash@{1}`: WIP on feat/zulip-events-polling **Восстановление balda стэша:** ```bash cd ~/Developer/balda && git stash pop stash@{0} ``` **norma-local** (`~/Developer/norma-local`): - OPENAI-DEBUG стэш дропнут — 9x fmt.Fprintf(os.Stderr, "OPENAI-DEBUG:...") в рабочей копии openai.go ## Бранчи (23.06.2026) **norma-local:** - `feat/hostedagent-mcp-tools` — коммит `56a6e1e` (DeepSeek tool_calls фикс), чистая бранча от `origin/master` - `master` — коммит `56a6e1e` (тот же, DeepSeek фиксы) **balda:** - `main` — `05159d1` (intermediate status фикс запушен) - `backup/our-main-before-upstream` — старый main - `feat/zulip-transport-intermediate` — от коммита `bc5fd03` ## Сборка ### Полный цикл сборки ```bash # 1. Инкремент buildTag в openai.go (строка buildTag) # ~/Developer/norma-local/pkg/runtime/hostedagent/openai.go # buildTag = "whale-YYYYMMDD-N" # 2. Применить OPENAI-DEBUG стэш (если нужно дебажить) cd ~/Developer/norma-local && git stash pop stash@{0} # 3. Сборка docker build --no-cache \ -f ~/Docker/claudio-agent/Dockerfile.claudio \ -t balda-agent-valera:latest \ /Users/admin/Developer # 4. Деплой cd ~/Docker/balda-agent && docker-compose up -d --force-recreate # 5. Проверка версии в логах docker logs balda-agent-valera-1 2>&1 | grep "norma-tools" ``` ### Особенности сборки - Dockerfile копирует `norma-local/` в `/src/norma-local/` - `go.mod` replace: `github.com/normahq/norma => ./norma-local` - `go mod edit -replace` применяется **до** `go mod download` - `--no-cache` обязателен при изменении norma-local - Образ надо таргетировать под нужное имя: `balda-agent-valera` для Валеры, `claudio-agent` для Клавдия - После сборки `docker-compose up -d --force-recreate` чтобы подхватить новый образ ## Диагностика молчания / проблем ### NORMA-TOOLS не появляется в логах Причина: агент использует не hostedagent провайдер. openai.go задействован только при provider=deepseek через ADK. ### MCP ошибка: `failed to list MCP tools: failed to connect: Not Found` Фикс: в config.yaml url должен заканчиваться на `/sse`: `url: http://obsidian-mcp:3101/sse` ### В логах `command running → command handled` за секунду, без `received provider event` Причина: стухший embedded NATS. Фикс: `docker-compose restart` или `up -d`. ### Удалён / пустой state.db Фикс: `rm state.db` и `up -d` (balda создаст заново). ### Нет owner Фикс: прописать `allowed_owners` в config.yaml. ### stream_only_with_session Фикс: написать `@Валера <текст>` чтобы создать сессию. ## Как инвентаризировать форк перед сверкой с апстримом (проверено 2026-09-25) Если снова встанет вопрос «что у нас есть, чего нет в апстриме» — этот порядок даёт полную картину без догадок: ```bash # 1. Подтянуть метаданные апстрима (не менять рабочую копию!) cd ~/Developer/balda git fetch --all --tags --prune # 2. Насколько мы отстали / опередили git rev-list --left-right --count HEAD...upstream/main # ожидаем "N M" # 3. Наши уникальные коммиты по КАЖДОЙ ветке (главное) for ref in origin/main backup/our-main-before-upstream feat/zulip-transport feat/zulip-events-polling; do echo "--- $ref ---"; git log --oneline upstream/main.."$ref" done # 4. Размер расхождения по файлам git diff --stat $(git merge-base upstream/main origin/main)..upstream/main | tail -5 # 5. Что апстрим сделал с файлами, которые мы правили git log --oneline --all -- pkg/runtime/hostedagent/openai.go ``` **Результат для нашего форка (2026-09-25):** | Репо | Отставание | Наши уникальные коммиты | |---|---|---| | `~/Developer/balda` | 900 файлов | `05159d15` (промежуточные статусы Zulip, `handlers/balda.go`), `bc5fd03c` (то же + Telegram + messageID), `feat/zulip-events-polling` (Events API long-poll) | | `~/Developer/norma-local` | 26 коммитов | `56a6e1e`=`91c2c0a` (DeepSeek tool_calls в `hostedagent/openai.go`) | **Важно про `5ad65955` в balda** — это НЕ фича, а `replace normahq/norma => ./norma-local` в `go.mod`. При переезде на чистый апстрим его нужно удалить, иначе сборка не пройдёт (файлов `norma-local` там нет). **Что апстрим сделал с нашими файлами:** - `norma v2` (`f8a7336`) **удалила целиком** `pkg/runtime/hostedagent/` и `pkg/runtime/agentfactory/`. Наш DeepSeek-фикс не переносится патчем. Перед переносом проверить, есть ли поддержка tool_calls для OpenAI-совместимых провайдеров в `normahq/runtime/v2 v2.0.11` — если да, фикс не нужен вовсе. - В balda наши `handlers/*` заменены на `handlersfx/*`; Zulip переехал в `channel/zulip/*`. Наш фикс промежуточных статусов надо переписывать под `handlersfx/zulip_ingress.go`. **Питфоллы при сверке:** - `origin` у `norma-local` — это `normahq/norma` (реальный апстрим), а у `balda` `origin` — **наш** `mallexxx/balda`, апстрим живёт в `upstream`. Не путать: «тянуть до апстрима» для balda ≠ `git pull origin main`. - Стэши теряются (не переживают `git gc`/клоны) — выносить в ветку до любых операций сброса. ## Известные грабли ### OPENAI_BASE_URL — обязательная env var Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env. ### DOCKER_OPTS — пустая строка убивает старт Должен быть валидный JSON: `DOCKER_OPTS={"max_tokens": 2048}` ### docker compose up -d не работает На этом хосте `docker compose` (без дефиса) не принимает `-d`. Использовать `docker-compose up -d`. ### Docker кеш COPY norma-local/ Слой копирования кешируется. `--no-cache` обязателен при изменении norma-local. ### go.mod replace `replace github.com/normahq/norma => ./norma-local` — путь относительно `/src` в контейнере, куда копируется `norma-local/`. ### zulip-router owner-форвард падал с 400 (23.06.2026) **Симптом:** Валера не отвечал на сообщения в закреплённых тредах без @mention. Router лог: `forward failed (owner) error="post ... status 400 body=bad request"`. Валера лог: `invalid zulip webhook payload error="unsupported message.type \"\""`. **Корень:** В `zulip-router/config.go` структура `Message` не имела поля `Type`. Zulip Events API присылает `message.type` ("stream"/"private"), но роутер его не парсил — в вебхук приходил пустой `""`. Balda-agent валидирует message.type. **Фикс:** Добавлен `Type string \`json:"type,omitempty"\`` в `Message struct`. Коммит `803ae68` (локальный, без remote). docker-compose up -d --build. Фикс: messageID int в сигнатуру handleAutoClaimMention, передаётся payload.Message.ID из processMessage. Файл: `~/Developer/balda/internal/apps/balda/handlers/zulip_handler.go` (в коммите `05159d1`)