[2026-09-28] eagle: personal/plans/balda-callee-prism-plan.md personal/plans/balda-mm-pr8-review-fixes.md
This commit is contained in:
1 parent
856728ec84
commit
1e44c429ff
2 files changed
+615
No files matched your search
@@ -0,0 +1,396 @@
|
||||
# Балда → Callee + Prism: план перестройки делегирования
|
||||
|
||||
> Создан 2026-09-28. Статус: **ПЛАН, работа не начата.**
|
||||
> Заменяет собой схему «мост :8790 → `claude -p` по подписке» из
|
||||
> [[personal/plans/balda-mm-phase4-agents]] (Фаза 4).
|
||||
> Причина: подписочный CLI требует ручного логина в контейнере (`Not logged in`,
|
||||
> проверено 2026-09-28) — блокер снимается переходом на документированную
|
||||
> архитектуру callee, где провайдер **рендерится из состояния**, а не логинится.
|
||||
|
||||
---
|
||||
|
||||
## 0. Что решено (и на чём основано)
|
||||
|
||||
Alex 2026-09-28: «Давай пока на deepseek с обоих концов соберём, потом будем с
|
||||
колексом решать» + «надо приколбасить prism для таск-трекинга» + «Ты скажи план».
|
||||
|
||||
| # | Решение | Источник |
|
||||
|---|---|---|
|
||||
| 1 | Оба конца — DeepSeek | команда Alex |
|
||||
| 2 | Оркестрация — **callee**, не самодельная | документация callee (см. §1) |
|
||||
| 3 | Таск-трекинг — **prism** + Beads (`bd`) | задание Alex |
|
||||
| 4 | Мост `:8790` — сохранить, перевести с `claude -p` на `callee agent run` | мой выбор, обоснован в §4 |
|
||||
| 5 | Host coding agent — **`callee` CLI в контейнере `balda-executor`** | мой выбор, обоснован в §4 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Что говорит документация (факты, проверено 2026-09-28)
|
||||
|
||||
Источники: `baldaworks/callee` (README + `docs/concepts/architecture.md`,
|
||||
`docs/guides/acp-providers.md`, `docs/agent-kinds/dynamic-role.md`),
|
||||
`baldaworks/prism`, `baldaworks/prism-callee`. Версия callee — **0.22.2** (npm).
|
||||
|
||||
### 1.1 Кто чем владеет — разделение жёсткое
|
||||
|
||||
```
|
||||
Markdown/YAML ресурсы
|
||||
→ decode + schema/semantic/template validation
|
||||
→ registry + static graph resolution ← отвергает циклы, дубли ID, битые ссылки
|
||||
→ resolved root tree + required parameter keys
|
||||
→ workflow runner + ОДИН общий ephemeral state-объект
|
||||
├→ Role / DynamicRole → Norma Runtime → ACP-процесс → свежая сессия
|
||||
├→ Script → локальный shell
|
||||
├→ Human → управляющий терминал
|
||||
└→ TypeSafeJev / OpenRouterDecision → HTTP API
|
||||
→ ОДИН корневой артефакт в stdout
|
||||
```
|
||||
|
||||
| Слой | Владелец | Цитата |
|
||||
|---|---|---|
|
||||
| Граф воркфлоу, валидация, state | **callee** | «validates the whole graph before execution, carries one explicit state object, returns one final artifact» |
|
||||
| Durable-состояние + одобрение | **coding agent** | «The coding agent owns durable state and approval» (prism-callee) |
|
||||
| Исполнение модели | ACP-процесс | «Role/DynamicRole → Norma Runtime → ACP provider process → fresh session» |
|
||||
|
||||
### 1.2 🔴 DeepSeek в списке ACP-провайдеров ОТСУТСТВУЕТ
|
||||
|
||||
Поддерживаемые типы — **закрытый список**:
|
||||
|
||||
| `type` | Команда по умолчанию | Предусловие |
|
||||
|---|---|---|
|
||||
| `codex` | сам callee + `bridge codex` | установленный и авторизованный Codex CLI |
|
||||
| `claude` | `npx -y @zed-industries/claude-code-acp@latest` | Node + npx + креды адаптера |
|
||||
| `opencode` | `opencode acp` | установленный OpenCode |
|
||||
| `copilot` | `copilot --acp --stdio` | установленный Copilot CLI |
|
||||
| `grok` | `grok agent stdio` | Grok CLI или `XAI_API_KEY` |
|
||||
| `cursor` | `agent acp` | установленный Cursor CLI |
|
||||
| `generic_acp` | **нет — требует непустой `cmd`** | любой ACP-исполняемый файл |
|
||||
|
||||
> «The supported type list is closed. **Gemini is not a valid provider type.**»
|
||||
|
||||
**Следствие:** «DeepSeek с обоих концов» напрямую через ACP-тип **не выражается**.
|
||||
Рабочий путь — **`generic_acp` + свой ACP-адаптер к DeepSeek** в `cmd`.
|
||||
Это ключевая развилка плана и главный риск (§7).
|
||||
|
||||
### 1.3 DynamicRole — механизм, который снимает наш блокер
|
||||
|
||||
Все поля провайдера — **шаблоны**, рендерятся из состояния в момент визита:
|
||||
|
||||
```yaml
|
||||
kind: DynamicRole
|
||||
spec:
|
||||
provider:
|
||||
type: '{{ .State.provider }}'
|
||||
model: '{{ .State.model }}'
|
||||
reasoning: '{{ .State.reasoning }}'
|
||||
timeout: '{{ .State.timeout }}'
|
||||
state:
|
||||
provider: codex
|
||||
model: gpt-5.6-sol
|
||||
reasoning: high
|
||||
timeout: 20m
|
||||
```
|
||||
|
||||
| Факт | Значение |
|
||||
|---|---|
|
||||
| Что рендерится | `type`, `cmd`, `model`, `reasoning`, `mode`, `timeout`, каждый `extraArgs` |
|
||||
| Из чего | `.Prompt`, текущий `.Input`, **один** согласованный post-node снапшот `.State`, `.Params` |
|
||||
| Чего **нельзя** | `.Output`, env, файловая система, сеть, часы, рандом, криптография, мутаторы |
|
||||
| Когда валидируется | до поиска и старта процесса; ошибка → визит падает, провайдер не стартует |
|
||||
| Периметр визита | конфиг фиксирован на весь визит, включая REPL-ходы; новый визит Loop рендерит заново |
|
||||
| Переиспользование процесса | одинаковые effective type+cmd → один процесс; **сессия всегда новая** |
|
||||
|
||||
🔴 **Безопасность (цитата):** «State-driven `cmd` and `extraArgs` can select executable
|
||||
code. Do not copy untrusted model output into them.» — `cmd` это **одно имя исполняемого
|
||||
файла**, `extraArgs` — argv-список; shell не участвует.
|
||||
|
||||
### 1.4 Callee и prism — НЕЗАВИСИМЫЕ установки
|
||||
|
||||
> «Each repository installs and validates independently — installing one plugin does
|
||||
> not install the other.»
|
||||
|
||||
| | `baldaworks/prism` | `baldaworks/prism-callee` |
|
||||
|---|---|---|
|
||||
| Что это | плагин в coding agent (Codex/Claude/Grok/Copilot/Cursor) | callee-агенты для тех же воркфлоу |
|
||||
| Требования | coding agent + `bd` (Beads) | coding agent + `bd` + **callee ≥ 0.19.0** |
|
||||
| Команды | `$prism:story`, `$prism:epic`, `$prism:lifecycle` | `$prism-callee:lifecycle` |
|
||||
| Состояние | **Beads (`bd`)** | **Beads (`bd`)** |
|
||||
| Импорт агентов | — | `callee agent import baldaworks/prism-callee --path pack/callee/prism --prefix prism` |
|
||||
|
||||
**Проверка валидности импорта:** `callee agent view prism/lifecycle --json` должен
|
||||
вернуть **kind `Router`**. Если `Sequential` — импорт устарел, обновлять `--force`.
|
||||
|
||||
### 1.5 Prism — жизненный цикл Story/Epic (это и есть таск-трекинг)
|
||||
|
||||
**Story:** Change intent → Specify → (clarify/ready) → Design → Breakdown →
|
||||
**Human approval** → Apply and review → Verify → Verified change.
|
||||
Фазы: `phase:story:specify` → `design` → `breakdown` → `human` → `apply` → `verify`.
|
||||
|
||||
**Epic:** Frame → Architecture → Roadmap → **approval** → Deliver Stories → Validation.
|
||||
Фазы: `phase:epic:frame` → `architecture` → `roadmap` → `approval` → `delivery` → `validation`.
|
||||
|
||||
| Правило | Значение |
|
||||
|---|---|
|
||||
| Дети Epic | только **Stories** |
|
||||
| Дети Story | только **Tasks** |
|
||||
| Вложенные Epic / Task напрямую в Epic | **невалидно** |
|
||||
| Фиксированного диапазона числа детей | **нет** — оценка качественная |
|
||||
| `Apply` | 🔴 **каждый переход требует явной авторизации человека** |
|
||||
| Одобрение Epic → Story | **не переносится** («approval never transfers from an Epic to a Story») |
|
||||
|
||||
---
|
||||
|
||||
## 2. Целевая архитектура
|
||||
|
||||
```
|
||||
Mattermost (Alex)
|
||||
│ «добавь CSV-экспорт в отчёт»
|
||||
▼
|
||||
Балда-оркестратор (Kraken, DeepSeek, balda-mm:0.1.13)
|
||||
│ верхний уровень: приём сообщения, чтение Obsidian, контекст, ответ в чат
|
||||
│ MCP-инструмент executor_run
|
||||
▼
|
||||
MCP-сервер balda-executor-mcp (Kraken, :8890, сеть mattermost_mm-net)
|
||||
│ HTTP + X-Bridge-Secret
|
||||
▼
|
||||
Мост agents-bridge (Mac, :8790, launchd com.agents.bridge)
|
||||
│ 🔴 МЕНЯЕТСЯ: было `claude -p --output-format text`
|
||||
│ стало `callee agent run <resource> --message <prompt>`
|
||||
▼
|
||||
callee CLI (контейнер balda-executor, Mac)
|
||||
│ компилирует граф → валидирует ЦЕЛИКОМ до запуска
|
||||
│ общий state-объект через весь прогон → ОДИН артефакт в stdout
|
||||
├→ Role / DynamicRole → ACP-процесс → DeepSeek (generic_acp + адаптер)
|
||||
├→ Script → локальные детерминированные проверки
|
||||
└→ Human → точка одобрения
|
||||
▼
|
||||
Beads (`bd`) — durable-состояние Story/Epic, переживает сессии
|
||||
▼
|
||||
артефакт → Балда → Mattermost
|
||||
```
|
||||
|
||||
### Разделение расходов
|
||||
|
||||
| Роль | Где | Провайдер | Стоимость |
|
||||
|---|---|---|---|
|
||||
| Оркестратор верхнего уровня | Kraken | DeepSeek `deepseek-chat` | дёшево |
|
||||
| Исполнитель-роли callee | Mac, контейнер | DeepSeek (через адаптер) | дёшево |
|
||||
| Позже | — | Codex (подписка) | решается после |
|
||||
|
||||
📌 Формулировка «DeepSeek с обоих концов» = **обе стороны на DeepSeek**, подписочный
|
||||
Codex подключается **позже** заменой `State.provider` в DynamicRole — без правки графа.
|
||||
|
||||
---
|
||||
|
||||
## 3. Фазы работ
|
||||
|
||||
### Фаза C0 — предварительная проверка (30-40 мин)
|
||||
|
||||
Цель: понять, существует ли готовый ACP-адаптер к DeepSeek. Если да — план короче
|
||||
на целую фазу.
|
||||
|
||||
1. Разведка: есть ли ACP-адаптер, принимающий OpenAI-совместимый endpoint.
|
||||
Кандидаты для проверки: `@normahq/*acp*`, `@zed-industries/*acp*`, npm-поиск по `acp`.
|
||||
2. Если готового нет — оценить объём своего (см. C2).
|
||||
3. Проверить актуальную версию callee и что `0.22.2` умеет `generic_acp`
|
||||
(документация говорит «да», версия из npm — свежая).
|
||||
|
||||
**Гейт:** решение «адаптер готов / пишем свой» зафиксировать в этом доке до C2.
|
||||
|
||||
### Фаза C1 — установка на Mac (1-2 ч)
|
||||
|
||||
Всё ставится **в контейнер `balda-executor`** (там Node 24 — родная среда callee).
|
||||
Проверено сейчас: ни `callee`, ни `bd` нет ни в контейнере, ни на хосте.
|
||||
|
||||
| # | Шаг | Проверка |
|
||||
|---|---|---|
|
||||
| 1 | `npm i -g @baldaworks/callee` в контейнере | `callee --version` |
|
||||
| 2 | `callee setup codex` (или иной host agent) → 6 стартовых ресурсов | `callee agent list` содержит `workflows/investigate` |
|
||||
| 3 | Установить Beads `bd` | `bd where` |
|
||||
| 4 | Импорт prism-пака | `callee agent import baldaworks/prism-callee --path pack/callee/prism --prefix prism` |
|
||||
| 5 | 🔴 Проверить импорт | `callee agent view prism/lifecycle --json` → **kind `Router`** (не `Sequential`!) |
|
||||
| 6 | Прогнать стартовый `investigate` | `callee agent run workflows/investigate --message "..."` |
|
||||
|
||||
📌 Персистентность: установки в контейнере живут в его слое. Если понадобится
|
||||
переживать пересборку — вынести `npm`-глобаль в том (решение принять на C1, п.1).
|
||||
|
||||
### Фаза C2 — провайдер DeepSeek для callee (главная работа, 4-8 ч)
|
||||
|
||||
Два варианта, выбор по результату C0:
|
||||
|
||||
**Вариант C2-A — готовый адаптер найден.** Описать `Role` со `spec.provider.type:
|
||||
generic_acp`, `cmd: <адаптер>`, `extraArgs` с endpoint DeepSeek. Проверить один
|
||||
`Role`-визит вживую.
|
||||
|
||||
**Вариант C2-B — пишем адаптер.** Минимальный ACP-процесс, транслирующий ACP-сессии
|
||||
в OpenAI-совместимый `chat/completions` DeepSeek. Опорный материал — наш прошлый
|
||||
опыт: патч `tool_calls` в `norma-local` (`pkg/runtime/hostedagent/openai.go`,
|
||||
коммит `91c2c0a`) и PR `normahq/runtime#4`. 🔴 **Там же грабли:** DeepSeek отдаёт
|
||||
`function.arguments` **двойной сериализацией** (JSON-строкой внутри JSON-строки).
|
||||
См. [[personal/projects/balda/balda-valera]].
|
||||
|
||||
**Гейт C2:** один `Role`-визит к DeepSeek отрабатывает вживую, в логе виден
|
||||
`role_model`, `role_reasoning` (документация: до успешного рендера эти поля
|
||||
опускаются — значит их появление = рендер прошёл).
|
||||
|
||||
### Фаза C3 — подключение воркфлоу к мосту (2-3 ч)
|
||||
|
||||
1. Бэкап `~/Developer/agents-bridge/executor.py`.
|
||||
2. Замена argv-таблицы `PROVIDERS`:
|
||||
|
||||
```python
|
||||
PROVIDERS = {
|
||||
# было: "claude": ["claude", "-p", "--output-format", "text"],
|
||||
# стало:
|
||||
"callee": ["callee", "agent", "run", "prism/lifecycle", "--message"],
|
||||
}
|
||||
```
|
||||
|
||||
3. Промпт передаётся последним аргументом (как сейчас).
|
||||
4. 🔴 `EXECUTOR_PROVIDER` в `~/Docker/balda-executor/.env` меняется на `callee`.
|
||||
5. Перезапуск моста: `launchctl bootout` + `bootstrap` (не `kickstart` — см. питфолл §6).
|
||||
6. Прогон задачи через мост (`POST /task` + `Idempotency-Key`), проверка `exit_code`.
|
||||
|
||||
### Фаза C4 — таск-трекинг prism/Beads (1-2 ч)
|
||||
|
||||
1. `bd` инициализируется в рабочей области `/agents`.
|
||||
2. Прогон `prism:story` на реальной задаче: ожидаемый поток
|
||||
`specify → design → breakdown → human → apply → verify`.
|
||||
3. 🔴 Проверить, что пауза на `phase:story:human` **доходит до Mattermost** —
|
||||
иначе точка одобрения недостижима и воркфлоу встанет намертво. Это ограничение
|
||||
известно: `ask` в Балде поддержан **только** для Telegram и Slackagent, для
|
||||
Mattermost фейлится закрыто. См. §6 «Питфолл 1» и
|
||||
`docs/architecture/interactive-questions.md`.
|
||||
4. Проверить durable: прервать сессию, возобновить Story — состояние в Beads на месте.
|
||||
|
||||
### Фаза C5 — сквозная приёмка (1 ч)
|
||||
|
||||
По правилам приёмки этого проекта — **от учётки Alex**, не подставным аккаунтом:
|
||||
|
||||
```
|
||||
@balda добавь CSV-экспорт в отчёт
|
||||
→ Specify (вопросы, если нужны)
|
||||
→ Design → Breakdown
|
||||
→ Human approval ← 🔴 Alex подтверждает
|
||||
→ Apply → Verify
|
||||
→ ответ в Mattermost + артефакт
|
||||
```
|
||||
|
||||
Доказательство — лента канала `qentra:balda-test`, автор поста = `cbwsorzwoigbtrdnteugnx4aqo`.
|
||||
Метод чтения ленты и постинга от владельца — §17 в
|
||||
[[personal/plans/balda-mm-phase4-agents]].
|
||||
|
||||
---
|
||||
|
||||
## 4. Обоснование моих решений (Alex делегировал выбор)
|
||||
|
||||
**Почему host coding agent — callee CLI в контейнере `balda-executor`:**
|
||||
там уже Node 24 (родная среда callee), там же монтируется `~/agents` (рабочая
|
||||
область), и оттуда мост уже умеет запускать команды через `docker exec`. Ставить
|
||||
callee на хост Mac — значит плодить вторую среду и разбираться с PATH в launchd.
|
||||
|
||||
**Почему мост сохраняем, а не выбрасываем:**
|
||||
документация callee не требует HTTP-прослойки — Балда могла бы вызывать `callee
|
||||
agent run` и через свой MCP. Но MCP-сервер `balda-executor-mcp` уже работает и
|
||||
принят (§19.5), туннель `agents-bridge.qentra.top` уже заведён, идемпотентность,
|
||||
таймауты и лимит в 1 задачу уже реализованы и проверены (6 проверок). Выбросить их
|
||||
во имя «чище по доке» — значит потерять работающий слой ради эстетики. Мост
|
||||
остаётся транспортом, меняется **одна таблица argv**.
|
||||
|
||||
**Почему не «вторая Балда»:**
|
||||
дублирование оркестратора даёт два источника истины по состоянию сессии и
|
||||
конфликты локаторов. Документация к такому не призывает.
|
||||
|
||||
---
|
||||
|
||||
## 5. Что снимается этим планом
|
||||
|
||||
| Дефект | Как снимается |
|
||||
|---|---|
|
||||
| 🔴 `Not logged in · Please run /login` в контейнере-исполнителе (найден 2026-09-28) | DeepSeek-провайдер рендерится из состояния — логина в подписочный CLI вообще нет |
|
||||
| Ручной OAuth-логин Alex'а в контейнер | не нужен |
|
||||
| Потеря логина при `docker compose up -d` (`.claude.json` не в хост-монтировании) | неактуально |
|
||||
| Самодельная оркестрация в промптах | заменяется валидируемым графом callee |
|
||||
| Нет таск-трекинга | prism + Beads |
|
||||
|
||||
---
|
||||
|
||||
## 6. Питфоллы
|
||||
|
||||
1. 🔴 **`Human`-узел против Mattermost.** Документация callee/`prism:story` требует
|
||||
пауз на одобрение (`phase:story:human`), а в Балде `ask` для Mattermost фейлится
|
||||
закрыто. Пока не решено — воркфлоу может встать намертво на первой же точке
|
||||
одобрения. Варианты: (а) прокинуть человеческий ответ через мой мост как
|
||||
отдельный HTTP-вызов; (б) на первом этапе использовать воркфлоу без `Human`;
|
||||
(в) чинить `ask` в MM-адаптере (отдельный PR). **Решать до C4.**
|
||||
2. 🔴 **`PROVIDERS` в мосте — argv-список, замена «в лоб» рвёт передачу промпта.**
|
||||
Сейчас: `["docker","exec","-w","/agents","@container", *base, prompt]`. Промпт
|
||||
должен остаться **одним** последним элементом.
|
||||
3. 🔴 **`.env` не перебивает plist.** `--host` жёстко в `ProgramArguments`; смена
|
||||
аргументов требует `bootout` + `bootstrap`. Проверка — `lsof -nP -iTCP:8790
|
||||
-sTCP:LISTEN` (должно быть `*:8790`).
|
||||
4. ⚠️ **`callee agent view prism/lifecycle --json` должен вернуть `Router`.**
|
||||
`Sequential` = устаревший импорт, лечится `--force`.
|
||||
5. ⚠️ **Двойная сериализация `function.arguments` у DeepSeek** — повторится в любом
|
||||
своём адаптере (C2-B). Готовая логика разбора есть в `norma-local`.
|
||||
6. ⚠️ **Установки в контейнере не переживают пересборку образа** — если callee/bd
|
||||
окажутся нужны после `docker compose build`, их надо либо заложить в `Dockerfile`,
|
||||
либо вынести в том.
|
||||
7. ⚠️ **`docker-compose` (дефис) на Mac, `docker compose` (пробел) на Kraken** —
|
||||
проверено, легко перепутать.
|
||||
8. ⚠️ **Provider `cmd` — одно имя исполняемого файла, не shell-строка.** Любые
|
||||
аргументы — только в `extraArgs`, иначе провайдер не стартует.
|
||||
9. ⚠️ **Никогда не маршрутизировать вывод модели в `cmd`/`extraArgs`** — прямой
|
||||
путь к исполнению произвольного кода (предупреждение документации).
|
||||
|
||||
---
|
||||
|
||||
## 7. Риски
|
||||
|
||||
| Риск | Оценка | Митигация |
|
||||
|---|---|---|
|
||||
| 🔴 **ACP-адаптера к DeepSeek может не существовать** | **высокий** | Фаза C0 решает это до основных работ. Если писать свой — 4-8 ч; опора на готовый патч `norma-local` |
|
||||
| 🔴 `Human`-узел не доходит через Mattermost | **высокий** | Решить до C4 (три варианта в §6.1) |
|
||||
| Импорт `prism/*` устаревает при обновлении | средний | проверять `kind: Router` после каждого импорта |
|
||||
| callee обновляется (0.22.2 → выше) и меняет контракт | средний | пиннить версию в ресурсах; следить за `docs/` |
|
||||
| Мост теряет работоспособность после смены argv | низкий | бэкап `executor.py`, откат одной правкой |
|
||||
| Стоимость DeepSeek растёт (обе стороны на нём) | низкий | оркестратор уже на DeepSeek; исполнитель-роли — короткие визиты |
|
||||
| Установка в контейнер не переживёт пересборку | низкий | решается томом или `Dockerfile` (§6.6) |
|
||||
|
||||
---
|
||||
|
||||
## 8. Порядок и что НЕ делать
|
||||
|
||||
**Порядок:** C0 → (решение) → C1 → C2 → C3 → C4 → C5.
|
||||
C0 и C1 независимы — можно параллелить.
|
||||
|
||||
**НЕ трогать в `~/Developer/balda`:**
|
||||
`go.mod`, `Dockerfile.release`, `go.sum`, `internal/apps/balda/app.go`,
|
||||
`internal/apps/balda/preflight.go` — это вне PR-дерева, нужны для сборки стенда
|
||||
Kraken. `git checkout -- .` / `stash` / `reset --hard` их снесёт. См.
|
||||
[[personal/plans/balda-mm-pr-plan]] §5.
|
||||
|
||||
**НЕ начинать работу** без явного «поехали» — правило проекта: план → апрув → работа.
|
||||
**Ничего не удалять** — образы для отката, диагностические контейнеры и старые теги
|
||||
не трогать без отдельного подтверждения.
|
||||
|
||||
---
|
||||
|
||||
## 9. Открытые вопросы
|
||||
|
||||
1. 🔴 **Есть ли готовый ACP-адаптер к DeepSeek** — Фаза C0, блокирует C2.
|
||||
2. 🔴 **Как проводить `Human`-одобрение через Mattermost** — §6.1, блокирует C4.
|
||||
3. Нужен ли подписочный Codex в контуре вообще, или после Colex'а схема меняется
|
||||
целиком (Alex: «потом будем с колексом решать»).
|
||||
|
||||
---
|
||||
|
||||
## Связанные заметки
|
||||
|
||||
- [[personal/plans/balda-mm-phase4-agents]] — Фаза 4 (предыдущая схема, мост :8790)
|
||||
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план Фаз 1-5
|
||||
- [[personal/plans/balda-mm-pr-plan]] — Фаза 2 (PR #8)
|
||||
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅ бот отвечает)
|
||||
- [[personal/projects/balda/balda-valera]] — патч `tool_calls` для DeepSeek (опора для C2-B)
|
||||
- [[personal/tech/agents-bridge]] — мост :8790
|
||||
- [[personal/tech/balda-executor-mcp]] — MCP-сервер делегирования
|
||||
- [[personal/projects/balda/balda-mattermost-connector]] — MM-коннектор
|
||||
@@ -0,0 +1,219 @@
|
||||
# Balda — PR #8 review fixes: slash commands, /locator, group DM session
|
||||
|
||||
> Создан 2026-09-28. Статус: **ПЛАН — ожидает подтверждения Alex.**
|
||||
> Источник: ревью Alex на `baldaworks/balda#8` (Mattermost transport).
|
||||
> Часть [[personal/plans/balda-mm-pr-plan]] (PR #8). Ветка **`feat/mattermost-channel`**.
|
||||
> Ветки / кода ещё не трогал — только чтение и сверка.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
|
||||
Ревью право по всем трём пунктам — **проверено в коде**, не на слово.
|
||||
|
||||
| # | Дефект | Подтверждение в коде (коммит `9f1e6c9c`) |
|
||||
|---|---|---|
|
||||
| Б1 | Slash-команды не доходят: `/start` и прочие обрабатываются только из WS-события `posted` | `channel/mattermost/ingress.go` — `switch event.Event` знает только `posted`; отдельного HTTP-приёмника slash нет вовсе. У Slack он есть: `slackagent/server.go` → `mux.HandleFunc(commandsPath, h.handleCommands)` |
|
||||
| Б2 | `/locator` всегда падает: `locatorref.Parse` не знает `mattermost` | `locatorref/locatorref.go:52-62` — `switch channelType` покрывает только `telegram` / `zulip` / `slackagent`; ветки `mattermost` нет → `unsupported channel type` |
|
||||
| В3 | Групповой DM расщепляется по отправителям | `locator_codec.go:102-118` — `dmAddressKey` = `d:<ChannelID>` (один чат = один ключ), а `dmSessionID` при непустом `UserID` = `mm-dm-<hash(UserID)>` (**автор**, не чат) |
|
||||
|
||||
🔴 **Ключевой вопрос по Б1 — не «как», а «что именно».** См. §2: две реализации, они
|
||||
архитектурно разные. Нужно решение Alex **до** написания кода.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сверка с докой (что именно было заявлено и оказалось неправдой)
|
||||
|
||||
`docs/commands.md:20-22` (коммит `58f27120`) утверждает:
|
||||
|
||||
> Mattermost uses one slash command per action, such as `/locator`, and accepts
|
||||
> them in direct messages and in channels where the bot is mentioned.
|
||||
|
||||
И таблица (`docs/commands.md:26+`) ставит **`/help`, `/topic`, `/goalkeeper`, `/auto`,
|
||||
`/usage`, `/close`, `/cancel`, `/user` → «yes»** для Mattermost.
|
||||
|
||||
⚠️ Всё это **ложь текущей реализации** — ни одна slash-команда в MM не обрабатывается.
|
||||
`/start` в таблице стоит `no`, но это тоже неверно: в MM `/start` — единственный
|
||||
документированный путь первичной привязки владельца, и он не работает.
|
||||
|
||||
📌 **Правка доков — обязательная часть фикса, а не «потом».** Колонка Mattermost в
|
||||
таблице доступности должна стать следствием кода, который реально есть.
|
||||
|
||||
---
|
||||
|
||||
## 2. Б1 — slash-команды. Решение: вариант A (HTTP-приёмник). Вопрос снят.
|
||||
|
||||
> **Решение Alex 2026-09-28:** «Тебе что, не очевидно в прод-качестве?» — выбора нет.
|
||||
> B (убрать slash из доки) = **откат функциональности**, а не фикс. Прод-качество = A.
|
||||
> Спрашивать было нечего. Ниже сохранён разбор обоих путей как обоснование.
|
||||
|
||||
Mattermost **не публикует** slash-команды как посты (в отличие от Telegram/Zulip) — поэтому
|
||||
текущий WS-путь их физически не видит. Есть два штатных механизма, и они взаимоисключающие
|
||||
по архитектуре:
|
||||
|
||||
📌 **Факт, подтверждённый докой MM** (`docs.mattermost.com/integrations-guide/slash-commands`):
|
||||
«if you type in a slash command it will execute **without posting a message**,
|
||||
whereas an outgoing webhook is only triggered by posted messages». То есть WS-события
|
||||
`posted` для slash **не будет** — ревью право на 100%.
|
||||
|
||||
🔴 **Что уже работает и что отсутствует — точно:**
|
||||
| Компонент | Статус |
|
||||
|---|---|
|
||||
| `mattermostInboundHandler.HandleCommand` (`handlersfx/mattermost_ingress.go`) — публикация в `commandcmd` | ✅ **написан и корректен** |
|
||||
| `ParseCommand` (`inbound.go:223`) — разбор `/name args` из текста | ✅ написан |
|
||||
| вызов `ParseCommand` из `processPosted` (`ingress.go`) — **mention-путь** | ✅ работает |
|
||||
| **HTTP-путь для slash** (MM POST → наш эндпоинт) | ❌ **отсутствует целиком** |
|
||||
|
||||
Значит фикс Б1 — **не переписывать command-машину**, а добавить к ней **второй вход**.
|
||||
Command-машина исправна; нет только способа донести до неё slash-запрос.
|
||||
|
||||
### Вариант A — Slack-style: исходящий webhook + HTTP-приёмник (ВЫБРАН)
|
||||
|
||||
Slash-команда MM настраивается на **Request URL** — MM делает `POST` в наш HTTP-эндпоинт.
|
||||
Это ровно то, что уже сделано для Slack, значит есть рабочий образец в репо.
|
||||
|
||||
Что дублируется из `slackagent/`:
|
||||
- HTTP-сервер с отдельным путём `/mattermost/commands` (`server.go:129`-аналог);
|
||||
- `handleCommands` → `decodeCommandRequestWithRegistry` → `commandcmd.Request` → common handler;
|
||||
- ответ в MM — JSON `{"response_type":"ephemeral"|"in_channel","text":"..."}` (MM-специфично,
|
||||
отличается от Slack `{"text":...}`);
|
||||
- **верификация**: у MM slash-команда подписывается полем `token` в теле запроса
|
||||
(не HMAC-подпись, как у Slack) — сравнение с токеном из конфига;
|
||||
- lifeзycle: тот же `appports.TransportLifecycleStage`, что у WS-ingress (либо расширить
|
||||
существующий `Ingress`, либо отдельный `Server` — см. §6).
|
||||
|
||||
Плюсы: `/start`, `/topic`, `/goalkeeper`, `/auto`, `/usage`, `/close`, `/cancel`, `/user`
|
||||
работают **в полном объёме**, как в доке и таблице. Минусы: нужен публичный HTTP-эндпоинт
|
||||
для MM и **новый ключ конфига** (slash-токен) + правки доков по настройке.
|
||||
|
||||
### Вариант B — «mention-форма»: команды только текстом в посте (ОТКЛОНЁН)
|
||||
|
||||
Оставить один WS-путь, а из доки/таблицы убрать slash-обещания: писать команды как
|
||||
`@balda /locator` (текст поста, `ParseCommand` их уже разбирает — `ingress.go`/`inbound.go`).
|
||||
|
||||
Почему отклонён: это **не фикс, а удаление уже обещанной функциональности**. `/start owner=<token>`
|
||||
в MM-развёртывании — штатный вход владельца, и `docs/mattermost.md:117-119` прямо описывает его
|
||||
как slash-команду в DM. Вариант B = переписать доку под урезанный код. Для «прод-качества» —
|
||||
неприемлемо.
|
||||
|
||||
### Итог
|
||||
|
||||
**A.** В доке уже обещаны все slash-команды, у Slack есть готовый образец
|
||||
(`slackagent/server.go:129` → `mux.HandleFunc(commandsPath, h.handleCommands)` + `handleCommands`
|
||||
→ `decodeCommandRequestWithRegistry` → `commandcmd.Request`), а `/start owner=<token>` — штатный
|
||||
вход владельца в MM-развёртывании.
|
||||
|
||||
---
|
||||
|
||||
## 3. Б2 — `/locator`: добавить `mattermost` в `locatorref`
|
||||
|
||||
Механически, по образцу `slackagentLocatorFromAddressKey` (там та же форма `c:`/`t:`):
|
||||
|
||||
| Что | Куда |
|
||||
|---|---|
|
||||
| ветка `case baldamattermost.ChannelType:` в `switch channelType` | `locatorref/locatorref.go:52-62` |
|
||||
| `mattermostLocatorFromAddressKey(addressKey string)` — разбор `c:<channel>`, `c:<channel>:<root>`, `d:<channel>` | рядом с `slackAgentLocatorFromAddressKey` |
|
||||
| конструкторы `NewMattermostChannelLocator` / `NewMattermostDMLocator` (по образцу `NewSlackAgent*`) | там же |
|
||||
| **импорт** `channel/mattermost` — ⚠️ проверить архитектурное правило | см. ниже |
|
||||
|
||||
🔴 **Питфолл архитектуры.** `AGENTS.md` прямо требует: «Keep `locatorref` **independent**
|
||||
from concrete transport adapter packages». Импорт `channel/mattermost` в `locatorref` это
|
||||
**нарушает**. Как это решено сейчас: `locatorref` импортирует `telegramref` (отдельный
|
||||
тонкий пакет-ref), а для zulip/slack — **дублирует константы строкой**:
|
||||
```go
|
||||
channelTypeZulip = "zulip"
|
||||
channelTypeSlackAgent = slackagentref.ChannelType // ← тонкий ref-пакет
|
||||
```
|
||||
📌 **Значит правильная форма для MM — тонкий пакет `mattermostref`** (или константа-строка
|
||||
в `locatorref`), **не** импорт транспорта. Решение сверить с `slackagentref`/`telegramref`,
|
||||
прежде чем писать код. Это тот случай, где `go-arch-lint` даст красное на наивной реализации.
|
||||
|
||||
**Ещё два следствия Б2, которые надо закрыть тем же изменением:**
|
||||
- `internalmcp/manager.go:409` — `locatorref.Parse(channelType + ":" + addressKey)`. Любой
|
||||
MM-таргет там сейчас падает.
|
||||
- `envelopetarget/resolve.go:62` — `locatorref.Parse(target.Key)`. То же самое →
|
||||
`balda.scheduler.jobs` с `target: locator` для MM **не работает**.
|
||||
|
||||
⚠️ Это и есть вторая половина фразы ревью «также исключает Mattermost из config targets,
|
||||
использующих тот же парсер». Правка только `handler.go` задачу не закрывает.
|
||||
|
||||
---
|
||||
|
||||
## 4. В3 — групповой DM: ключ чата вместо ключа автора
|
||||
|
||||
Сейчас:
|
||||
```go
|
||||
func dmAddressKey(a LocatorAddress) string { return fmt.Sprintf("d:%s", url.PathEscape(a.ChannelID)) } // ЧАТ
|
||||
func dmSessionID(a LocatorAddress) string {
|
||||
if a.UserID != "" { return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.UserID)) } // АВТОР ← баг
|
||||
return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.ChannelID))
|
||||
}
|
||||
```
|
||||
DM (`D`) приходит с `UserID` → сессия по автору. **Групповой DM (`G`) канал общий, а авторов
|
||||
много** → у каждого участника своя сессия. Ревью право.
|
||||
|
||||
Правильная форма — **различать `D` и `G` явно**, а не «если есть UserID»:
|
||||
|
||||
| Тип канала MM | AddressKey | SessionID |
|
||||
|---|---|---|
|
||||
| `D` (1:1) | `d:<channel_id>` | `mm-dm-<hash(channel_id)>` (чат = 2 участника, стабильно) |
|
||||
| `G` (групповой) | `d:<channel_id>` | `mm-gdm-<hash(channel_id)>` — **по чату** |
|
||||
|
||||
⚠️ Соображения, которые надо учесть при реализации:
|
||||
- **Обратная совместимость:** существующие 1:1 DM-сессии на Kraken имеют id
|
||||
`mm-dm-<hash(UserID)>` (перепривязка сломает их → новые сессии, старые «осиротеют»).
|
||||
Либо принять (тестовая инсталляция), либо оставить для `D` форму по `UserID`.
|
||||
- `IsDirectChannelType` (`inbound.go`) уже знает `D`/`G` — переиспользовать.
|
||||
- Точка входа — `LocatorForPost` (`inbound.go`), она и должна различать `D`/`G`.
|
||||
- Правка ведёт к обновлению `locator_codec_test.go` (тест на изоляцию thread↔channel
|
||||
придётся дополнить тестом «два автора в `G` → одна сессия»).
|
||||
|
||||
---
|
||||
|
||||
## 5. Порядок работ (порядок важен: сначала решается A/B)
|
||||
|
||||
| # | Шаг | Файл | Тип |
|
||||
|---|---|---|---|
|
||||
| 0 | **Решение Alex: вариант A или B для Б1** | — | ⛔ блокирует всё остальное |
|
||||
| 1 | Б2: `mattermost` в `locatorref` (через ref-пакет, не импорт транспорта) | `locatorref/`, новый `mattermostref`? | код |
|
||||
| 2 | Б2-хвост: тесты на `internalmcp/manager.go` и `envelopetarget/resolve.go` путях | тесты | код |
|
||||
| 3 | В3: `D`/`G` различать в `dmSessionID` + `LocatorForPost` | `locator_codec.go`, `inbound.go` | код |
|
||||
| 4 | В3: тест «два автора в групповом DM → одна сессия» | `locator_codec_test.go` | тест |
|
||||
| 5 | Б1 (если A): HTTP-приёмник slash + `token`-верификация + DI + конфиг-ключ | `channel/mattermost/`, `mattermostfx/`, `app.go`, `balda.yaml` | код |
|
||||
| 6 | Б1 (если A): `docs/mattermost.md` — настройка slash Request URL и токена | доки | доки |
|
||||
| 7 | Правка таблицы команд `docs/commands.md` — колонка Mattermost = **реальность**, не обещание | доки | доки |
|
||||
| 8 | Гейты: `go build` / `vet` / `golangci-lint` / `go-arch-lint` / `go test -race` | — | проверка |
|
||||
| 9 | Push в `feat/mattermost-channel` → **дождаться CI 8/8** | — | проверка |
|
||||
| 10 | Обновить Obsidian: [[personal/plans/balda-mm-pr-plan]], [[personal/projects/balda/balda-mattermost-connector]] | доки | доки |
|
||||
|
||||
📌 Коммиты — Conventional Commits, правки — **дельта-апплаером** (`tools/run-project-deltas.sh`,
|
||||
`check` → `apply`), не sed/in-place (правило Alex).
|
||||
|
||||
📌 Ветка та же (`feat/mattermost-channel`) — правки уедут в открытый PR #8 и **сбросят
|
||||
пройденный CI**. Это ожидаемо: PR не смержен, ревью просит исправлений.
|
||||
|
||||
---
|
||||
|
||||
## 6. Риски / питфоллы, которые уже видны
|
||||
|
||||
1. 🔴 **`locatorref` не должен импортировать `channel/mattermost`** — `go-arch-lint` даст
|
||||
`composition_root`/boundary-ошибку. Форма фикса — тонкий ref-пакет, как `slackagentref`.
|
||||
2. 🔴 **Slash-команды MM требуют публичного HTTPS** для Request URL. Если стенд на Kraken
|
||||
без публичного адреса — нужен туннель либо вариант B. **Проверить до выбора A.**
|
||||
3. ⚠️ **MM slash-запросы возвращаются в течение 3 с**, иначе MM показывает timeout. Тяжёлые
|
||||
команды (`/goalkeeper`) обязаны отвечать сразу, а результат доставлять постом — как у Slack.
|
||||
4. ⚠️ **`token` в slash-запросе — не HMAC.** Другая модель верификации, чем у Slack; копировать
|
||||
`signedSlackRequest`-логику нельзя.
|
||||
5. ⚠️ **Обратная совместимость DM-сессий** (§4) — смена id осиротит существующие сессии.
|
||||
6. ⚠️ Изменение Б3 меняет поведение `transport_user_id` в persisted-метаданных сессии —
|
||||
перепроверить тест `TestLocatorForPostScopesReplyToThreadRoot` и «чужой user_id» из
|
||||
§«Два не-блокера» [[personal/plans/balda-mm-pr-plan]].
|
||||
|
||||
---
|
||||
|
||||
## Связанные заметки
|
||||
|
||||
- [[personal/plans/balda-mm-pr-plan]] — PR #8, состояние, деплой, приёмка
|
||||
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2 (код коннектора)
|
||||
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы, дельта-апплаер, arch-lint
|
||||
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план
|
||||
Reference in new issue
Block a user