[2026-09-26] eagle: personal/plans/balda-mm-phase4-agents.md personal/plans/tagwriter-container.md personal/projects/balda/balda-valera.md personal/tech/balda-go-fx-pitfalls.md personal/tech/music-tags-repair-truenas.md
This commit is contained in:
1 parent
593b867467
commit
84fee3eea2
5 files changed
+507
-16
No files matched your search
@@ -20,6 +20,17 @@
|
||||
> `balda.session_memory` vs `balda.memory`, MCP через секцию провайдера.
|
||||
> §«Сброс падающих тестов» — сначала `git stash`, потом слово «регрессия»;
|
||||
> `buildx` без `--load` не кладёт образ в `docker images`.
|
||||
> 🔴 **Добавлено 2026-09-26 (вторая итерация той же сессии):**
|
||||
> §«Подключение MCP ≠ ИСПОЛНЕНИЕ инструментов» — `type: openai` подключает ID серверов,
|
||||
> но инструменты исполняет только **ACP-провайдер** (`builder.go:479-518`); лимиты
|
||||
> reconciler'а опровергнуты. §«Живая приёмка бота БЕЗ GUI» — `mmctl --local post list`
|
||||
> как доказательство, и почему `post create` в local-режиме невозможен.
|
||||
> 🔴🔴 **Добавлено 2026-09-26 (третья итерация):** §«НАСТОЯЩИЙ корень: в `runtime v2`
|
||||
> вырезан `tool_calls`» — апстрим переписал `hostedagent/openai.go` и **потерял** наш
|
||||
> фикс из `norma-local` (`91c2c0a`). Полный разбор 4 блоков патча + 2 питфолла DeepSeek
|
||||
> (двойная сериализация аргументов, ключ `output`) + регресс v2 «ответ без текста».
|
||||
> 📌 Урок метода: **прежде чем объявлять ограничение апстрима — свериться со своим
|
||||
> старым патчем.** Ключ Alex: «Это не та же проблема, что мы решали в форке?»
|
||||
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
|
||||
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
|
||||
> Проектные доки: [[personal/projects/balda/balda-mattermost-connector]],
|
||||
@@ -216,6 +227,187 @@ warn … session-memory capability retry limit exhausted
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴 Подключение MCP ≠ ИСПОЛНЕНИЕ инструментов: нужен ACP-провайдер
|
||||
|
||||
Найдено 2026-09-26. Класс ошибки: **два разных слоя, которые легко слить в один вывод.**
|
||||
|
||||
| Слой | Что происходит | Зависит от типа провайдера? |
|
||||
|---|---|---|
|
||||
| **Подключение** | ID серверов попадают в сессию, видны в стартовом сообщении (`MCP: balda, executor-mcp, obsidian`) | ❌ нет — список собирается из конфига |
|
||||
| **Исполнение** | модель реально вызывает инструменты | 🔴 **да — только ACP-провайдеры** |
|
||||
|
||||
**Симптом неисполнения:** сессия перечисляет серверы, но бот на вопрос отвечает
|
||||
«доступно только `balda.memory`» — и то же говорит про **свой** `balda.workspace`.
|
||||
То есть недоступен **ни один** MCP-инструмент, включая встроенный. Это маркер слоя,
|
||||
а не отдельного сервера: если не работает и bundled — дело не в конфиге серверов.
|
||||
|
||||
**Причина в коде** — `internal/apps/balda/agent/builder.go`:
|
||||
|
||||
```go
|
||||
// :518 — bundled `balda` добавляется КОДОМ, не конфигом
|
||||
MCPServers: mergeMCPServerIDsWithBase([]string{"balda"}, agentCfg.MCPServers, nil),
|
||||
|
||||
// :479-500 — GetAgentMetadata знает 10 ACP-типов
|
||||
switch strings.TrimSpace(agentCfg.Type) {
|
||||
case agentconfig.AgentTypeGenericACP: acp = agentCfg.GenericACP
|
||||
case agentconfig.AgentTypeGeminiACP: acp = agentCfg.GeminiACP
|
||||
case agentconfig.AgentTypeCodexACP: acp = agentCfg.CodexACP
|
||||
case agentconfig.AgentTypeOpenCodeACP: acp = agentCfg.OpenCodeACP
|
||||
case agentconfig.AgentTypeCopilotACP: acp = agentCfg.CopilotACP
|
||||
case agentconfig.AgentTypeClaudeCodeACP,
|
||||
agentconfig.AgentTypeClaudeACP: acp = agentCfg.ClaudeCodeACP
|
||||
case agentconfig.AgentTypeGrokACP: acp = agentCfg.GrokACP
|
||||
case agentconfig.AgentTypeRegistryACP: acp = agentCfg.RegistryACP
|
||||
case agentconfig.AgentTypeAgyACP: acp = agentCfg.AgyACP
|
||||
case agentconfig.AgentTypeAntigravityACP: acp = agentCfg.AntigravityACP
|
||||
}
|
||||
|
||||
// :504-511 — разделение по классу
|
||||
switch {
|
||||
case acp != nil: // ACP-рантайм: инструменты исполняются
|
||||
model = strings.TrimSpace(acp.Model)
|
||||
case agentCfg.OpenAI != nil: // 🔴 только модель, БЕЗ ACP
|
||||
model = strings.TrimSpace(agentCfg.OpenAI.Model)
|
||||
}
|
||||
```
|
||||
|
||||
📌 **Правило:** `type: openai` (и любой чистый chat-completions провайдер) подключает
|
||||
ID серверов, но **не исполняет** их инструменты. Для рабочих MCP-инструментов есть
|
||||
два пути: **ACP-провайдер** (`claude_code_acp`, `codex_acp`, `gemini_acp`, `opencode_acp`,
|
||||
`copilot_acp`, `grok_acp`, `registry_acp`, `agy_acp`, `antigravity_acp`, `generic_acp`)
|
||||
**или** починить tool_calls в `hostedagent/openai.go` — см. следующий раздел.
|
||||
|
||||
⚠️ **Как не запутаться (мой промах):** сначала я записал «`openai` не умеет MCP» как
|
||||
**ложный след**, потом снова пришёл к нему же. Оба раза прав был частично — на **разных
|
||||
слоях**. Писать надо явно: «ID подключаются у любого типа; инструменты исполняются
|
||||
только у ACP». Формулировка без указания слоя гарантирует путаницу.
|
||||
|
||||
📌 **Опровергнутая гипотеза (не тратить время):** лимиты reconciler'а.
|
||||
`mcpruntime/reconciler.go:97` — defaults `MaxServers: 32, MaxTools: 256,
|
||||
MaxToolMetadataBytes: 256<<10`. Три сервера и три инструмента в них укладываются
|
||||
с большим запасом; серверы **не** отбрасываются лимитом.
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴 НАСТОЯЩИЙ корень: в `runtime v2` вырезан `tool_calls` для openai-провайдера
|
||||
|
||||
Найдено 2026-09-26. **Alex опознал проблему раньше меня:** «Это не та же проблема,
|
||||
которую мы решали в mallexxx-форке старой балды?» — да, ровно та же. Я весь вечер
|
||||
подходил к выводу «openai не умеет инструменты» и не догадался свериться со **своим
|
||||
же старым патчем** в `~/Developer/norma-local`.
|
||||
|
||||
📌 **Урок метода:** прежде чем объявлять ограничение апстрима («тип провайдера не умеет»),
|
||||
проверить, не закрывали ли мы ЭТУ дыру раньше своим патчем. Ограничение и незакрытая
|
||||
дыра выглядят одинаково, но лечатся по-разному.
|
||||
|
||||
### Что было в патче (закрыт Alex'ом 2026-06-22)
|
||||
|
||||
`~/Developer/norma-local/pkg/runtime/hostedagent/openai.go`, коммит `91c2c0a`
|
||||
(+286 строк кода, +312 строк тестов) — 4 блока:
|
||||
|
||||
| Блок | Что добавляет |
|
||||
|---|---|
|
||||
| Структуры | `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` |
|
||||
| Запрос | `Tools []openAIToolDefinition` + `openAIToolsFromConfig()`: `genai.Tool[]` → OpenAI, фильтр имён `^[a-zA-Z0-9_-]+$` |
|
||||
| Ответ | `parseChatResponse()` — сборка `FunctionCall` из `tool_calls` |
|
||||
| История | `contentToOpenAI()` — конвертация `FunctionCall`/`FunctionResponse` в сообщения (`role: assistant` с `tool_calls`, `role: tool` с `tool_call_id`) |
|
||||
| Цикл | `TurnComplete = !hasToolCalls` — ADK получает второй раунд на исполнение |
|
||||
|
||||
**Питфолл DeepSeek — двойная сериализация аргументов.** Модель отдаёт
|
||||
`function.arguments` как **JSON-строку**, а не объект. Разбор в два шага:
|
||||
```go
|
||||
var argsStr string
|
||||
if err := json.Unmarshal(tc.Function.Arguments, &argsStr); err == nil {
|
||||
if err2 := json.Unmarshal([]byte(argsStr), &args); err2 != nil {
|
||||
args = map[string]any{"raw": argsStr}
|
||||
}
|
||||
} else if err2 := json.Unmarshal(tc.Function.Arguments, &args); err2 != nil {
|
||||
args = map[string]any{"raw": string(tc.Function.Arguments)}
|
||||
}
|
||||
```
|
||||
Без этого Obsidian MCP падал с `Cannot read properties of undefined (reading 'replace')`.
|
||||
|
||||
**Питфолл ключа ответа.** `FunctionResponse.Response` приходит в разных формах —
|
||||
проверять по очереди: `result`, `response`, `text`, `error`, `content`, **`output`**
|
||||
(последний — специфика Obsidian MCP), иначе `json.Marshal` всего словаря.
|
||||
|
||||
### Что в `runtime v2.0.11` — вырезано НОЛЬ из пяти
|
||||
|
||||
`$(go env GOMODCACHE)/github.com/normahq/runtime/v2@v2.0.11/hostedagent/openai.go` —
|
||||
**246 строк** (против 490 у пропатченного):
|
||||
|
||||
| Что | Статус в v2 |
|
||||
|---|---|
|
||||
| `Tools` в запросе | ❌ `openAIToolsFromConfig` отсутствует целиком |
|
||||
| `tool_calls` в ответе | ❌ `parseChatResponse` знает только `Content` |
|
||||
| История tool-результатов | ❌ `openAIMessagesFromRequest:204-208` режет всё, кроме текста |
|
||||
| `TurnComplete` | ❌ жёстко `true` (:107) |
|
||||
|
||||
🔴 **Отдельный регресс v2 — падение на «ответ без текста»** (:180-182):
|
||||
```go
|
||||
if len(parsed.Choices) == 0 || strings.TrimSpace(parsed.Choices[0].Message.Content) == "" {
|
||||
return nil, fmt.Errorf("openai response missing content")
|
||||
}
|
||||
```
|
||||
Ответ **только** с `tool_calls` (без текста) валиден для OpenAI API, но v2 считает
|
||||
его ошибкой. Чинить **первым** — иначе второй раунд ADK упадёт до вызова инструмента.
|
||||
|
||||
📌 **Версии ADK при переносе:** в `norma-local` — `adk` v1 (`google.golang.org/adk/model`),
|
||||
в `runtime v2` — `adk/v2`. Типы `genai.Tool` / `FunctionDeclaration` сверять по месту.
|
||||
|
||||
📌 **Модуль кэша read-only:** патчить `go/pkg/mod/...` нельзя. Путь — форк + `replace`
|
||||
в `go.mod` потребителя (как делали с `norma-local`).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Живая приёмка бота в Mattermost БЕЗ GUI и БЕЗ участия заказчика
|
||||
|
||||
Метод 2026-09-26. Alex отказался быть «руками» для проверки («ты ведь даже не поверил
|
||||
ожил ли балда вообще»). Агент обязан проверять сам.
|
||||
|
||||
### Что НЕ работает
|
||||
|
||||
| Путь | Ошибка | Причина |
|
||||
|---|---|---|
|
||||
| `mmctl post create <team:channel> --message '…'` | `creating posts is not supported in local mode` | `--local` ходит через unix-сокет: **чтение разрешено, запись от имени человека — нет** |
|
||||
| `mmctl post create --channel <id>` | `unknown flag: --channel` | цель — **позиционный** аргумент формата `team:channel` |
|
||||
| `docker exec mattermost curl -X POST …/users/login -d '{…}'` | `Syntax error: Unterminated quoted string` | вложенные кавычки JSON ломаются в ssh+sh. Писать **скрипт в файл**, не инлайн (правило Alex) |
|
||||
|
||||
### Что работает
|
||||
|
||||
```bash
|
||||
docker exec mattermost mmctl --local team list # → qentra
|
||||
docker exec mattermost mmctl --local channel list qentra # → balda-test, off-topic, town-square
|
||||
docker exec mattermost mmctl --local post list qentra:balda-test 2>&1 | tail -25
|
||||
```
|
||||
|
||||
`post list` печатает ленту с автором (`[admin]` / `[balda]`) — это **однозначное
|
||||
доказательство**, что бот отвечает. Заменяет «видно в UI», когда GUI недоступен.
|
||||
|
||||
⚠️ Для генерации **человеческого** поста `--local` не годится — нужен токен админа
|
||||
через REST-логин. Либо принять, что проверка идёт по уже существующей ленте.
|
||||
|
||||
### Проверка самих MCP-серверов из docker-сети
|
||||
|
||||
```bash
|
||||
# executor-mcp: initialize → получаем mcp-session-id → tools/list
|
||||
docker exec mattermost curl -s -X POST "http://balda-executor-mcp:8890/mcp" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json, text/event-stream" \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}'
|
||||
# → 200 + mcp-session-id + capabilities:{tools:{}}
|
||||
|
||||
# obsidian-mcp (SSE): жив, если отдаёт event: endpoint
|
||||
docker exec mattermost curl -s -m 5 -i "http://obsidian-mcp:3101/sse"
|
||||
# → 200, Content-Type: text/event-stream, event: endpoint / data: /messages?sessionId=…
|
||||
```
|
||||
|
||||
📌 **`curl` внутри контейнера `mattermost` предпочтительнее `docker run curlimages/curl`** —
|
||||
последний тянет образ из сети и легко уходит в таймаут на медленном канале.
|
||||
📌 Редиректы `-o file` внутри `docker exec` могут не оставить файла на хосте — читать stdout.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Сброс падающих тестов: сначала `git stash`, потом слово «регрессия»
|
||||
|
||||
Метод 2026-09-26. `go test -race ./...` на ветке `feat/mattermost-channel` даёт 7 падений
|
||||
|
||||
Reference in new issue
Block a user