25 KiB
Executable File
[!WARNING] Историческая схема Balda Не использовать пути, контейнеры, provider или MCP transport из этой заметки для текущего запуска. Канонический текущий runbook: personal/projects/balda/setup. Актуально:
balda-mm-mac,/Users/admin/Docker/balda-mm-mac/config/config.yaml, Mattermost, Codex ACP, Obsidiantype: 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.📌 Как восстановить, если понадобится (образ придётся пересобрать — он удалён):
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.
Что здесь всё ещё верно: грабли эксплуатации (NATS, owner,
DOCKER_OPTS,stream_only_with_session), бранчи/стэши, диагностика молчания. Что мертво: раздел проopenai.go/norma-localreplace как «текущее».
🔴 ВАЖНАЯ ПОПРАВКА (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Коммит 733b32dPR в апстрим 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.releaseCOPY runtime-local/доgo mod downloadОбраз balda-mm:0.1.12adkAPIфорк на 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-endpointPR в апстрим https://github.com/normahq/runtime/pull/5 База main(12f1210),0позади /1впереди — rebase не нуженКоммит b01bdc6Объём 2 файла, +263 / −0 Патч agentfactory/agentfactory.go+74;agentfactory/duplicate_toolset_test.gonew (6 тестов)Суть фикса.
hostedToolsetsсхлопывает MCP-конфиги, различающиеся только query/fragment. Балда регистрирует bundled-сервер дважды:balda(…/mcp/balda) иbalda-session-memory-<token>(…/mcp/balda?ctx=<token>). Оба отдают один список инструментов, ADK пакует всё в одну карту →duplicate tool.🔴 Ключевое решение: выживает SCOPED-запись, не первая по алфавиту. Её URL несёт контекст-токен сессии — это строгое надмножество plain-эндпоинта. Наивный дедуп «первый победил» отбросил бы токен и сломал session-scoped MCP-вызовы. Новые функции:
mcpEndpointKey(stdlibnet/url, срезаетRawQuery+Fragment) иmcpURLIsScoped.stdio-транспорты возвращают""→ никогда не схлопываются.Скип логируется — видно в проде:
[mcp] skipping duplicate toolset "balda": endpoint already served by "balda-session-memory-<token>"Гейты (локально, все зелёные на закоммиченном дереве):
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.modreplace: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:
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вopenAIChatRequestopenAIToolsFromConfig()— конвертация genai.Tool[] → OpenAI definitionsparseChatResponse()— парсинг 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_handlerinternal/apps/balda/actors/swarm_task_actor.go— TASK-ACTOR: dispatching/dispatch OK/FAILEDinternal/apps/balda/handlers/zulip_handler.go— eventCount, response_len, running session turn log
stash@{1}: WIP on feat/zulip-events-polling
Восстановление balda стэша:
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/mastermaster— коммит56a6e1e(тот же, DeepSeek фиксы)
balda:
main—05159d1(intermediate status фикс запушен)backup/our-main-before-upstream— старый mainfeat/zulip-transport-intermediate— от коммитаbc5fd03
Сборка
Полный цикл сборки
# 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.modreplace:github.com/normahq/norma => ./norma-localgo 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)
Если снова встанет вопрос «что у нас есть, чего нет в апстриме» — этот порядок даёт полную картину без догадок:
# 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(реальный апстрим), а уbaldaorigin— наш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`)