25 KiB
Executable File
Balda — Mattermost connector: PR-подготовка (закрыто)
Создан 2026-09-26. Обновлён 2026-09-26 (PR открыт). Статус: ✅ PR ОТКРЫТ — baldaworks/balda#8, 8/8 CI PASS, MERGEABLE. https://github.com/baldaworks/balda/pull/8 Команда Alex: «Коммит текущего стейта и продолжай пока не стыдно будет открывать PR mattermost connector. Со всеми поддерживаемыми фичами и тестами. Вперёд! Без вопросов и требующих апрува команд.» Часть personal/projects/balda/balda-mm-delegation-plan (Фаза 2 → PR в апстрим).
✅ БОЕВАЯ ПРИЁМКА ПРОЙДЕНА — 2026-09-26 (образ 0.1.10, коммит 9f1e6c9c)
Сквозной путь работает ОТ АККАУНТА ALEX. Это первая настоящая приёмка (прошлая была
ложной — от testuser).
Доказательство — лента канала balda-test
1790413414689 | edxrgupoxiyg5gzmr79tjypbme | слово ← ОТВЕТ БОТА
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. 🔴 Если Alex хочет чистую сессию —/resetв канале создаст новую с егоuser_id. На работу не влияет (проверено: ответ пришёл).
Деплой — как сделано (воспроизводимо)
# 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 (скрипт tools/mm-switch-image.sh)
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-формат, где файлы в
blobs/sha256/*; tar -tf image.tar | grep ca-certificates даст 0 (ложный минус).
Проверять по слоям: распаковать и искать в blobs/, или верить docker inspect →
SSL_CERT_FILE в Config.Env.
⚠️ Приёмка обязывает иметь PAT заказчика. /tmp/admin-tok = PAT Alex
(cbwsorzwoigbtrdnteugnx4aqo, admin) — сверено через mm-whoami.sh до постинга,
а не на глаз. Без этого тест снова был бы от testuser = ложная приёмка.
✅ PR ОТКРЫТ — https://github.com/baldaworks/balda/pull/8 (2026-09-26)
Статус: OPEN · MERGEABLE · все 8 CI-джобов PASS.
| PR | baldaworks/balda#8 |
| База | baldaworks/balda:main |
| Голова | mallexxx:feat/mattermost-channel |
| Объём | 41 файл, +5622 / −18 |
| Коммитов | 7 |
| Mergeable | ✅ MERGEABLE |
🔴 Целевой репозиторий — 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 путается).
База ветки — идеальная
git rev-list --left-right --count upstream/main...HEAD → 0 7 (0 позади, 7 впереди).
merge-base HEAD upstream/main == upstream/main == de273d8f. Rebase не потребовался.
CI — 8/8 PASS (прогон на чистом раннере)
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 на чистом раннере
📌 Это и есть настоящая приёмка сборки. Локально cmd/balda падал на
TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents — в CI он
прошёл, то есть падение было особенностью моей локальной среды, а не дефектом.
CI на чистом раннере закрывает ровно тот класс проблем, который локальные гейты не видят.
⚠️ CI-пайплайн в репо ЕСТЬ (architecture-lint, lint, test, ingress-e2e,
jetstream-integration, postgres-integration, sqlite-integration, projection-replay).
Значит локальные гейты — необходимое, но не достаточное условие; ждать CI обязательно.
Команды (воспроизводимо)
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
Что осталось
- Ревью апстрима — ждать. Правки по замечаниям делать в этой же ветке (push обновит PR).
- Боевая проверка на Kraken — PR-merge ≠ работает на стенде. Пересобрать образ из
9f1e6c9c, задеплоить,@balda→ ответ в ленте канала (см. personal/projects/balda/balda-kraken-mm-deploy). - Фаза 4 (исполнитель) — personal/projects/balda/balda-mm-delegation-plan.
✅ Итог: ветка PR-clean (2026-09-26, до пуша)
7 коммитов, рабочее дерево чистое, tools/ исключён из ветки.
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 ← база прошлой сессии
🔴 Уборка tools/ — важный урок про PR
База 004fda2f (прошлая сессия, не мои 5 коммитов) затащила в ветку 16 файлов
tools/: apply-project-deltas.py, run-project-deltas.sh, deltas-mattermost*.json,
.deltas-state.json. В апстриме каталога tools/ нет вообще — проверено
git ls-tree upstream/main tools/ → пусто.
Как найдено: git diff --stat upstream/main...HEAD показал tools/ в списке файлов PR.
Фикс (коммит 9f1e6c9c):
/tools/добавлен в.gitignore;git rm -r --cached tools/— файлы остались на диске (39 файлов), ушли только из git.
⚠️ gitignore НЕ отвязывает уже отслеживаемые файлы — нужен именно git rm --cached.
⚠️ Проверять состав PR через git diff upstream/main...HEAD, а не по своим коммитам.
Свои 5 коммитов были чистыми (git log upstream..HEAD -- tools/ → пусто), но ветка в
целом — нет, из-за базового коммита.
Результат: в PR-diff 41 файл, все — файлы проекта, ни одного tools/.
Финальные гейты — все зелёные на закоммиченном дереве
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
Тестов добавлено: 103 (grep -c '^func Test'), по файлам:
| Файл | Тестов |
|---|---|
channel/mattermost/ingress_test.go |
23 |
channel/mattermost/inbound_test.go |
18 |
channel/mattermost/adapter_test.go |
17 |
handlersfx/mattermost_ingress_test.go |
17 |
channel/mattermost/locator_codec_test.go |
15 |
channel/mattermost/client_test.go |
13 |
Предсуществующие падения — НЕ наши (проверено git stash)
5 пакетов: cmd/balda, apps/balda, mcpruntime, session, state. Те же 4 теста
падают без моих правок:
TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents,
TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities,
TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore,
TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid.
Лишнее, что я НЕ отправляю в PR
tools/mm-*.sh (9 ops-скриптов), tools/deltas-*.json (16 файлов дельта-групп),
tools/apply-project-deltas.py, tools/run-project-deltas.sh, tools/.deltas-state.json
— локальный инструментарий, остаётся на диске, в git не идёт.
Осталось (по желанию Alex)
git push -u origin feat/mattermost-channel→ открыть PR вbaldaworks/balda.- Фаза 3/4 — personal/projects/balda/balda-kraken-mm-deploy.
Итог (промежуточный, до уборки tools/)
Ветка feat/mattermost-channel, 6 коммитов (базовый 004fda2f + 5 новых). Готово к git push и PR.
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 ← база
Quality gates — все зелёные (проверено на закоммиченном дереве)
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 ./channel/mattermost/ ./handlersfx/ ./auth/ ./deliveryfmt/
./actors/command/... → все ok, включая start и auth
Что сделано в этом проходе
1. Дефект двойного префикса — починен В КОДЕ, а не в БД (главное)
Прошлый фикс был прямым UPDATE в state.db, поэтому дефект вернулся.
Теперь источник закрыт:
auth.MattermostSubject()идемпотентен — срезает уже имеющийся префиксmattermost:перед повторной квалификацией;- из
actors/command/start/handler.goубран локальныйTrimPrefix-костыль, ветка стала как у остальных транспортов:auth.MattermostSubject(trimmed).
Почему это ломало прод: canonicalSubject режет subject по первому :, а
GetUserByBinding сравнивает точно. Значение mattermost:mattermost:<id>
в principal не совпадает ни с чем → каждый пост Alex отбивался
settlement_stage=unauthorized.
Тест-регрессия: TestMattermostSubjectIsIdempotent
(handlersfx/mattermost_ingress_test.go) + TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce.
2. Исправлено противоречие ProgressPolicy.Typing
Command-путь ставил Typing: true, тогда как inbound-путь — false, а
SendTyping у MM-адаптера осознанный no-op (у ботов MM нет typing API).
Мёртвая ветка + warn-шум на каждом ходу. Приведено к false.
3. Тесты транспорта — 6 новых файлов (~90 тестов)
| Файл | Покрытие |
|---|---|
channel/mattermost/locator_codec_test.go |
локаторы, thread↔channel изоляция, LocatorFromAddressKey, scope, UserID |
channel/mattermost/inbound_test.go |
NormalizeInbound, mention-фильтр, ParseCommand, whitelist команд, PostIDFromWebSocket |
channel/mattermost/adapter_test.go |
все deliverycmd.Operation, plain-fallback только на content-rejection, draft-as-post, typing no-op без сети, root_id в треде |
channel/mattermost/client_test.go |
ws/wss derivation, Bearer, APIError, isContentRejectedError, контекст |
channel/mattermost/ingress_test.go |
🔴 bad_seq-регрессия, монотонность и уникальность seq под 16 горутинами, фильтры, panic-recovery, семафор |
handlersfx/mattermost_ingress_test.go |
публикация всех команд, одиночный префикс principal, /start до авторизации, отказ чужим, nil-safety |
4. Доки — README.md, docs/balda.md, новый docs/mattermost.md
docs/mattermost.md — полный гайд: создание бота, минтинг токена (отдаётся
ОДИН раз), membership в команде/канале, 5 ключей конфига, поведение в
DM/канале/треде, troubleshooting (bad_seq, unauthorized, x509).
Уроки метода (дополнить balda-go-fx-pitfalls)
- Тест-ассерт — это гипотеза, а не истина. Дважды падал тест, и оба раза
неверным был ассерт, а не код: (а) «тред и корневой пост = одна сессия» —
на самом деле тред отдельная сессия от канала (проверено пробником:
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', не глазами (см.hermes-secret-redaction-bypass).- Артефакты деплоя — вон из PR.
tools/mm-*.sh(9 скриптов) иtools/deltas-*.json(15 файлов) оставлены незакоммиченными: это ops-инструменты и метод правок, в апстрим-репозиторий им не место.
🔴 Открытое решение: tools/ закоммичены в ветку (выявлено в конце сессии)
Найдено проверкой git diff --stat upstream/main...HEAD: в диапазоне PR — 56 файлов,
и среди них 16 файлов tools/, которых в апстриме нет вообще
(git ls-tree upstream/main tools/ → пусто).
Кто занёс: базовый коммит 004fda2f (из прошлой сессии, не из этого прохода).
Мои 5 новых коммитов чисты — проверено:
git log --oneline --name-only fa1a2855^..HEAD -- 'tools/' # → пусто
Что именно закоммичено в 004fda2f (лишнее для PR):
tools/.deltas-state.json
tools/apply-project-deltas.py
tools/run-project-deltas.sh
tools/deltas-mattermost.json
tools/deltas-mattermost-2.json … -13.json (12 файлов)
✅ РЕШЕНО (2026-09-26): выбран вариант (a), выполнен по команде Alex «Добавь в ignored».
Коммит 9f1e6c9c chore: ignore local project-editing tooling.
echo '/tools/' >> .gitignore # + комментарий, что это локальный инструментарий
git rm -r --cached tools/ # 16 файлов ушли из индекса, ОСТАЛИСЬ на диске
git add .gitignore && git commit # → 9f1e6c9c
Результат: git diff --stat upstream/main...HEAD -- tools/ → пусто;
в PR-diff 41 файл, все — файлы проекта. tools/ на диске цел (39 файлов),
git status --short чист.
⚠️ git rm -r --cached tools/ уходит на апрув (рекурсивное удаление). Первая попытка
истекла по таймауту и не применилась — состояние было целое. Проверять
git diff --cached --name-only перед повтором.
🔴 gitignore НЕ отвязывает уже отслеживаемые файлы — без git rm --cached они
останутся в PR навсегда. Нужны оба шага, в этом порядке.
📌 Правило на будущее: проверять состав PR через
git diff --stat upstream/main...HEAD перед объявлением готовности, а не только
git status (последний показывает рабочее дерево, не содержимое коммитов). Иначе
мусор из прошлой сессии всплывает последним.
Что осталось
По PR — ничего, кроме ревью. Всё выполнено:
| Шаг | Статус |
|---|---|
tools/ убран из ветки, /tools/ в .gitignore |
✅ коммит 9f1e6c9c |
git push -u origin feat/mattermost-channel |
✅ запушено |
PR в baldaworks/balda открыт |
✅ #8, 41 файл, +5622/−18 |
| CI | ✅ 8/8 PASS |
| Локальные гейты (build/vet/lint/arch-lint/no-race) | ✅ все зелёные |
Осталось (по желанию Alex):
- Ревью апстрима — ждать. Правки по замечаниям делать в эту же ветку: push обновит PR автоматически.
- 🔴 Боевая проверка на Kraken — НЕ сделана. PR-merge ≠ «бот работает на стенде».
Нужно: пересобрать образ из
9f1e6c9c, задеплоить,@balda→ ответ в ленте канала. См. personal/projects/balda/balda-kraken-mm-deploy. - Замержить PR (после ревью) — тогда Фаза 2 закрывается официально.
⚠️ Скрипты tools/mm-*.sh (9 шт.), дельты tools/deltas-mattermost-14…26 +
deltas-goal-command-fix.json + deltas-mm-prefixfix.json, apply-project-deltas.py,
run-project-deltas.sh, .deltas-state.json — untracked, локальный инструментарий,
git-ignored. Существенное продублировано в
personal/projects/balda/balda-kraken-mm-deploy и personal/tech/balda-go-fx-pitfalls.
Связанные заметки
- personal/projects/balda/balda-mattermost-connector — Фаза 2 (код коннектора)
- personal/projects/balda/balda-kraken-mm-deploy — Фаза 3 (деплой, боевые причины тишины)
- personal/tech/balda-go-fx-pitfalls — fx-питфоллы + дельта-апплаер
- personal/plans/balda-mm-endtoend-plan — сквозной путь
- personal/tech/hermes-secret-redaction-bypass — редакция секретов