25 KiB
Executable File
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, stateCOMMENTED, inline-комментов 0). Закрыты: slash-команды (HTTP-приёмник + setup в доке),/locator(locatorref+ guard-тест), групповой DM (сессия по чату + тест двух отправителей) + регистрация вScopeResolver, которой не было в PR. Сверка построчно — personal/plans/balda-mm-pr8-review-fixes §0a. 📌 Мерж прошёл безAPPROVED-ревью —reviewDecisionоставался пустым, PR влит вручную мейнтейнером. Мерж-коммитbf4a6ae4= merge86643007(голова 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».
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 отсутствует.
Два не-блокера
warn: getMe response missing resultпри старте — косметика: startup-лог не смог прочитать identity бота. На работу НЕ влияет (bot_user_id_set=trueиз конфига). Low priority, чинить в апстриме.transport_user_id = c6udeze8dfrdxm5jbgq55bd58rв логах сессии — этоtestuser, id прошлого владельца, зашитый в persisted-метаданные сессииmm-c-7aafea13(создана в прошлой сессии от testuser). Авторизация прошла верно (поprincipalиз bindings), ответ ушёл Alex'у, но сессия хранит чужойuser_id. Чистится/resetв канале. На работу не влияет (проверено: ответ пришёл).
3. Сборка и деплой (воспроизводимо)
# 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, контекст |
Что починено по существу
- Дефект двойного префикса — в коде, а не в БД. Прошлый фикс был прямым
UPDATEвstate.db, поэтому дефект вернулся. Теперьauth.MattermostSubject()идемпотентен (срезает уже имеющийся префиксmattermost:перед повторной квалификацией), а изactors/command/start/handler.goубран локальныйTrimPrefix-костыль. Почему ломало прод:canonicalSubjectрежет subject по первому:, аGetUserByBindingсравнивает точно →mattermost:mattermost:<id>не совпадает ни с чем → каждый пост Alex отбивалсяsettlement_stage=unauthorized. Регрессия:TestMattermostSubjectIsIdempotent,TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce. - Противоречие
ProgressPolicy.Typing. Command-путь ставилTyping: true, inbound-путь —false, аSendTypingу MM-адаптера осознанный no-op (у ботов MM нет typing API). Мёртвая ветка + warn-шум на каждом ходу → приведено кfalse. - Доки —
README.md,docs/balda.md, новыйdocs/mattermost.md(создание бота, минтинг токена — отдаётся ОДИН раз, membership в команде/канале, 5 ключей конфига, поведение в DM/канале/треде, troubleshootingbad_seq/unauthorized/x509).
Предсуществующие падения — НЕ наши (проверено git stash)
5 пакетов: cmd/balda, apps/balda, mcpruntime, session, state. Те же 4 теста падают
без наших правок:
TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents,
TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities,
TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore,
TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid.
Команды PR (воспроизводимо)
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. Питфоллы, которые стоили времени
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.- Проверять состав PR через
git diff --stat upstream/main...HEAD, а не по своим коммитам. Свои 5 коммитов были чистыми (git log upstream..HEAD -- tools/→ пусто), но ветка в целом — нет, из-за базового коммита.git statusпоказывает рабочее дерево, не содержимое коммитов. - Тест-ассерт — это гипотеза, а не истина. Дважды падал тест, и оба раза неверным был
ассерт, а не код: (а) «тред и корневой пост = одна сессия» — на самом деле тред отдельная
сессия от канала (пробник:
mm-c-a4aa02efvsmm-t-89bbf64f); (b) «NewIngressфолбэчитbotUserIDиз клиента» — на самом делеonStartотказывается стартовать безbot_user_id. Прежде чем править код под падающий тест — проверить, чему код обязан соответствовать. goconstв тестах — повторяющиеся строковые литералы ("channel-1"×15) валят линтер. Заводить константы (testChannelID) сразу.dupword—"thread channel = %q, channel channel = %q"пойман как повтор слова.write_file+***в доке — плейсхолдеры видаBALDA_MATTERMOST_TOKEN=***в примерах конфига остаются на диске целыми; искажается только вывод чтения. Проверятьawk 'NR==N', не глазами (см. personal/tech/hermes-secret-redaction-bypass).- ⛔ 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 — редакция секретов