From a4d9749e8bdb458421e0ca8ead785988cd39e21f Mon Sep 17 00:00:00 2001 From: Vedant Patel Date: Sat, 25 Jul 2026 23:34:36 -0700 Subject: [PATCH] fix: show clear message when widget hits token budget (429) Skip the non-streaming fallback on ConversationTokenBudgetExceeded and tell users to start a new chat instead of a misleading retry prompt. Co-authored-by: Cursor --- src/agent_manager/api/static/widget.js | 39 ++++++++------ .../api/static/widget/react/AgentChatApp.tsx | 47 +++++++++------- tests/e2e/widget.spec.ts | 54 +++++++++++++++++++ 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/src/agent_manager/api/static/widget.js b/src/agent_manager/api/static/widget.js index 8aaca32..6b23fde 100644 --- a/src/agent_manager/api/static/widget.js +++ b/src/agent_manager/api/static/widget.js @@ -52922,6 +52922,7 @@ function statusLabel(state) { // src/agent_manager/api/static/widget/react/AgentChatApp.tsx var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1); +var TOKEN_BUDGET_EXCEEDED_MESSAGE = "This conversation has reached its context limit. Start a new chat to continue."; var nextMessageCounter = 0; function nextMessageId(role) { nextMessageCounter += 1; @@ -53033,23 +53034,27 @@ function AgentChatApp({ client, config, onAnswer, panelId, titleId }) { } patchEntry(assistantId, (entry) => ({ ...entry, typing: false })); onAnswer(finalDetail); - } catch (error) { - try { - const data = await sendToAgent(text10); - patchEntry(assistantId, () => ({ - id: assistantId, - role: "ai", - text: data.answer, - route: data.visited, - tools: data.used_tools - })); - onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] }); - } catch { - patchEntry(assistantId, () => ({ - id: assistantId, - role: "ai", - text: error instanceof Error && error.message ? `Something went wrong. Please try again.` : "Something went wrong. Please try again." - })); + } catch (streamError) { + if (streamError instanceof AgentChatHttpError && streamError.status === 429) { + patchEntry(assistantId, () => ({ id: assistantId, role: "ai", text: TOKEN_BUDGET_EXCEEDED_MESSAGE })); + } else { + try { + const data = await sendToAgent(text10); + patchEntry(assistantId, () => ({ + id: assistantId, + role: "ai", + text: data.answer, + route: data.visited, + tools: data.used_tools + })); + onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] }); + } catch (sendError) { + patchEntry(assistantId, () => ({ + id: assistantId, + role: "ai", + text: sendError instanceof AgentChatHttpError && sendError.status === 429 ? TOKEN_BUDGET_EXCEEDED_MESSAGE : "Something went wrong. Please try again." + })); + } } } finally { setSending(false); diff --git a/src/agent_manager/api/static/widget/react/AgentChatApp.tsx b/src/agent_manager/api/static/widget/react/AgentChatApp.tsx index 4bd932c..3ce0ed7 100644 --- a/src/agent_manager/api/static/widget/react/AgentChatApp.tsx +++ b/src/agent_manager/api/static/widget/react/AgentChatApp.tsx @@ -34,6 +34,9 @@ type MessageEntry = { tools?: ToolRecord[]; }; +const TOKEN_BUDGET_EXCEEDED_MESSAGE = + "This conversation has reached its context limit. Start a new chat to continue."; + let nextMessageCounter = 0; function nextMessageId(role: MessageEntry["role"]): string { @@ -166,26 +169,30 @@ export function AgentChatApp({ client, config, onAnswer, panelId, titleId }: Age } patchEntry(assistantId, (entry) => ({ ...entry, typing: false })); onAnswer(finalDetail); - } catch (error) { - try { - const data = await sendToAgent(text); - patchEntry(assistantId, () => ({ - id: assistantId, - role: "ai", - text: data.answer, - route: data.visited, - tools: data.used_tools, - })); - onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] }); - } catch { - patchEntry(assistantId, () => ({ - id: assistantId, - role: "ai", - text: - error instanceof Error && error.message - ? `Something went wrong. Please try again.` - : "Something went wrong. Please try again.", - })); + } catch (streamError) { + if (streamError instanceof AgentChatHttpError && streamError.status === 429) { + patchEntry(assistantId, () => ({ id: assistantId, role: "ai", text: TOKEN_BUDGET_EXCEEDED_MESSAGE })); + } else { + try { + const data = await sendToAgent(text); + patchEntry(assistantId, () => ({ + id: assistantId, + role: "ai", + text: data.answer, + route: data.visited, + tools: data.used_tools, + })); + onAnswer({ visited: data.visited ?? [], used_tools: data.used_tools ?? [] }); + } catch (sendError) { + patchEntry(assistantId, () => ({ + id: assistantId, + role: "ai", + text: + sendError instanceof AgentChatHttpError && sendError.status === 429 + ? TOKEN_BUDGET_EXCEEDED_MESSAGE + : "Something went wrong. Please try again.", + })); + } } } finally { setSending(false); diff --git a/tests/e2e/widget.spec.ts b/tests/e2e/widget.spec.ts index 2c94f2b..4da1f83 100644 --- a/tests/e2e/widget.spec.ts +++ b/tests/e2e/widget.spec.ts @@ -432,6 +432,60 @@ test("backend error renders a user-friendly message", async ({ page }) => { await expect.poll(() => shadowText(page, ".messages")).toContain("Something went wrong. Please try again."); }); +async function mockConversationApiWithTokenBudgetExceeded(page: Page) { + const calls: string[] = []; + + await page.route("**/conversations", async (route) => { + calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ conversation_id: "conv-budget", session_id: "conv-budget" }), + }); + }); + + await page.route("**/conversations/*/messages/stream", async (route: Route) => { + calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`); + await route.fulfill({ + status: 429, + contentType: "application/json", + body: JSON.stringify({ detail: "ConversationTokenBudgetExceeded" }), + }); + }); + + await page.route("**/conversations/*/messages", async (route: Route) => { + calls.push(`${route.request().method()} ${new URL(route.request().url()).pathname}`); + if (route.request().method() === "GET") { + await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) }); + return; + } + await route.fulfill({ + status: 429, + contentType: "application/json", + body: JSON.stringify({ detail: "ConversationTokenBudgetExceeded" }), + }); + }); + + return calls; +} + +test("token budget exceeded shows context-limit message instead of generic error", async ({ page }) => { + const calls = await mockConversationApiWithTokenBudgetExceeded(page); + await page.goto("/widget-demo.html"); + await shadowClick(page, ".launcher"); + await shadowFill(page, ".input", "one more message"); + await shadowClick(page, ".send"); + + await expect + .poll(() => shadowText(page, ".messages")) + .toContain("This conversation has reached its context limit. Start a new chat to continue."); + const text = await shadowText(page, ".messages"); + expect(text).not.toContain("Something went wrong. Please try again."); + expect(calls).toContain("POST /conversations/conv-budget/messages/stream"); + // Streaming already returned 429 — do not waste a second request on the non-streaming fallback. + expect(calls).not.toContain("POST /conversations/conv-budget/messages"); +}); + test("stale stored conversation is replaced before sending to the agent", async ({ page }) => { const calls = await mockConversationApiWithStaleConversation(page); await page.goto("/widget-demo.html");