Files
obsidian-vault/personal/plans/balda-mm-pr-plan.md
T

378 lines
25 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Balda — Mattermost connector: PR-подготовка
> Обновлён 2026-09-28. Обновлён 2026-09-29 (ревизия: PR СМЕРЖЕН, ветки снесены).
> Статус: **✅ PR #8 СМЕРЖЕН 2026-09-29T06:06:48Z — `bf4a6ae4`.** MM-транспорт в `upstream/main`.
> https://github.com/baldaworks/balda/pull/8
> 🔧 **2026-09-28: правки по ревью применены — голова `05342c5a` (9 коммитов), CI 8/8 PASS.**
> Ревью `metalagman` опубликовано в PR (`reviews`, state `COMMENTED`, inline-комментов **0**).
> Закрыты: slash-команды (HTTP-приёмник + setup в доке), `/locator` (`locatorref` +
> guard-тест), групповой DM (сессия по чату + тест двух отправителей) + **регистрация в
> `ScopeResolver`, которой не было в PR**. Сверка построчно —
> [[personal/plans/balda-mm-pr8-review-fixes]] §0a.
> 📌 **Мерж прошёл без `APPROVED`-ревью** — `reviewDecision` оставался пустым, PR влит вручную
> мейнтейнером. Мерж-коммит `bf4a6ae4` = merge `86643007` (голова PR, 29 коммитов,
> 72 файла, +7921/−74) в `93c7ac89` (`upstream/main` на тот момент).
> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2 → PR в апстрим).
---
## 0. ✅ ПОСТ-МЕРЖ: git-гигиена (2026-09-29)
Ветки снесены, prune выполнен, worktree почищены, `/tmp` и форки `norma` вычищены.
Полный разбор — [[personal/plans/balda-runtime-rebuild-mm-validation]] блок «2026-09-29».
```bash
cd ~/Developer/balda
git checkout main && git pull --no-rebase origin main
git branch -D feat/mattermost-channel feat/zulip-transport feat/zulip-transport-intermediate \
backup/our-main-before-upstream feat/zulip-events-polling
git push origin --delete feat/mattermost-channel feat/zulip-transport
git remote prune origin && git remote prune upstream
# ⚠️ СНАЧАЛА worktree, потом ветки — иначе branch -D падает
git worktree remove --force <11 путей> && git worktree prune
git branch -D codex/mattermost-prod-fix
rm -rf ~/Developer/balda/personal
rm -rf /tmp/balda-* /private/tmp/balda-* # ~130 файлов
rm -rf ~/Developer/norma-local ~/Developer/norma-fork ~/Developer/norma-upstream-check # 291 МБ
```
**Итог: `~/Developer/balda` = только `main`, один worktree, чистое дерево** (кроме `.gitignore`).
🔴 **Питфолл: `git worktree list` показывал 10 несуществующих путей** `/private/tmp/balda-mm-pr8-*`.
Каталоги снесены из `/tmp`, но `.git/worktrees/<name>` живы и **держат ветку**:
`git branch -D` → `error: cannot delete branch 'X' used by worktree at '/path'`.
Порядок обязателен: `worktree remove --force` → `worktree prune` → `branch -D`.
🔴 **Питфолл при сносе форков `norma`:** в `~/Developer/norma-fork` лежал **непушенный** коммит
`70ff876 feat: support custom OpenAI base URL via OPENAI_BASE_URL env var` (ветка
`feat/custom-openai-base-url`). Это тот самый `OPENAI_BASE_URL`, на котором держится запуск Балды.
Проверять `git log --branches --not --remotes --oneline` **перед** `rm -rf` любого репо.
📌 **`.gitignore` изменён (не закоммичен):** добавлены `/personal/` и `/tools/` — доки Obsidian
дважды попадали в корень репо из-за `write_file` с vault-относительным путём при cwd = репо.
📌 **`~/Developer/balda/personal/` был дубликатом вотла, не уникальным контентом** — обе версии
в `~/obsidian` новее по размеру и дате. Сверять перед сносом, а не удалять по имени файла.
⚠️ **Локальная `main` отстала от `upstream/main`.** MM-транспорт ушёл в **апстрим**, не в
`origin/mallexxx/balda`, поэтому `git pull origin main` → `Already up to date` — это не «всё чисто»,
а «origin не содержит мержа». Догонять через `git merge upstream/main` (не rebase — `AGENTS.md`).
Решение по этому шагу **не принято**.
## 1. Состояние (актуально на 2026-09-29)
| | |
|---|---|
| PR | **baldaworks/balda#8** — `feat(mattermost): add Mattermost channel transport` |
| Состояние | ✅ **MERGED** 2026-09-29T06:06:48Z · merge-commit **`bf4a6ae4`** |
| База / голова | `baldaworks/balda:main` ← `mallexxx:feat/mattermost-channel` @ `86643007` |
| Объём на момент мержа | **72 файла, +7921 / −74**, 29 коммитов |
| Ревью | `COMMENTED` от `metalagman`, `APPROVED` не было — мерж вручную |
| Ветка в форке | 🗑️ удалена локально и в `origin` 2026-09-29 |
```
architecture-lint pass 2m2s
ingress-e2e pass 30s
jetstream-integration pass 1m0s
lint pass 2m46s
postgres-integration pass 57s
projection-replay pass 26s
sqlite-integration pass 1m15s
test pass 6m0s ← главный: полный go test на чистом раннере
```
**Ревью состоялось 2026-09-28** (`metalagman`, state `COMMENTED`) — правки применены, голова
`05342c5a`, CI 8/8 на новом коммите. См. §0a [[personal/plans/balda-mm-pr8-review-fixes]].
🔴 **Блокер: `reviewDecision` пустой = ревью было COMMENTED, а не APPROVED.** Мержа не будет,
пока ревьюер не сменит состояние на approval. Наш ход — после правок дождаться ре-ревью;
периодически перепроверять `gh pr view 8 --repo baldaworks/balda --json reviewDecision,reviews`.
🔴 **Целевой репозиторий — `baldaworks/balda`, НЕ `normahq/balda`.** Проверено через API,
не по названию remote'а:
```
gh repo view mallexxx/balda --json parent
→ parent: baldaworks/balda ← форк от него
gh repo view normahq/balda --json owner
→ owner.id: O_kgDOEgAHrw (= baldaworks) ← редирект, не отдельный репо
```
`origin` = `mallexxx/balda` (форк), `upstream` = `normahq/balda` (редирект на baldaworks).
PR создавать **в `baldaworks/balda`**, head указывать как `mallexxx:<branch>` — не полный
URL форка, иначе `gh` путается.
---
## 2. Боевая приёмка — ПРОЙДЕНА 2026-09-26 (образ `0.1.10`, коммит `9f1e6c9c`)
Сквозной путь работает **от аккаунта Alex**. Это первая настоящая приёмка — прошлая была
ложной (от `testuser`).
### Доказательство — лента канала `balda-test`
```
1790413414689 | edxrgupoxiyig5gzmr79tjypbme | слово ← ОТВЕТ БОТА
1790413409055 | cbwsorzwoigbtrdnteugnx4aqo | @balda скажи одно слово ← пост ALEX
```
Запрос — `cbwsorzwoigbtrdnteugnx4aqo` (**аккаунт Alex**, `admin`), ответ — бот.
В логах: **`settlement_stage=accepted`** (НЕ `unauthorized`), `assembled provider user content`,
`delivery settled`.
### Привязка владельца — починена
```
balda_user_bindings:
channel_type = mattermost
principal = cbwsorzwoigbtrdnteugnx4aqo ← БЕЗ префикса ✅ (иначе unauthorized)
display_name = mattermost:cbwsorzwoigbtrdnteugnx4aqo ← ОДИНАРНЫЙ префикс ✅ (нет мусора)
```
Дефект двойного префикса вылечен **в коде** (идемпотентный `MattermostSubject`), в БД мусора нет.
### WS-здоровье и старт
| Метрика | Значение |
|---|---|
| `bad_seq` | **0** ✅ |
| `Invalid sequence` | **0** ✅ |
| `pong` | **6** (пинг раз в 30 с работает) |
| `RestartCount` | **0** ✅ |
| Контейнер | `balda-mm:0.1.10` Up стабильно |
```
mattermost transport configured server_url=http://mattermost:8065 bot_user_id_set=true
session manager ready balda_provider=deepseek
mattermost websocket connected ws://mattermost:8065/api/v4/websocket
Balda bot started.
```
Никакого crash-loop, `advertised commands have no actor handler` отсутствует.
### Два не-блокера
1. **`warn: getMe response missing result`** при старте — косметика: startup-лог не смог
прочитать identity бота. На работу НЕ влияет (`bot_user_id_set=true` из конфига).
Low priority, чинить в апстриме.
2. **`transport_user_id = c6udeze8dfrdxm5jbgq55bd58r`** в логах сессии — это `testuser`,
id прошлого владельца, зашитый в persisted-метаданные сессии `mm-c-7aafea13` (создана
в прошлой сессии от testuser). Авторизация прошла верно (по `principal` из bindings),
ответ ушёл Alex'у, **но сессия хранит чужой `user_id`**. Чистится `/reset` в канале.
На работу не влияет (проверено: ответ пришёл).
---
## 3. Сборка и деплой (воспроизводимо)
```bash
# 1. образ из PR-коммита (на Mac)
cd ~/Developer/balda
docker buildx build --builder arm64builder --platform linux/arm64 \
-f Dockerfile.release --build-arg VERSION=v0.3.1-mm-pr8 \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--provenance=false --load -t balda-mm:0.1.10 .
# 2. перенос (распаковка zstd НА MAC — на хосте zstd 1.5.4 не читает фрейм macOS)
docker save balda-mm:0.1.10 -o /tmp/balda-0110.tar
zstd -3 -T0 -o /tmp/balda-0110.tar.zst --force /tmp/balda-0110.tar
zstd -dc /tmp/balda-0110.tar.zst | ssh kraken 'docker load'
# 3. переключение с бэкапом compose
ssh kraken "cat > /tmp/mm-switch-image.sh" < tools/mm-switch-image.sh
ssh kraken 'bash -n /tmp/mm-switch-image.sh' < /dev/null # SYNTAX_OK
ssh kraken 'bash /tmp/mm-switch-image.sh' < /dev/null
```
| Скрипт | Что делает |
|---|---|
| `tools/mm-switch-image.sh` | смена тега образа в compose + бэкап + `--force-recreate` |
| `tools/mm-whoami.sh` | какой MM-аккаунт у PAT (`/users/me`) — нужен для приёмки от заказчика |
⚠️ **Проверка CA в образе:** `docker save` даёт **OCI**-формат, files в `blobs/sha256/*`;
`tar -tf image.tar | grep ca-certificates` даст **0** (ложный минус). Проверять по слоям или
верить `docker inspect` → `SSL_CERT_FILE` в `Config.Env`.
⚠️ **Приёмка обязывает иметь PAT заказчика.** `/tmp/admin-tok` = PAT Alex
(`cbwsorzwoigbtrdnteugnx4aqo`, `admin`) — сверено через `mm-whoami.sh` **до** постинга.
Без этого тест снова был бы от `testuser` = ложная приёмка.
---
## 4. Что в PR
### Коммиты (7)
```
9f1e6c9c chore: ignore local project-editing tooling
58f27120 docs(mattermost): document the transport setup and behavior
80d4f41d test(mattermost): cover the transport, the ingress and the handler
c7f18654 fix(mattermost): send a monotonic seq on every client frame
6dc3077a feat(mattermost): add the inbound handler and wire it into DI
fa1a2855 feat(auth): support the Mattermost channel subject
004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии
```
База ветки идеальная: `git rev-list --left-right --count upstream/main...HEAD` → `0 7`
(0 позади, 7 впереди), `merge-base` == `upstream/main` == `de273d8f`. Rebase не потребовался.
### Гейты — все зелёные на закоммиченном дереве
```
go build ./... → exit 0
go vet ./... → exit 0
go tool golangci-lint run → 0 issues
go tool go-arch-lint check --project-path . → OK - No warnings found
go test -race (затронутые пакеты) → все ok
тесты mattermost + handlersfx (verbose) → 126 PASS, 0 FAIL, 0 SKIP
```
📌 **Настоящая приёмка сборки — CI на чистом раннере.** Локально `cmd/balda` падал на
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` — в CI он **прошёл**,
то есть падение было особенностью локальной среды, а не дефектом. Локальные гейты —
необходимое, но не достаточное условие; ждать CI обязательно.
### Тесты — 103 добавлено (`grep -c '^func Test'`)
| Файл | Тестов | Покрытие |
|---|---|---|
| `channel/mattermost/ingress_test.go` | 23 | 🔴 `bad_seq`-регрессия, монотонность и уникальность `seq` под 16 горутинами, фильтры, panic-recovery, семафор |
| `channel/mattermost/inbound_test.go` | 18 | `NormalizeInbound`, mention-фильтр, `ParseCommand`, whitelist команд, `PostIDFromWebSocket` |
| `channel/mattermost/adapter_test.go` | 17 | все `deliverycmd.Operation`, plain-fallback только на content-rejection, draft-as-post, typing no-op без сети, `root_id` в треде |
| `handlersfx/mattermost_ingress_test.go` | 17 | публикация всех команд, **одиночный** префикс principal, `/start` до авторизации, отказ чужим, nil-safety |
| `channel/mattermost/locator_codec_test.go` | 15 | локаторы, thread↔channel изоляция, `LocatorFromAddressKey`, scope, UserID |
| `channel/mattermost/client_test.go` | 13 | ws/wss derivation, `Bearer`, `APIError`, `isContentRejectedError`, контекст |
### Что починено по существу
1. **Дефект двойного префикса — в коде, а не в БД.** Прошлый фикс был прямым `UPDATE` в
`state.db`, поэтому дефект вернулся. Теперь `auth.MattermostSubject()` идемпотентен (срезает
уже имеющийся префикс `mattermost:` перед повторной квалификацией), а из
`actors/command/start/handler.go` убран локальный `TrimPrefix`-костыль.
Почему ломало прод: `canonicalSubject` режет subject по **первому** `:`, а `GetUserByBinding`
сравнивает **точно** → `mattermost:mattermost:<id>` не совпадает ни с чем → каждый пост Alex
отбивался `settlement_stage=unauthorized`.
Регрессия: `TestMattermostSubjectIsIdempotent`,
`TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce`.
2. **Противоречие `ProgressPolicy.Typing`.** Command-путь ставил `Typing: true`, inbound-путь —
`false`, а `SendTyping` у MM-адаптера осознанный no-op (у ботов MM нет typing API). Мёртвая
ветка + warn-шум на каждом ходу → приведено к `false`.
3. **Доки** — `README.md`, `docs/balda.md`, новый `docs/mattermost.md` (создание бота, минтинг
токена — отдаётся ОДИН раз, membership в команде/канале, 5 ключей конфига, поведение в
DM/канале/треде, troubleshooting `bad_seq`/`unauthorized`/`x509`).
### Предсуществующие падения — НЕ наши (проверено `git stash`)
5 пакетов: `cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`. Те же 4 теста падают
**без** наших правок:
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents`,
`TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities`,
`TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore`,
`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`.
### Команды PR (воспроизводимо)
```bash
cd ~/Developer/balda
git fetch upstream --quiet
git rev-list --left-right --count upstream/main...HEAD # 0 7
git push -u origin feat/mattermost-channel
gh pr create --repo baldaworks/balda --base main \
--head mallexxx:feat/mattermost-channel \
--title "feat(mattermost): add Mattermost channel transport" \
--body-file /tmp/mm-pr-body.md
gh pr view 8 --repo baldaworks/balda --json state,mergeable,changedFiles,additions
gh pr checks 8 --repo baldaworks/balda
```
---
## 5. Пять файлов на диске — зачем они, НЕ трогать
В `~/Developer/balda` пять файлов изменены относительно коммита `9f1e6c9c` и **в PR не входят**.
Это **не фиксы** и **не потерянный код** — так и задумано.
**Откуда взялось:** 2026-09-25 18:58 (Zulip `personal/Балда`, msg `116952`) ветка пересоздана
от чистого апстрима: `git branch -f feat/mattermost-channel upstream/main` — «без мержа наших
коммитов». WIP прошлых сессий остался висеть на диске. Работа продолжилась в этой же ветке и
завершилась PR #8; существенное вошло в коммит `9f1e6c9c`.
| Файл | Что это | В PR? |
|---|---|---|
| `go.mod` | `replace github.com/normahq/runtime/v2 => ./runtime-local` — потомок `5ad65955` | ❌ |
| `Dockerfile.release` | `COPY runtime-local/` перед `go mod download` — иначе сборка падает | ❌ |
| `go.sum` | −2 хэша runtime v2.0.11 под тот же replace | ❌ |
| `internal/apps/balda/app.go` | WIP прошлых сессий (MM-импорты без псевдонима) — артефакт переключения ветки | ❌ |
| `internal/apps/balda/preflight.go` | то же + `ClassifyLocatorScope` во вторую карту | ❌ |
**Зачем они нужны:** без `runtime-local` стенд Balda на Kraken **не собирается**. `go.mod`
берёт не апстримовый runtime, а локальный форк (592 KB) с патчем `tool_calls` для hostedagent
(авторский PR `normahq/runtime#4`) — без него делегирование в CLI-агентов не работает.
Остальные 4 файла обслуживают этот же replace: `Dockerfile.release` копирует папку в образ,
`go.sum` больше не содержит хэшей апстримового runtime.
**Почему не в PR:** `replace` на локальную папку ломает сборку у любого, кто склонирует
`baldaworks/balda`. Такое апстрим не принимает. Место этому — в локальной ветке (`local/*`).
**Решение (2026-09-28, Alex):** оставлено как есть, отдельная ветка не делалась.
⚠️ **Риск:** `git checkout -- .` / `git stash` / `git reset --hard` в `~/Developer/balda`
снесёт эти 5 файлов. Перед любой такой командой — сверить `git status --short`.
---
## 6. Следующие шаги, чтобы «всё работало» — по приоритету
| # | Шаг | Тип |
|---|---|---|
| 1 | Дождаться ревью #8 → правки в ту же ветку (push обновит PR) | ⛔ внешний блокер |
| 2 | **Фаза 4 — исполнитель (делегирование)** — превращает «бот отвечает» в «бот работает» | 🎯 основной фронт |
| 3 | Боевая перепроверка приёмки на Kraken (образ `0.1.10` примлен 26.09 — повторять только если что-то менялось) | ↩ по необходимости |
| 4 | Замержить PR после ревью → Фаза 2 закрывается официально | ⏳ после 1 |
📌 **Пока PR висит на ревью — работать над Фазой 4 в ветке `local/*`, НЕ в
`feat/mattermost-channel`.** Иначе новые коммиты уедут в открытый PR и обесценят пройденный CI.
---
## 7. Питфоллы, которые стоили времени
1. **`gitignore` НЕ отвязывает уже отслеживаемые файлы.** База `004fda2f` затащила в ветку
**16 файлов `tools/`**, которых в апстриме нет вообще (`git ls-tree upstream/main tools/` →
пусто). Найдено проверкой `git diff --stat upstream/main...HEAD`. Фикс (коммит `9f1e6c9c`):
`/tools/` в `.gitignore` **плюс** `git rm -r --cached tools/` — файлы остались на диске
(39 файлов), ушли только из git. Нужны оба шага, в этом порядке.
⚠️ `git rm -r --cached` уходит на апрув (рекурсивное удаление) — первая попытка истекла по
таймауту и не применилась, состояние было целое. Проверять `git diff --cached --name-only`.
2. **Проверять состав PR через `git diff --stat upstream/main...HEAD`, а не по своим коммитам.**
Свои 5 коммитов были чистыми (`git log upstream..HEAD -- tools/` → пусто), но ветка в
целом — нет, из-за базового коммита. `git status` показывает рабочее дерево, не содержимое
коммитов.
3. **Тест-ассерт — это гипотеза, а не истина.** Дважды падал тест, и оба раза неверным был
ассерт, а не код: (а) «тред и корневой пост = одна сессия» — на самом деле тред **отдельная**
сессия от канала (пробник: `mm-c-a4aa02ef` vs `mm-t-89bbf64f`); (b) «`NewIngress` фолбэчит
`botUserID` из клиента» — на самом деле `onStart` отказывается стартовать без `bot_user_id`.
Прежде чем править код под падающий тест — проверить, чему код **обязан** соответствовать.
4. **`goconst` в тестах** — повторяющиеся строковые литералы (`"channel-1"` ×15) валят линтер.
Заводить константы (`testChannelID`) сразу. **`dupword`** — `"thread channel = %q, channel
channel = %q"` пойман как повтор слова.
5. **`write_file` + `***` в доке** — плейсхолдеры вида `BALDA_MATTERMOST_TOKEN=***` в примерах
конфига остаются на диске **целыми**; искажается только вывод чтения. Проверять
`awk 'NR==N'`, не глазами (см. [[personal/tech/hermes-secret-redaction-bypass]]).
6. **⛔ zstd-фрейм macOS.** `zstd -d` на Kraken не читает фрейм, записанный на macOS (хостовый
zstd 1.5.4). Распаковывать **на Mac**: `zstd -dc file.tar.zst | ssh kraken 'docker load'`.
---
## Связанные заметки
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2 (код коннектора)
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (деплой, боевые причины тишины)
- [[personal/projects/balda/balda-mm-delegation-plan]] — Фаза 4 (делегирование)
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы + дельта-апплаер
- [[personal/plans/balda-mm-endtoend-plan]] — сквозной путь
- [[personal/tech/hermes-secret-redaction-bypass]] — редакция секретов