From 487f7b7c9d709d78b195148951107e5dca46c305 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 01:58:14 +0300 Subject: [PATCH] fix: explain chat session and runtime failures --- cmd/creator-web/chat_test.go | 13 ++++++ cmd/creator-web/static/assets/chat.js | 45 ++++++++++++++----- dev/ACTIVE_TASK.json | 4 +- ...EV-000090-chat-runtime-error-recovery.json | 23 ++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 dev/tasks/DEV-000090-chat-runtime-error-recovery.json diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go index 85c9e85..d4538d2 100644 --- a/cmd/creator-web/chat_test.go +++ b/cmd/creator-web/chat_test.go @@ -37,6 +37,19 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) { } } +func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) { + body, err := staticFiles.ReadFile("static/assets/chat.js") + if err != nil { + t.Fatal(err) + } + script := string(body) + for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} { + if !strings.Contains(script, required) { + t.Fatalf("chat script is missing recovery message %q", required) + } + } +} + func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) { handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive") if err != nil { diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 79f71df..a397cca 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -24,6 +24,14 @@ let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || ""; let localHistory = []; let cognitiveStatusTimer = null; +class ChatApiError extends Error { + constructor(status, detail = "") { + super(detail || `chat api returned ${status}`); + this.name = "ChatApiError"; + this.status = status; + } +} + function containsSecretShapedText(value) { return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value); } @@ -58,7 +66,11 @@ async function loadCognitiveStatus() { try { const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"}); renderCognitiveStatus(result.State || result.state || "UNKNOWN"); - } catch { + } catch (error) { + if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) { + expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор."); + return; + } renderCognitiveStatus("UNAVAILABLE"); } } @@ -116,10 +128,21 @@ async function api(path, options = {}) { const headers = {"Content-Type": "application/json", ...(options.headers || {})}; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(path, {...options, headers}); - if (!response.ok) throw new Error(await response.text()); + if (!response.ok) throw new ChatApiError(response.status, await response.text()); return response.json(); } +function expireSession(message = "") { + token = ""; + conversationID = ""; + sessionStorage.removeItem("domovoy.chat.token"); + sessionStorage.removeItem("domovoy.chat.conversation"); + messages.replaceChildren(); + updateActiveFocus(""); + if (message) loginError.textContent = message; + showLogin(); +} + function humanMeta(result) { if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам."; if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`; @@ -249,8 +272,14 @@ async function sendChatMessage(text, options = {}) { await loadToday(); activateTab("today-panel"); } - } catch { - appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime."); + } catch (error) { + if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) { + expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор."); + } else if (error instanceof ChatApiError && error.status >= 500) { + appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд."); + } else { + appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку."); + } } finally { sendButton.disabled = false; messageInput.focus(); @@ -323,13 +352,7 @@ if (aiDataForm) { if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError()); logoutButton.addEventListener("click", () => { - token = ""; - conversationID = ""; - sessionStorage.removeItem("domovoy.chat.token"); - sessionStorage.removeItem("domovoy.chat.conversation"); - messages.replaceChildren(); - updateActiveFocus(""); - showLogin(); + expireSession(); }); if (token) { diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 7b06cc1..b9f7960 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json", - "task_id": "DEV-000089", + "task_file": "dev/tasks/DEV-000090-chat-runtime-error-recovery.json", + "task_id": "DEV-000090", "task_revision": 1 } diff --git a/dev/tasks/DEV-000090-chat-runtime-error-recovery.json b/dev/tasks/DEV-000090-chat-runtime-error-recovery.json new file mode 100644 index 0000000..9d163a4 --- /dev/null +++ b/dev/tasks/DEV-000090-chat-runtime-error-recovery.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000090", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "57fb572d5b7dadff9fd82e995448c1fea30e1b5f", + "risk_tier": "E", + "intent": "Make Domovoy chat failures understandable and recoverable by distinguishing expired sessions, runtime outages, and invalid requests in the human UI.", + "requirement_ids": ["Human sees the actual recovery state", "Expired sessions return to login", "Runtime failure does not pretend to be a model answer"], + "allowed_change_scope": {"modules": ["creator-web", "development-governance"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/assets/chat.js", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000090-chat-runtime-error-recovery.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing creator-web HTTP status contract"], "may_change": ["human-facing chat error handling"]}, + "semantic_owner": "creator-web", + "canonical_state": [], + "state_transitions": ["401/403 -> clear session and show login", "5xx -> explain runtime outage and allow retry", "other client error -> explain invalid request"], + "side_effects_and_privileges": ["No new privilege", "No internal identifiers or raw upstream error text shown"], + "persistence_and_migrations": ["No persistence or migration"], + "failure_retry_restart": ["Session recovery is local and deterministic", "Runtime outage remains retryable"], + "assumptions": ["Creator Web status codes remain the source of truth for session and runtime failure classes"], + "acceptance_scenarios": ["Expired browser token returns to login with an explanation", "Runtime outage shows a useful retry message", "Valid responses remain unchanged", "No generic 'message not accepted runtime' text remains"], + "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}