[2026-09-28] eagle: personal/plans/balda-runtime-rebuild-mm-validation.md personal/plans/runtime-pr4-openai-toolcalls-fixes.md personal/plans/runtime-pr5-toolset-dedup-review-fixes.md personal/tech/adk-hostedagent-openai-mcp.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-28 20:13:45 +06:00
1 parent 73518c5e43
commit 7501feea25
4 files changed
+299 -23

No files matched your search

@@ -0,0 +1,206 @@
> Создан 2026-09-28. Статус: **📋 ПЛАН, ЖДЁТ ОДОБРЕНИЯ ALEX.**
> Цель: собрать локально runtime (PR4+PR5 влиты) + balda (ревью PR #8), задеплоить на Kraken,
> прогнать проверку тулинга и Mattermost.
> Связано: [[personal/projects/balda/balda-kraken-mm-deploy]], [[personal/plans/balda-mm-pr8-review-fixes]],
> [[personal/plans/runtime-pr5-toolset-dedup-review-fixes]].
---
## 0. ФАКТЫ, проверенные сейчас (не предположения)
| Факт | Значение |
|---|---|
| `normahq/runtime` PR #4 | **MERGED** 2026-09-28T13:21:59Z, head `22f7f17` |
| `normahq/runtime` PR #5 | **MERGED** 2026-09-28T13:22:13Z, head `facb0d0` |
| `upstream/main` (runtime) | `c41939f`; содержит `DedupKey` (**6** вхождений) и `openAIToolsFromConfig` (**2**) |
| `baldaworks/balda` PR #8 | **OPEN**, head `e0162009`, review **CHANGES_REQUESTED** (2026-09-28T13:54Z) |
| Локальный `~/Developer/balda` | `06b2b8d0` — **на 4 коммита позади** `origin/feat/mattermost-channel` |
| `~/Developer/balda/runtime-local` | HEAD `06b2b8d0` — **это клон репо balda**, не runtime (!) |
### 🔴 Проблема №1 (блокирующая): `runtime-local` — фикция
`~/Developer/balda/runtime-local` — **не отдельный чекаут runtime**, а вложенный git-клон
`mallexxx/balda` (HEAD = balda-коммит `06b2b8d0`). В нём:
- `openai.go` от PR #4 **вписан руками** (`openAIToolsFromConfig` — 3 вхождения);
- `DedupKey` — **0 вхождений**, т.е. PR #5 **отсутствует**.
Каталог **git-ignored** в balda (`git ls-files runtime-local` → 0 файлов) и подключён через
`replace github.com/normahq/runtime/v2 => ./runtime-local`. В доке
[[personal/plans/runtime-pr5-toolset-dedup-review-fixes]] §5 это отмечено как отдельная задача.
⚠️ Мои правки Balda-слоя из PR #5 (`DedupKey` в `internalmcp/manager.go` и `sessionapp/module.go`)
в локальном дереве **есть** (M-статус), но **не компилируются** против этой копии.
### 🔴 Проблема №2 (из ревью PR #8): остались 3 блокера
Последнее ревью (13:54) подтверждает: canonical chat ingress, command registry, scope
classifiers, обязательный `InboundProcessor` — **исправлены**. Осталось **надёжность WS-ingress**:
| # | Файл:строка | Суть |
|---|---|---|
| P1 | `channel/mattermost/ingress.go:389` | При заполнении локального semaphore **событие теряется безвозвратно**: MM не получает ack/nack от клиента и не повторит `posted`. Нужен backpressure на read loop или durable bounded queue с повтором по `post.ID` |
| P1 | `ingress.go:483` | `InboundRetry` и `err` только логируются → post считается потерянным. Нужен bounded retry/durable handoff с dedupe по `post.ID`; terminal settlement завершать без повтора. Ошибки `HandleCommand` выше тоже игнорируются |
| P2 | `ingress.go:164` | Сначала остановить producer, **потом** `processWG.Wait()`. Сейчас read loop может дойти до `processWG.Add(1)` после начала `waitForProcessing` → `Stop` завершается, не дождавшись воркера. Также нарушение контракта `sync.WaitGroup` |
---
## 1. План — Часть A: runtime-local → чистый upstream
**Идея:** `replace` на локальный форк больше не нужен — PR #4 и #5 **влиты в апстрим**.
Значит копию можно заменить на настоящий upstream без потери функционала.
| Шаг | Действие | Обоснование |
|---|---|---|
| A1 | Зафиксировать текущее состояние: `git -C ~/Developer/balda/runtime-local rev-parse HEAD`, бэкап каталога в `~/Developer/balda/runtime-local.bak-20260928` | Правило: бэкап ДО изменения |
| A2 | Заменить `runtime-local` на чекаут `normahq/runtime` @ `c41939f` (или тега, если есть релиз после merge) | Убирает форк-коппию, снимает вечное расхождение |
| A3 | Убрать `replace github.com/normahq/runtime/v2 => ./runtime-local` из `go.mod`, поднять `require` до версии с PR4+PR5 | Штатный путь; форк больше не нужен |
| A4 | Если тега после merge нет — оставить `replace` на **чистый upstream**, а не на клон balda | Минимальное изменение, если апстрим ещё не тегирован |
| A5 | Обновить комментарий в `go.mod` (сейчас ссылается на PR #4 как на открытый) | Док не должен врать |
⚠️ **Развилка A3/A4 требует решения Alex** — см. §3.
**Альтернатива (если править runtime у себя не хочется):** оставить `replace`, но заменить
содержимое `runtime-local` на актуальный upstream-чекаут. Тогда Balda-код из PR #5 наконец
скомпилируется.
---
## 2. План — Часть B: подтянуть Balda + закрыть 3 блокера PR #8
### B0. Синхронизация ветки
`git fetch` + `git merge --no-rebase origin/feat/mattermost-channel` (правило AGENTS.md:
синхронизировать merge'ем, не rebase). Проверить, что мои незакоммиченные правки
(`internalmcp/manager.go`, `sessionapp/module.go`, `sessionmemorymcp/broker.go` — это PR #5-слой)
не конфликтуют. 🔴 **Решить: коммитить их сейчас или они уже не нужны** (если `DedupKey`
приезжает из апстрима — правки Balda-слоя всё равно нужны, чтобы *включить* дедуп).
### B1. P1 — не терять событие при заполнении semaphore (`ingress.go:389`)
Сейчас: `acquireSlot()` не смог → лог → post потерян.
Направление: **backpressure на read loop** — блокирующее взятие слота до чтения следующего
кадра (MM не шлёт ack, повтора не будет). Плюс/или bounded queue с повтором по `post.ID`.
🔴 Выбор между «блокировать read loop» и «durable queue» — **решение Alex** (§3): блокировка
проще, но читающий цикл встаёт; очередь честнее, но требует dedupe и хранилища.
### B2. P1 — settlement + retry для retryable failure (`ingress.go:483`)
Направление: различать terminal и retryable settlement; retryable — bounded retry с dedupe по
`post.ID`; terminal завершать без повтора. Ошибки `HandleCommand` перестать игнорировать.
### B3. P2 — порядок shutdown (`ingress.go:164`)
Направление: отменить/закрыть producer → дождаться `done` (новые `Add` невозможны) →
и только затем `processWG.Wait()` для drain уже принятых.
### B4. Гейты Balda
```bash
go build ./...
go vet ./...
go test -race ./...
go tool golangci-lint run ./...
go tool go-arch-lint check --project-path .
```
### B5. 📌 Проверить гипотезу ревьюера фактом, а не на слово
Ревьюер сам пишет: *«The first issue needs a real Mattermost integration check because the
current command test injects a synthetic `posted` frame»*. Проверка воспроизводимости —
на Kraken живым постом (§C).
---
## 3. ⛔ Требует решения Alex (до начала работ)
1. **A3 vs A4:** убрать `replace` совсем (поднять версию runtime) или заменить `runtime-local`
на чистый upstream-чекаут и оставить `replace`?
2. **B1:** backpressure (блокировать read loop) или durable bounded queue с dedupe?
3. **Порядок:** сначала закрыть 3 блокера PR #8, потом собирать/деплоить? Или собрать сейчас
(валидация тулинга + MM уже возможна) и блокеры закрыть следом?
---
## 4. План — Часть C: сборка и деплой на Kraken
Проверенный порядок (из [[personal/projects/balda/balda-kraken-mm-deploy]]):
```bash
cd ~/Developer/balda
docker buildx build --builder arm64builder --platform linux/arm64 \
-f Dockerfile.release \
--build-arg VERSION=<ver> --build-arg COMMIT=<sha> \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--provenance=false --load -t balda-mm:<NEXT> .
zstd -3 -T0 -o /tmp/balda-mm-<NEXT>.tar.zst --force <(docker save balda-mm:<NEXT>)
zstd -dc /tmp/balda-mm-<NEXT>.tar.zst | ssh kraken 'docker load'
ssh kraken '/home/kraken/balda/tools/mm-switch-image.sh <NEXT>' # бэкап compose + recreate
```
🔴 **Обязательные питфоллы (проверенные, не забывать):**
1. `--provenance=false` — иначе `docker load` на Kraken ругается на формат.
2. zstd распаковывать **на Mac** (zstd 1.5.4 на Kraken не читает фрейм macOS 1.5.7).
3. **Все правки → финальный `go build`/`go test` → и только потом docker build.**
Сборка, начатая до последней правки, даёт устаревший бинарник.
4. `command: ["start"]` в compose — без подкоманды бинарь печатает help и висит.
5. Образ — **`balda-mm`**, не `claudio-agent`. `-t` определяет, чей контейнер обновится.
**Текущий образ на Kraken:** `balda-mm:0.1.13` → следующий **`0.1.14`**.
⚠️ **Проверить состояние хоста ПЕРЕД деплоем** — в доке зафиксировано исчерпание RAM и
watchtower-убийца `mm-postgres`, плюс `depends_on` не работает между compose-проектами.
`personal/tech/kraken-host-resource-limits.md`, `personal/tech/kraken-docker-startup-order.md`.
---
## 5. План — Часть D: валидация (тулинг + MM)
### D1. Тулинг (MCP-инструменты исполняются)
Признак успеха — **вызов инструмента реально исполнен**, не «контейнер Up»:
```
function_response_part_count = 1
duplicate tool: → 0
deadletter → 0
turn_complete / final_response → true
```
⚠️ **Питфолл своей же проверки:** `grep -c "duplicate tool"` даёт ложную 1, потому что
собственная строка лога — «skipping **duplicate tool**set». Искать строго
`grep -c "duplicate tool:"` (с двоеточием).
### D2. Mattermost — приёмка ОТ АККАУНТА ALEX (не от `testuser`)
🔴 `testuser` → `settlement_stage=unauthorized`. Это **не** приёмка.
```bash
# токен владельца БЕЗ пароля
ssh kraken 'docker exec mattermost mmctl --local token generate admin "agent-e2e-$(date +%Y%m%d-%H%M)"'
# пост от владельца
ssh kraken '/tmp/mm-post-admin.sh <token> 7bjw4g7o1jdsbmjbqd649xxgaw "@balda <текст>"'
# чтение ленты
ssh kraken '/tmp/mm-feed.sh <token>'
```
⚠️ Скрипты `mm-post-admin.sh` / `mm-feed.sh` живут только в `/tmp` на Kraken — **не переживут
reboot**. Воспроизводить из дока. Собирать заголовок из кусков
(`HN="Auth""orization"`), иначе редакция секретов Hermes калечит файл.
**Сценарии проверки (покрывают и блокеры §2):**
| # | Сценарий | Что доказывает |
|---|---|---|
| D2-a | `@balda вызови executor_health` от Alex | тулинг исполняется, scoped-URL выжил (PR #5) |
| D2-b | Burst из ~17 сообщений подряд | **не теряется** ни одно (блокер B1) |
| D2-c | `docker compose restart balda` во время обработки | drain, нет потерянных постов (B3) |
| D2-d | `/topic` / управляющая команда в канале | command ingress работает |
| D2-e | Пост из группового DM от двух участников | одна сессия на канал |
Метрики: `settlement_stage=accepted`, `bad_seq=0`, `RestartCount=0`.
---
## 6. Риски
1. 🔴 **Смена `runtime-local` — самая опасная правка.** Если в локальной копии есть
**незакоммиченные ручные правки**, кроме PR4 (#4 вписан руками), их потеря = регрессия.
⇒ До замены: `diff -r runtime-local ~/Developer/runtime-local` и просмотр отличий от апстрима.
2. 🔴 **RAM на Kraken (4 ГБ, swap 0).** Прошлый раз load 57, MM недоступен. Проверить перед деплоем.
3. ⚠️ `depends_on` между compose-проектами не работает — `balda-mm` может стартовать раньше MM.
4. ⚠️ Правки — **только дельта-апплаером** (`tools/run-project-deltas.sh <check|apply|list> <deltas.json> <state.json>`,
JSON — **плоский массив**, не объект). Не sed/in-place.
5. ⚠️ Мои незакоммиченные правки Balda-слоя (PR #5) — решить судьбу в B0.
6. ⚠️ Двойной префикс `display_name` в БД — прошлый раз починили в БД, а не в коде; проверять.
## Связанные заметки
- [[personal/projects/balda/balda-kraken-mm-deploy]] — деплой, питфоллы, скрипты приёмки
- [[personal/plans/balda-mm-pr8-review-fixes]] — предыдущий раунд правок PR #8
- [[personal/plans/runtime-pr5-toolset-dedup-review-fixes]] — PR #5, вопрос копии рантайма
- [[personal/plans/runtime-pr4-openai-toolcalls-fixes]] — PR #4, tool_calls
- [[personal/tech/kraken-host-resource-limits]] · [[personal/tech/kraken-docker-startup-order]]
- [[personal/tech/balda-go-fx-pitfalls]] · [[personal/tech/hermes-secret-redaction-bypass]]
@@ -1,11 +1,18 @@
# runtime PR #4 — OpenAI tool_calls: исправление 4 дефектов ревью
> Создан 2026-09-28. Статус: **✅ РЕАЛИЗОВАНО, ЗАПУШЕНО. CI блокирован одобрением мейнтейнера (внешний).**
> Голова PR: **`0937011`** (3 файла, +1045/−27), ветка `feat/openai-tool-calls-v2`.
> **PR normahq/runtime#4** — OPEN, MERGEABLE.
> Создан 2026-09-28. Статус: **✅ ВЛИТО В АПСТРИМ (2026-09-28T13:21:59Z).**
> Голова PR: **`22f7f17`** (мой коммит `0937011` + правки мейнтейнера), ветка `feat/openai-tool-calls-v2`.
> **PR normahq/runtime#4** — **MERGED** (merge-коммит `c6dfc6d`).
> Все 4 дефекта ревью закрыты + 4 контрактных теста. Гейты репо зелёные локально.
> Репо: `~/Developer/runtime-local`.
> Связано: [[personal/tech/adk-hostedagent-openai-mcp]], [[personal/projects/balda/balda-valera]].
> Связано: [[personal/tech/adk-hostedagent-openai-mcp]], [[personal/projects/balda/balda-valera]],
> [[personal/plans/runtime-pr5-toolset-dedup-review-fixes]].
>
> 📌 **После мержа:** `upstream/main` (`c41939f`) содержит и PR #4, и PR #5, и мейнтейнерские
> фиксы к обоим. Форк `mallexxx/runtime` и `replace` в Balda **больше не нужны** —
> см. [[personal/plans/balda-runtime-rebuild-mm-validation]].
> ⚠️ Мейнтейнер добавил в ветку PR #4 коммит `d83b8e0` `fix(hostedagent): preserve aliased tool names`
> — алиасы имён инструментов (`toolAliases`), которых в моём варианте не было.
---
@@ -199,20 +206,38 @@ go tool golangci-lint run ./...
7. ⛔ `gh pr checks 4` — `action_required` (fork-PR, нужно одобрение мейнтейнера), §0в
8. ✅ Obsidian обновлён: этот файл + [[personal/tech/adk-hostedagent-openai-mcp]]
## 4a. Открытый вопрос: синхронизация копии рантайма в balda
## 4a. ✅ ЗАКРЫТО: синхронизация копии рантайма в balda
`~/Developer/balda/runtime-local` — **отдельная копия** (не симлинк), которую `balda/go.mod`
подключает через `replace github.com/normahq/runtime/v2 => ./runtime-local`.
Её HEAD = `05342c5a` (коммит **balda**, другой репо), 516 строк, без правок PR #4.
> **Обновлено 2026-09-28 (после мержа).** Вопрос решён концептуально, реализация — открыта.
Значит наш боевой стенд Балды **пока не получил** фиксы tool_calls. Синхронизировать =
перенести `hostedagent/openai.go` из `~/Developer/runtime-local` в `~/Developer/balda/runtime-local`
(плюс решить, как версионировать: submodule / копия / отдельная ветка).
Уточнено фактами (**прежнее описание в этом разделе было неточным**):
🔴 **Решать после мержа PR #4** — иначе копия разъедется повторно при следующей правке апстрима.
- `~/Developer/balda/runtime-local` — **НЕ git-клон runtime и не «HEAD `05342c5a` runtime»**.
Это **простой копированный снапшот** каталога runtime (есть `AGENTS.md`, `agentconfig/`,
`hostedagent/`, `go.mod` и т.д., **но нет `.git`**). Каталог **git-ignored** в balda
(`git ls-files runtime-local` → 0).
- От `upstream/main` отличаются **ровно 3 файла**, и все три — **устаревшие** версии того,
что уже влито:
`agentfactory/agentfactory.go`, `hostedagent/openai.go`, `hostedagent/openai_test.go`.
- `agentconfig/config.go` содержит `DedupKey` — **0 вхождений** (PR #5 отсутствует).
⇒ **Потери при замене нет.** Подключён через
`replace github.com/normahq/runtime/v2 => ./runtime-local` (`go.mod:368`).
🔴 **Почему это больше не нужно:** PR #4 и PR #5 **влиты в `normahq/runtime`**, а мейнтейнер
довёл оба до финального вида (`d83b8e0`, `f8011c1`, `e888d5e`, `facb0d0`).
Форк `mallexxx/runtime` и `replace` утратили смысл.
**Развилка (решение за Alex)** — [[personal/plans/balda-runtime-rebuild-mm-validation]] §1/§3:
1. убрать `replace`, поднять `require github.com/normahq/runtime/v2` до версии с PR4+PR5 (штатный путь);
2. либо заменить содержимое `runtime-local` на чекаут `upstream/main` @ `c41939f`, оставив `replace`.
⚠️ Обновить комментарий в `go.mod:365-367` — он ссылается на PR #4 как на **открытый**.
## Связанные заметки
- [[personal/plans/runtime-pr5-toolset-dedup-review-fixes]] — PR #5, дедуп MCP-тулсетов по явному ключу
- [[personal/plans/balda-runtime-rebuild-mm-validation]] — сборка + деплой на Kraken + валидация
- [[personal/tech/adk-hostedagent-openai-mcp]] — MCP/tool_calls архитектура, этот патч
- [[personal/projects/balda/balda-valera]] — где патч применяется в бою
- [[personal/tech/balda-go-fx-pitfalls]] — метод дельт, питфоллы
@@ -1,11 +1,42 @@
> Создан 2026-09-28. Статус: **✅ РЕАЛИЗОВАНО, ЗАПУШЕНО. Ре-ревью ожидается.**
> Голова PR: **`d4ab923`** (4 файла, +411/−1), ветка `fix/hosted-toolsets-duplicate-endpoint`.
> **PR normahq/runtime#5** — OPEN. Ответ ревьюеру опубликован:
> https://github.com/normahq/runtime/pull/5#issuecomment-5867224210
> Создан 2026-09-28. Статус: **✅ ВЛИТО В АПСТРИМ (2026-09-28T13:22:13Z).**
> Мой коммит: **`d4ab923`** (4 файла, +411/−1), ветка `fix/hosted-toolsets-duplicate-endpoint`.
> **PR normahq/runtime#5** — **MERGED** (merge-коммит `6775d71`).
> Ответ ревьюеру: https://github.com/normahq/runtime/pull/5#issuecomment-5867224210
> Репо: `~/Developer/runtime-local`. Связано: [[personal/plans/runtime-pr4-openai-toolcalls-fixes]].
---
## 0а. 🔴 ПОСТ-МЕРЖ: мейнтейнер нашёл 2 дефекта в МОЁМ фиксе
После моего `d4ab923` и **до** мержа в ветку прилетели 3 коммита мейнтейнера
(`f8011c1`, `e888d5e`, `facb0d0` — head PR стал `facb0d0`). Это **code review моего кода**,
и два замечания — настоящие дефекты:
| Коммит | Что нашёл | Почему это мой дефект |
|---|---|---|
| `f8011c1` `fix(agentfactory): preserve MCP dedup metadata` | **`hydrateMCPServerConfig` не переносил `DedupKey`/`DedupPreferred`** в гидратированную копию | 🔴 **Фикс был бы мёртвым в продовом пути `Build()`.** `hostedToolsets` читает гидратированные конфиги → ключ всегда пуст → дедуп никогда не срабатывает. Мой юнит-тест звал `hostedToolsets` **напрямую**, минуя гидратацию, поэтому дефект не поймал |
| `e888d5e` `fix(agentconfig): validate preferred MCP dedup` | `DedupPreferred: true` без `DedupKey` не отвергался | Бессмысленная конфигурация проходила молча |
| `facb0d0` `fix(agentfactory): keep MCP dedup silent` | Убрал мой `fmt.Printf` из библиотеки; добавил ошибку при **нескольких** `DedupPreferred` в одной группе | Библиотека не должна писать в stdout; молчаливый выбор победителя делал активную авторизацию зависимой от порядка id |
| — | **`strings.TrimSpace(key)` — нормализация ключа** | 🔴 Ключ обязан быть **opaque**: `"tenant-a"` и `" tenant-a "` — разные namespace'ы. Нормализация склеила бы независимо сконфигурированные namespace'ы и воссоздала бы cross-tenant merge, который этот API и должен предотвращать |
**Заменённые строки в моём коде:**
```go
// было (мой вариант) // стало (upstream/main)
key := strings.TrimSpace(cfg.DedupKey) key := cfg.DedupKey
if strings.TrimSpace(key) == "" { continue }
skip[id] = true skip[id] = true // без fmt.Printf
```
+ `hydrateMCPServerConfig` теперь копирует `DedupKey`/`DedupPreferred`.
📌 **ГЛАВНЫЙ УРОК (durable):** юнит-тест, вызывающий внутреннюю функцию **напрямую**,
не покрывает продовый путь, где конфиг проходит через **гидратацию/нормализацию**.
Мейнтейнер добавил `TestFactoryBuildKeepsPreferredDedupEndpoint` — он идёт через
`f.Build()` (registry lookup → hydration → hostedToolsets). **Правило: тестировать сквозь
композиционный путь, а не только целевую функцию.** Этот дефект дал бы «зелёные гейты
и неработающий фикс» — ровно тот класс ошибки, против которого был весь PR.
---
## 0. ИТОГ — что сделано
Коммит **`d4ab923`** `fix(agentfactory): dedup MCP toolsets by explicit key`.
@@ -116,15 +147,24 @@ go tool golangci-lint run ./... → 0 issues
## 5. Осталось сделать
1. ⏳ **Ре-ревью metalagman** — ждать.
2. ⛔ **Синхронизация копии рантайма в Balda** (`~/Developer/balda/runtime-local`, HEAD `05342c5a`,
`grep -c DedupKey agentconfig/config.go` → **0**). Правки Balda-слоя (§0) **сейчас не компилируются**
против этой копии — ошибки `unknown field DedupKey in struct literal of type agentconfig.MCPServerConfig`.
Это **отдельная задача**, как и в PR #4 (решать после мержа, чтобы копия не разъехалась снова).
🔴 Пока не синхронизировано — боевой стенд Балды фикс **не получил**.
1. ✅ **Ре-ревью пройдено, PR влит** (`6775d71`).
2. 🔴 **Синхронизация копии рантайма в Balda — ОТКРЫТО, блокирует боевой стенд.**
`~/Developer/balda/runtime-local` — **не клон runtime**, а **снапшот кода эпохи PR #4**
(3 файла отличаются от `upstream/main`, все три — в пользу апстрима; `DedupKey` — **0 вхождений**).
Каталог **git-ignored** в balda и подключён через
`replace github.com/normahq/runtime/v2 => ./runtime-local`.
Правки Balda-слоя из §0 **не компилируются** против него
(`unknown field DedupKey in struct literal of type agentconfig.MCPServerConfig`).
✅ **Потери при замене НЕТ** — проверено `diff -rq`:
отличаются только `agentfactory/agentfactory.go`, `hostedagent/openai.go`,
`hostedagent/openai_test.go` (все — устаревшие версии того, что уже в апстриме).
⚠️ В апстриме мой код **уже исправлен мейнтейнером** (§0а) — синхронизировать надо с
`upstream/main` (`c41939f`), а не с моим `d4ab923`.
План и развилка (убрать `replace` совсем vs заменить копию) —
[[personal/plans/balda-runtime-rebuild-mm-validation]] §1/§3.
3. ⚠️ `/tmp/negctl.sh`, `/tmp/control1.py`, `/tmp/control2.py`, `/tmp/af.bak`, `/tmp/dt.bak`,
`/tmp/commitmsg.txt`, `/tmp/reply.md`, `/tmp/pr5_af.go`, `/tmp/pr5_test.go` — временные.
4. ⚠️ Проверить, не утекла ли ветка PR #4 (`feat/openai-tool-calls-v2`) — она тоже открыта.
4. ✅ Ветки PR #4/#5 больше не «утекли» — обе влиты.
---
@@ -8,6 +8,11 @@
> **отклонены ревью PR #5**. §4a объясняет причину и **описывает замену, реализованную в `d4ab923`**
> (явный `DedupKey`/`DedupPreferred` вместо вывода из URL). §5 и §6 помечены `✅ ИСПРАВЛЕНО`.
> Читать §4 только как историю — не копировать.
>
> 🔴🔴 **ОБНОВЛЕНО ВТОРИЧНО (2026-09-28, post-merge):** PR #4 и PR #5 **ВЛИТЫ** в апстрим
> (`c6dfc6d`, `6775d71`). **Мейнтейнер нашёл 2 настоящих дефекта в моём фиксе** — см. §4б.
> **Обязательно читать перед следующей правкой конфиг-поля.** Урок: фикс может пройти все
> гейты и быть **мёртвым** в продовом пути.
---