0.167.23
Fixed
- removeOrphanedToolMessages: асимметрия проходов пропускала orphan tool_call_id на провод ( — P2). Проход 1 был глобальным (результат валиден, если id объявлен любым assistant где угодно — даже позже), проход 2 — только смежным (поиск обрывался на первом несмежном сообщении). Следствия: (а) результат, отделённый от своего assistant-хода интерлюдией (user/другой assistant), проходил проход 1, но проход 2 срезал его tool_call → orphan tool на проводе → 400 от OpenAI/Anthropic; (б) результат, стоящий ПЕРЕД своим assistant-ходом (смешанная история после агрессивной обрезки), выживал в обоих проходах как «парная», но неправильно упорядоченная пара — API это тоже отвергает. Фикс: проход 1 — forward-scan с позиционным правилом (результат выживает, только если assistant объявил id РАНЬШЕ него); проход 2 — глобальный по выжившим (вызов выживает, если его результат выжил; благодаря проходу 1 он гарантированно стоит после объявившего assistant). Плюс: malformed-вызовы (пустой ID или Name — материал для API-400, ранее только логировались buildParams) больше не объявляют свой id в проходе 1 → их результаты удаляются вместе с ними, сирота от malformed-вызова невозможен; assistant, у которого после чистки не осталось ни Content, ни ToolCalls, получает плейсхолдер
emptyContentPlaceholder(единая константа с sanitizeMessage — раньше sanitizeMessage уже отработал и повторно не заполнял). Валидация в buildParams превращена в пост-условие-тревогу. АгентскийrepairToolPairs(context.go) сознательно не переиспользуется: providers.Message — транспортный тип, а позиционное правило здесь строгое по требованиям wire-протокола. Тесты (+6): результат перед assistant → удаляется вместе с вызовом, assistant не уходит пустым; результат через интерлюдию → пара сохраняется; все вызовы срезаны → плейсхолдер; malformed (пустые ID/Name/ToolCallID, включая результат у malformed-вызова) → вычищаются согласованно; список из одних tool → пусто; параллельные вызовы ×3 → сохраняются все.