0.157.0
Added
- REST:
GET /api/v1/agents/{id}/pending_ask— восстановление ждущего ask_user после F5. WebUI раньше показывал форму вопроса (ChatAskUser) только из эфемерного WS-событияask_user, которое теряется при перезагрузке страницы. Теперь эндпоинт возвращает последний неотвеченныйask_usertool_call ({call_id, question, options}) или204 No Content. Фронт (chat.svelte.ts: restorePendingAsk) вызывает его вconnectWS.onopen— как при первом подключении, так и при reconnect, с re-checkpendingAskпослеawaitпротив race с WS-событием. internal/agent/pending_ask.go— экспортируемаяFindPendingAskUser+ общий core-хелперfindPendingAskUserCore(O(n), один проход снизу-вверх с accumulating-set отвеченныхtool_call_id). Точно воспроизводит контракт приватнойfindPendingAskUser(только последующие role=tool ответы), устраняя дублирование и поведенческое расхождение. Question/options извлекаются из arguments tool_call’а (фоллбэк question на content assistant-сообщения).MaxAskUserOptions = 32— cap против патологического LLM-ответа (lossy, без сигнала caller’у — для UI recovery приемлемо, >32 options неюзабельны). Fast-pathstrings.Contains(ToolCalls, "name":"ask_user")пропускает json.Unmarshal для assistant-сообщений без ask_user.internal/store— новый методGetRecentMessagesUnfiltered(SQLite + PG + DualStore): последние N сообщений БЕЗ фильтровarchivedиrole. Нужен т.к.GetRecentUIMessagesисключаетrole='tool'(не видит ответы), аGetRecentMessagesфильтруетarchived=0(потерял бы ask_user после non-destructive compaction, см. ADR 2026-06-30).internal/server/handler/chat.go: GetPendingAsk— handler с?limit=override (1..1000, по умолчанию 200). РоутGET /agents/{id}/pending_askвrouter.go.web/src/lib/api/endpoints.ts—api.pendingAsk(agentId); тип ответа{call_id?, question?, options?}(204 →{}).- Проверки:
internal/agent/pending_ask_test.go(10 тестов: happy/fallback/answered/no-ask/empty/broken-json/cap/non-string-dropped/later-answered-earlier-pending/empty-question-content/json-shape);internal/server/handler/chat_pending_ask_test.go(4: pending-200, no-pending-204, answered-204, invalid-id-400).
Fixed
Agent: PlanExecute игнорировал
StopReason="ask_user"— вопрос пользователя терялся (P0). При стратегииplanшаг сask_userпомечался"done", план продолжался, аactor.go:810(единственное место проверкиresult.StopReason == "ask_user") не срабатывал, т.к. возвращалсяfinalResultсинтеза. → assistant сtool_callsне сохранялся, WS_ask_userне отправлялся, агент работал «вслепую» без ответа пользователя. Подтверждено по данным PostgreSQL: за 30 дней 80ask_userвtool_audit_log, но лишь 3 вmessages; за 1 июля 20:47–21:00 — 4 в audit, 0 в messages; вjournalctlотсутствовали"ask_user stop reason detected"/"ask_user sending WS event". Фикс (internal/agent/plan_strategy.go): послеrunner.Run(stepSpec)— еслиresult.StopReason == "ask_user", прервать план и вернуть шаг-результат наверх, чтобыactor.goсохранил assistant + tool_calls и отправил WS.Agent: «question+tool_calls conflict» обрезал ask_user как stop.
runner.go:145— когда LLM возвращал одновременно content (заканчивается на?) Иask_usertool_call, срабатывала эвристика «конфликт» → ход обрывался какstop, причёмassistantMsgсоздавался безtool_calls. Дляask_usercontent+tool_call — норма (LLM часто дублирует вопрос в content). Подтверждено логом"agent: question+tool_calls conflict, treating as stop" iteration=5 tool_calls=1. Фикс (internal/agent/runner.go): в условие добавлено&& !toolCallsContainName(resp.ToolCalls, "ask_user")— ask_user exempt, идёт в нормальную обработку →AskUserInterrupt.Agent:
ask_userвtool_audit_logвсегда помечалсяok=false.tool_audit_hook.go:56—ok := event.Status == "completed". Дляask_userstatus="waiting"(нормальный interrupt, ожидающий ответ) → всегдаok=false→ в audit выглядит «сломанным». Фикс:ok := event.Status == "completed" || event.Status == "waiting".Проверки:
go test ./internal/...— зелёно;npm run check— 0/0;npm test— 571/571;golangci-lint— 0 замечаний;make build-cross— 6 бинарников; циклocr review(13 итераций) — валидные замечания устранены (критичное:GetRecentMessagesфильтрует archived → введёнGetRecentMessagesUnfiltered; O(n²)→O(n) accumulating-set; поведенческое расхождение приватной/публичной версий устранено через общий core; named constMaxAskUserOptions; edge-case тесты; race re-check вrestorePendingAsk), невалидные/subjective обоснованно отклонены (map-iteration — ocr перепутал slice/map; перевод godoc на английский rationale — AGENTS.md; truncated-flag — >32 неюзабельно; билингвальные rationale-комментарии).