Files

25 KiB
Executable File
Raw Permalink Blame History

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».

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. Сборка и деплой (воспроизводимо)

# 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 (воспроизводимо)

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'.

Связанные заметки