0.157.0

Added

  • REST: GET /api/v1/agents/{id}/pending_ask — восстановление ждущего ask_user после F5. WebUI раньше показывал форму вопроса (ChatAskUser) только из эфемерного WS-события ask_user, которое теряется при перезагрузке страницы. Теперь эндпоинт возвращает последний неотвеченный ask_user tool_call ({call_id, question, options}) или 204 No Content. Фронт (chat.svelte.ts: restorePendingAsk) вызывает его в connectWS.onopen — как при первом подключении, так и при reconnect, с re-check pendingAsk после 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-path strings.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.tsapi.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 дней 80 ask_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_user tool_call, срабатывала эвристика «конфликт» → ход обрывался как stop, причём assistantMsg создавался без tool_calls. Для ask_user content+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:56ok := event.Status == "completed". Для ask_user status="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 const MaxAskUserOptions; edge-case тесты; race re-check в restorePendingAsk), невалидные/subjective обоснованно отклонены (map-iteration — ocr перепутал slice/map; перевод godoc на английский rationale — AGENTS.md; truncated-flag — >32 неюзабельно; билингвальные rationale-комментарии).