[2026-09-27] eagle: personal/plans/balda-mm-phase4-agents.md personal/projects/balda/balda-kraken-mm-deploy.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 01:35:32 +06:00
1 parent 7f85c2094a
commit 9493ae5514
2 files changed
+182 -41

No files matched your search

+163 -35
View File
@@ -21,9 +21,10 @@
| 6c | Structured-output память на `deepseek-chat` | ⚠️ open: `dead_letter / model_failure` — память не наполняется, ответы работают. См. §13 |
| 6d | Живая приёмка: Балда отвечает | ✅ **подтверждено 2026-09-26** через `mmctl post list` — бот отвечает, `duplicate tool` ушёл. См. §14 |
| 6e | 🔴 MCP-инструменты НЕ исполняются | ✅ **патч готов 2026-09-27** — форк `mallexxx/runtime` + PR [#4](https://github.com/normahq/runtime/pull/4), `replace` в `go.mod`, образ `balda-mm:0.1.12`. См. §15-16 |
| 6f | 🔴 `duplicate tool` на `0.1.12` — второй, более глубокий баг | 🟡 **корень локализован 2026-09-27** — форк включил второй раунд ADK (`TurnComplete = !hasToolCalls`) и **обнажил** двойную паковку `balda.control.shutdown` (`Tools` + `Toolset`). На `0.1.11` не проявлялся, т.к. инструменты не пакались вообще. 8 версий исключены фактами. См. §16 |
| 6g | A/B-эксперимент откатом на `0.1.11` | ⏸ **не доведён** — `0.1.11` поднят и стартовал, пост-сравнение не отправлено. Следующее действие. См. §16.4 |
| 7 | Приёмка от аккаунта Alex | 🟡 **метод готов** — `mmctl --local token generate admin` даёт токен владельца без пароля; пост от `admin` проходит авторизацию (`settlement_stage: accepted`). Блокер — ход падает на `duplicate tool` (п. 6f). См. §17 |
| 6f | 🔴 `duplicate tool` — **КОРЕНЬ НАЙДЕН И ДОКАЗАН** (2026-09-27) | ✅ **корень: двойная паковка в ADK `v2.4.0`, к форку отношения НЕ имеет** — `toolProcessor` расширяет инструменты toolset'ов в `f.Tools`, затем `toolPreprocess(f.Tools)` и `toolsetPreprocess(Toolsets)` пакуют их ДВАЖДЫ → `packTool` падает. См. §16.2 (исправлен) и §18 |
| 6g | A/B-эксперимент откатом на `0.1.11` | ✅ **доведён 2026-09-27** — `0.1.11` падает С ТЕМ ЖЕ `duplicate tool`. Гипотеза §16.2 («форк обнажил») **ОПРОВЕРГНУТА**: баг был всегда, независимо от раунда. См. §16.4 |
| 6h | Фикс двойной паковки | 🔴 **НЕ начат** — решение принято (Option A, §18.5), правка в `runtime-local/agentfactory/agentfactory.go`, ждёт go-ahead Alex. Целевой образ `balda-mm:0.1.13` |
| 7 | Приёмка от аккаунта Alex | 🟡 **метод готов и проверен** — `mmctl --local token generate admin` даёт токен владельца без пароля; пост от `admin` проходит авторизацию (`settlement_stage: accepted`, лог подтверждает). Блокер — ход падает на `duplicate tool` (п. 6f/6h). См. §17 |
---
@@ -807,7 +808,7 @@ strings /tmp/chkx/usr/local/bin/balda | grep -E "tool_call_id|missing content an
⚠️ `tar -tf image.tar | grep ca-certificates` даёт **ложный 0** на OCI-образе — файлы внутри
слоёв. Так же и с поиском кода форка: смотреть только распакованный бинарник.
### 16.2 🔴 Форк не «сломал» Балду — он ОБНАЖИЛ скрытый баг
### 16.2 🔴 Корень: двойная паковка инструментов в ADK `v2.4.0` (исправлено 2026-09-27)
**Симптом после деплоя `0.1.12`:** каждый ход падает одинаково, Балда молчит.
@@ -820,25 +821,61 @@ warn … publish event "balda.v1.evt.command.in_progress": context deadline exc
**Это тот же `duplicate tool`, что чинили в §13.** В конфиге дубля НЕТ — проверено:
`balda.mcp_servers: []`, ID только у провайдера. Значит причина другая.
**Механизм (по коду ADK `v2.4.0`):**
```
base_flow.go:778 toolPreprocess() — пакует agent.Tools → req.Tools
base_flow.go:798 toolsetPreprocess() — пакует Toolsets → req.Tools
└─ mcptoolset/tool.go:89 → PackTool() → toolutils.go:47-48
if _, ok := req.Tools[name]; ok { return fmt.Errorf("duplicate tool: %q", name) }
```
`tools_processor.go:31` — `if f.Tools != nil { return }` — защищает **только** сборку `f.Tools`,
но **не** повторный вызов `toolsetPreprocess`. Если инструмент присутствует и в `Tools`,
и в одном из `Toolsets` (либо в двух toolsets) — второй проход падает.
> ### ⚠️ ИСПРАВЛЕНИЕ РАННЕЙ ВЕРСИИ ЭТОГО РАЗДЕЛА (2026-09-27)
> Ранее здесь было записано: «форк включил второй раунд ADK (`TurnComplete = !hasToolCalls`) и
> **обнажил** двойную паковку; на `0.1.11` её не было, т.к. инструменты не пакались вообще».
> **Это ОПРОВЕРГНУТО экспериментом §16.4.** `0.1.11` падает с ТЕМ ЖЕ `duplicate tool` при явном
> запросе инструмента. Значит:
> - паковка инструментов идёт **независимо** от `TurnComplete` — она происходит на фазе
> сборки запроса, **до** первого ответа модели;
> - второй раунд форка **не является** условием бага;
> - баг жил в связке Balda↔ADK **всегда**, на обеих версиях.
> Не воспроизводить старую формулировку: она ведёт к ложному выводу «откат на `0.1.11` лечит».
**Почему на `0.1.11` этого не было:** апстримный `openai.go` жёстко ставил `TurnComplete: true`
(:107). Агент завершал ход после **первого** ответа модели — **инструменты не пакались вообще**.
Ошибки не было, потому что не было второго раунда. Но и инструменты не работали — что и было
исходной проблемой (§14, «серверы перечислены, инструменты не исполняются»).
**Механизм — три шага, все по коду ADK `v2.4.0` (прочитано построчно):**
> **Вывод:** на `0.1.11` Балда отвечала, но инструменты были мертвы. Форк включил второй раунд
> (`TurnComplete = !hasToolCalls`) → паковка началась → **вскрылся баг сборки инструментов,
> который жил в связке Balda↔ADK всё время.** Форк не регрессия — форк диагност.
**Шаг 1.** `internal/llminternal/tools_processor.go:40-58` — `toolProcessor` **расширяет**
инструменты каждого toolset'а в `f.Tools`:
```go
if f.Tools != nil { return } // защита только от повторного расширения
tools := Reveal(llmAgent).Tools
for _, toolSet := range Reveal(llmAgent).Toolsets {
tsTools, err := toolSet.Tools(icontext.NewReadonlyContext(ctx))
tools = append(tools, tsTools...) // 🔴 mcpTool'ы скопированы в f.Tools
}
f.Tools = tools
```
**Шаг 2.** `internal/llminternal/base_flow.go:765-768` — **оба** прохода подряд над ОДНИМ `req`,
между ними **нет** защиты:
```go
if err := toolPreprocess(ctx, req, f.Tools); err != nil { … } // :778
if err := toolsetPreprocess(ctx, req); err != nil { … } // :798
```
**Шаг 3.** `tool/mcptoolset/tool.go:88-90` — `mcpTool.ProcessRequest` → `toolutils.PackTool`:
```go
// toolutils/toolutils.go:47-48
if _, ok := req.Tools[name]; ok {
return fmt.Errorf("duplicate tool: %q", name)
}
```
**Итог:** `f.Tools` уже содержит `balda.control.shutdown` (расширен на шаге 1) → `toolPreprocess`
кладёт его в `req.Tools` → `toolsetPreprocess` пытается положить его **второй раз** через
MCP-toolset → падение.
🔴 **Это дефект архитектуры ADK, а не Балды и не форка.** Два слоя (tools и toolsets) трактуются
как взаимоисключающие (`if f.Tools != nil { return }`), но `base_flow` вызывает их **безусловно
оба**. `PackTool` идемпотентным не является by design.
📌 **Почему §16.4-кандидат «`Tools` + `Toolsets` в `llmagent.Config`» — тоже не источник:**
`agentfactory.go:644` строит `toolsets = hostedToolsets(req.Toolsets, resolvedMCP)`, которая
**копирует** `req.Toolsets` и добавляет по одному toolset'у на MCP-сервер. Балда
(`internal/apps/balda/agent/builder.go:296-303`) задаёт **только `MCPServerIDs`** — `Tools` и
`Toolsets` в `BuildRequest` **пустые**. Значит MCP-серверы приходят **исключительно** как
toolsets, и «дубль в двух полях» невозможен. Источник дубля — **`toolProcessor`**, не
`agentfactory`.
### 16.3 Что исключено фактами (не повторять)
@@ -857,22 +894,56 @@ base_flow.go:798 toolsetPreprocess() — пакует Toolsets → req.To
вхождений подстроки**, а не число серверов: `balda` встречается в каждом логе, в `balda.v1.*`,
в `balda.control.shutdown`. Не делать из этого вывод о дублях — считать **уникальные** ID.
### 16.4 Где встали и что осталось
### 16.4 ✅ A/B-эксперимент на `0.1.11` — ДОВЕДЁН, гипотеза опровергнута (2026-09-27)
**Незавершённый эксперимент (следующее действие).** Оставлен откат на `0.1.11` для A/B:
если там `duplicate tool` повторяется при **явном** вызове инструмента → баг был всегда;
если нет → он вносится вторым раундом. На момент записи: `0.1.11` поднят и стартовал
(`Balda bot started`), пост-сравнение не отправлен.
**Что сделано.** `0.1.11` уже стоял живым (`docker ps` → `balda-mm balda-mm:0.1.11 Up`,
compose строка 3 = `0.1.11`). Взял токен владельца без пароля и послал пост с **явным**
требованием вызвать инструмент:
**Рекомендованный порядок:**
1. Довести A/B на `0.1.11` (пост от владельца, см. §17 — как постить).
2. Найти, что даёт **вторую регистрацию** `balda.control.shutdown`: кандидаты —
bundled-`balda` попадает и как `Tools`, и как `Toolset` одновременно
(`agentfactory.go:655-656`: `Tools: req.Tools` + `Toolsets: toolsets`, оба в
`llmagent.Config`); либо `balda` присутствует в двух toolsets.
3. Оценить фикс **в апстриме ADK/`agentfactory`**, а не в Балде: `PackTool` идемпотентным не
является by design, значит дублировать источник нельзя.
4. Вернуть `0.1.12` после фикса.
```bash
ssh kraken 'docker exec mattermost mmctl --local token generate admin "ab-011-$(date +%H%M)"'
# → 86qus47tbfys9xehf4x5yxzuyr: ab-011-0129
ssh kraken '/tmp/mm-post-admin.sh 86qus47tbfys9xehf4x5yxzuyr 7bjw4g7o1jdsbmjbqd649xxgaw \
"@balda вызови инструмент executor_health и покажи результат"'
# → posted id=hg6f1tgat7n53m345smertii5h user=cbwsorzwoigbtrdnteugnx4aqo
```
**Результат: `0.1.11` падает ТЕМ ЖЕ error'ом.** Лог `docker logs balda-mm --since 5m`:
```json
{"level":"info","component":"balda.handlersfx.mattermost",…,"settlement_stage":"accepted",…}
{"level":"error","component":"balda.turn_dispatcher",
"error":"agent run: duplicate tool: \"balda.control.shutdown\"",…,"message":"turn task failed"}
{"level":"warn","component":"balda.execution_bus","delivery_attempt":1,…,"settlement_class":"retrying:unclassified"}
```
📌 **Авторизация прошла корректно** (`settlement_stage: accepted`, `post_user_id` = Alex) —
новый метод постинга (§17.1) работает, пост **не** отбит. До падения доходит именно ход агента.
**Выводы (durable):**
| Утверждение | Статус |
|---|---|
| `duplicate tool` вносится вторым раундом форка | ❌ **опровергнуто** — `0.1.11` падает так же |
| На `0.1.11` инструменты «не пакались вообще» | ❌ **опровергнуто** — паковка идёт до первого ответа модели |
| Откат на `0.1.11` — рабочее состояние | ❌ **ложно** — на `0.1.11` инструменты вызывать НЕЛЬЗЯ, ход падает |
| Баг существует независимо от версии образа | ✅ **подтверждено** |
> 🔴 **Следствие для стратегии:** держать `0.1.11` «для отката» как рабочий вариант **нельзя** —
> он падает на любом явном вызове инструмента. Это не безопасная точка отката, а сломанное
> состояние. Единственный путь — фикс двойной паковки (§18) и `0.1.13`.
**Метод воспроизведения (1 команда, без Alex и без GUI):**
```bash
TOK=$(ssh kraken 'docker exec mattermost mmctl --local token generate admin "ab-$(date +%s)"' | awk '{print $1}')
ssh kraken "/tmp/mm-post-admin.sh $TOK 7bjw4g7o1jdsbmjbqd649xxgaw '@balda вызови инструмент executor_health'"
sleep 45
ssh kraken 'docker logs balda-mm --since 3m 2>&1 | grep -E "duplicate tool|turn task failed" | tail -3'
```
📌 Пост обязан содержать **явное требование вызвать инструмент** — на обычное «скажи одно слово»
Балда отвечает текстом, инструменты не пакуются, и баг **не проявляется** (что и породило
ложный вывод «на `0.1.11` всё хорошо»).
⚠️ **Побочный эффект откатов:** жёсткое убийство контейнера оставляет незакрытый `state.db-wal`,
и следующий старт на `0.1.11` идёт ~4.5 мин вместо ~1m11s (переигрывание WAL). Не считать это
@@ -961,6 +1032,63 @@ ORDER BY m.date_sent DESC LIMIT 10;"
---
## 18. 🔴 Фикс двойной паковки — план (2026-09-27, ждёт go-ahead Alex)
**Контекст:** корень доказан (§16.2), A/B проведён (§16.4). Фикс **не начат** — правка кода
в форке требует подтверждения Alex (правило: план → апрув → работа).
### 18.1 Где чинить и почему НЕ в Балде
| Вариант | Вердикт |
|---|---|
| Патч ADK `base_flow.go` | ❌ **невозможно без вендоринга** — `google.golang.org/adk/v2 v2.4.0` это модульная зависимость (`go.mod:27`), файлы в `$GOMODCACHE`, read-only |
| Патч `internal/apps/balda/*` | ❌ Балда задаёт только `MCPServerIDs` (`builder.go:296-303`), `Tools`/`Toolsets` пустые — дублировать нечего, чинить нечего |
| **Патч форка `runtime-local/agentfactory/agentfactory.go`** | ✅ **единственная доступная точка** — форк уже подключён через `replace` в `go.mod`, мы им владеем |
### 18.2 Суть фикса (Option A)
`toolProcessor` (§16.2 шаг 1) **сам** расширяет toolsets в `f.Tools` и при этом ставит
`f.Tools != nil` — после чего защита `if f.Tools != nil { return }` срабатывает и
повторное расширение не происходит. Но `toolsetPreprocess` вызывается **безусловно** и
пакует те же инструменты второй раз.
**Решение:** не отдавать MCP-toolsets одновременно и как `Toolsets`. Для
`openAIConstructor` / `aistudioConstructor`:
- `Toolsets:` передавать только `req.Toolsets` (без `hostedToolsets(...)`),
- MCP-инструменты добавлять в `Tools` — тогда `f.Tools != nil` и `toolsetPreprocess`
останется без MCP-toolsets, которые всё равно уже расширены в `Tools`.
**Файл:** `runtime-local/agentfactory/agentfactory.go`
**Функции:** `openAIConstructor` (:635-658), `aistudioConstructor` (:660-685) — **обе**
(симметричный дефект), плюс `hostedToolsets` (:360-384) как источник.
### 18.3 Порядок работ (после апрува)
1. Правка `agentfactory.go` + тесты в `agentfactory/agentfactory_test.go`.
2. Гейты: `go build ./...`, `go test ./...`, `go tool golangci-lint run ./...` (по `AGENTS.md` форка).
3. **Тест на регрессию обязателен:** тест, который падает на текущем коде с
`duplicate tool` и проходит после фикса.
4. Сборка образа на Mac: `docker buildx build --builder arm64builder --platform linux/arm64
-f Dockerfile.release --provenance=false --load -t balda-mm:0.1.13 .`
5. `docker save` → `scp` → `docker load` на Kraken → `tools/mm-switch-image.sh` →
`docker compose up -d --force-recreate`.
6. Приёмка (§16.4 метод): пост от владельца **с явным вызовом инструмента** →
ждём `executor_health` в ответе и **отсутствие** `duplicate tool` в логе.
7. Обновить §0 (6h → ✅) и Фазу 3 в [[personal/projects/balda/balda-kraken-mm-deploy]].
### 18.4 🔴 `0.1.11` — НЕ точка отката
По итогам A/B (§16.4): на `0.1.11` любой ход с явным вызовом инструмента падает с тем же
`duplicate tool`. Держать его как «безопасный откат» **нельзя**. Образы для отката:
`0.1.10` (отвечает текстом, инструменты мертвы) — единственный, где бот **отвечает**.
### 18.5 Открытый вопрос Alex
Правку `runtime-local/agentfactory/agentfactory.go` (Option A) писать сейчас?
---
## Связанные заметки
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план (Фазы 4-5)
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅)
@@ -3,15 +3,28 @@
> Создан 2026-09-26. Обновлён 2026-09-26 (**✅ ФАЗА ЗАКРЫТА — боевая приёмка пройдена**).
> Статус: **✅ РАБОТАЕТ. Бот отвечает аккаунту Alex.** Образ `0.1.10` (коммит `9f1e6c9c`,
> он же PR baldaworks/balda#8).
> ⚠️ **АКТУАЛЬНЫЙ РАБОЧИЙ ОБРАЗ НА KRAKEN — `balda-mm:0.1.11`** (2026-09-27).
> Собран был **`0.1.12`** (форк runtime с tool-calling для `type: openai`: `openAIToolsFromConfig`,
> 🔴 **ТЕКУЩЕЕ СОСТОЯНИЕ (2026-09-27): бот отвечает ТЕКСТОМ, инструменты вызвать НЕЛЬЗЯ.**
> На Kraken стоит `balda-mm:0.1.11`. Любой ход с **явным вызовом инструмента** падает с
> `duplicate tool: "balda.control.shutdown"` — **на `0.1.11` тоже**, не только на `0.1.12`
> (A/B проведён 2026-09-27, лог подтверждён). Обычные текстовые ответы работают — отсюда
> ложное впечатление «рабочего образа».
> 🔴 **Корень (доказан по коду ADK `v2.4.0`):** двойная паковка инструментов —
> `toolProcessor` (`tools_processor.go:40-58`) расширяет инструменты toolset'ов в `f.Tools`,
> затем `base_flow.go` вызывает `toolPreprocess(f.Tools)` **и** `toolsetPreprocess(Toolsets)`
> без защиты между ними → `PackTool` (`toolutils.go:47-48`) видит имя второй раз → падение.
> **Это дефект ADK, не Балды и не форка.** Полный разбор — [[personal/plans/balda-mm-phase4-agents]] §16.2.
> Фикс (Option A, патч форка `runtime-local/agentfactory/agentfactory.go`) спланирован, но
> **не начат** — ждёт go-ahead Alex. Целевой образ `balda-mm:0.1.13`. См. §18 того же дока.
> ⚠️ Собран был **`0.1.12`** (форк runtime с tool-calling для `type: openai`: `openAIToolsFromConfig`,
> `parseChatResponse`, `TurnComplete = !hasToolCalls`; форк `mallexxx/runtime`, ветка
> `feat/openai-tool-calls-v2`, коммит `733b32d`, PR **https://github.com/normahq/runtime/pull/4**;
> в Балде — `replace github.com/normahq/runtime/v2 => ./runtime-local`).
> 🔴 **Но `0.1.12` откачен на `0.1.11`:** форк включил второй раунд ADK и **обнажил** двойную
> паковку инструментов → `duplicate tool: "balda.control.shutdown"` на каждом ходу, Балда молчала.
> На `0.1.11` Балда отвечает, но инструменты не исполняются (инструменты не пакались вообще).
> Разбор и следующий шаг — [[personal/plans/balda-mm-phase4-agents]] §16.
> 🔴 **`0.1.12` откачен на `0.1.11` — но это оказалось НЕ лечением** (2026-09-27): ранняя
> версия этого дока утверждала, что форк «обнажил» двойную паковку. **Опровергнуто A/B:**
> `0.1.11` падает тем же `duplicate tool` при явном вызове инструмента. Паковка инструментов
> идёт на сборке запроса, **до** первого ответа модели, и от второго раунда ADK не зависит.
> Баг жил в связке Balda↔ADK всегда.
> Разбор и следующий шаг — [[personal/plans/balda-mm-phase4-agents]] §16.2 и §18.
> Compose: `/home/kraken/balda/docker-compose.yml` (`image: balda-mm:0.1.11`).
> **Доказательство (лента `balda-test`):** `cbwsorzwoigbtrdnteugnx4aqo` (Alex) —
> `@balda скажи одно слово` → `edxrgupoxiyig5gzmr79tjypbme` (бот) — `слово`.