1.5.3

Fixed (#395)

  • Byte-обрезка превью больше не ломает multibyte UTF-8 (#395): срезы вида content[:500] + "..." резали кириллицу/CJK/эмодзи посреди символа — PostgreSQL отвергал такие строки на INSERT (SQLSTATE 22021: неверная последовательность байт ... 0xd1 0x2e), из-за чего на PG-инстансах молча терялись персист tool-результатов и memory_contextmemory_search — оба пути сразу; баг с v0.167.8, на SQLite невидим — тот не валидирует UTF-8). Новый пакет internal/truncate — единая точка обрезки: UTF8(s, maxRunes, marker) — рунный бюджет, Bytes(s, maxBytes, marker) — байтовый с откатом к границе руны (контракты вида MCP MaxResponseBytes), HeadTail — head+tail-превью по рунам. Переведены все найденные обрезки: tools/memory.go (3, источник прод-ошибок), tools/tool_log.go (6), tools/chat_history.go, tools/message_get.go, tools/exec.go, tools/web_fetch.go (head/tail), tools/mcp.go, tools/email_send.go (тема письма в имени файла), plan_strategy.go (3), subagent.go, tool_preview.go + дубликат-хелперы tool_audit_hook.go/skill_run_steps_hook.go (audit-суммари), server/handler/document.go (2). Бюджеты превью тулов — в рунах («символы», как в докстрингах/доках); audit-суммари maxArgsSummary/maxResultSummary — в символах (контракт docs/audit); короткие превью iteration-лога (tool_preview.go) — сознательно байтовые (наследие, комментарий в коде). webhook.go maskToken не переводился: токены ASCII, срез безопасен.

Review (#395)

  • ocr review (несколько прогонов) — закрыты: high «смена семантики audit-суммари 500 символов → байты» (переведены на UTF8, рунный бюджет — как в docs/audit; комментарий-контракт в коде); medium «web_fetch head/tail должен идти через пакет» (добавлен truncate.HeadTail); low «докстринг Bytes: предусловие валидности s/marker», «неточный комментарий в mcp.go», «тонкие обёртки chat_history» (инлайн), «докстринг обёртки truncateUTF8», low «tool_preview — UTF8 или Bytes» (зафиксировано комментарием: Bytes осознанно). Отклонены с обоснованием: low «Sprintf-паника на постороннем % в midFormat HeadTail» — посылка неверна: fmt.Sprintf не паникует на несовпадении глаголов (даёт %!d(MISSING)), а предлагаемый guard !strings.Contains(midFormat, "%d") → "" ломал бы легитимный маркер без счётчика; контракт задокументирован в докстринге. Проверки: go test ./... -count=1, golangci-lint run ./... (0), go vet, make build-cross (6 платформ) — зелёные; фронт не менялся.