From a505ed3245fdb6cb80813a06bde73e10e6c6c72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 2 Jul 2026 21:26:41 +0200 Subject: [PATCH] feat(ai): typed CONTEXT_WINDOW_EXCEEDED error that hides the pointless Retry (#22488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale When message pruning can't fit the conversation into the model's context window, `chat-execution.service.ts` throws a **raw `Error`**. `mapErrorToStreamError` classifies it as generic `STREAM_EXECUTION_FAILED`, so the client renders a standard failure with a **Retry button that deterministically fails again** — the conversation doesn't get shorter by retrying. Users loop on Retry against a permanently-failing thread. ## Why this is the root cause, not a symptom patch The failure is *terminal for the thread by construction*, and the error channel already distinguishes terminal-vs-retryable via typed `AiExceptionCode`s — this failure just never got one. Adding `CONTEXT_WINDOW_EXCEEDED` (typed exception → `UserInputError` mapping instead of a 500 → both error surfaces render the start-a-new-thread message without `onRetry`) puts it on the same rails as `API_KEY_NOT_CONFIGURED` and the other special-cased codes. Both frontend error surfaces route through `AiChatErrorRenderer`, so one case covers the in-message and under-list renderings. The deeper endgame (auto-summarize/compact older turns so threads never brick) is a multi-week feature — and this typed error remains necessary even then, as its terminal fallback. ## User impact Instead of an opaque error and a Retry that never works, users hitting the context limit get told exactly what happened and what to do (start a new thread), and monitoring stops counting a user-condition as a server error. ## Test plan - [ ] CI green - [ ] Manual: fill a thread past the model limit → typed message, no Retry on either error surface https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38 --- _Generated by [Claude Code](https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38)_ Review in cubic --- .../src/modules/ai/components/AiChatErrorRenderer.tsx | 4 ++++ .../twenty-front/src/modules/ai/utils/aiChatErrorCode.ts | 1 + .../ai/ai-chat/services/chat-execution.service.ts | 7 ++++++- .../src/engine/metadata-modules/ai/ai.exception.ts | 3 +++ .../ai/utils/ai-graphql-api-exception-handler.util.ts | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/ai/components/AiChatErrorRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AiChatErrorRenderer.tsx index b4f6e7af1a..a8217903d1 100644 --- a/packages/twenty-front/src/modules/ai/components/AiChatErrorRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AiChatErrorRenderer.tsx @@ -22,6 +22,10 @@ export const AiChatErrorRenderer = ({ return ; } + if (isGraphqlErrorOfType(error, AiChatErrorCode.CONTEXT_WINDOW_EXCEEDED)) { + return ; + } + if (isGraphqlErrorOfType(error, AiChatErrorCode.CONNECTION_LOST)) { return ; } diff --git a/packages/twenty-front/src/modules/ai/utils/aiChatErrorCode.ts b/packages/twenty-front/src/modules/ai/utils/aiChatErrorCode.ts index b106c8c978..bb534f76de 100644 --- a/packages/twenty-front/src/modules/ai/utils/aiChatErrorCode.ts +++ b/packages/twenty-front/src/modules/ai/utils/aiChatErrorCode.ts @@ -1,5 +1,6 @@ export const AiChatErrorCode = { BILLING_CREDITS_EXHAUSTED: 'BILLING_CREDITS_EXHAUSTED', + CONTEXT_WINDOW_EXCEEDED: 'CONTEXT_WINDOW_EXCEEDED', API_KEY_NOT_CONFIGURED: 'API_KEY_NOT_CONFIGURED', CONNECTION_LOST: 'CONNECTION_LOST', } as const; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index 816cac8602..90799abe8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -41,6 +41,10 @@ import { getToolMetricName } from 'src/engine/core-modules/tool-provider/utils/g import { isToolOutputSuccessful } from 'src/engine/core-modules/tool-provider/utils/is-tool-output-successful.util'; import { resolveToolName } from 'src/engine/core-modules/tool-provider/utils/resolve-tool-name.util'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + AiException, + AiExceptionCode, +} from 'src/engine/metadata-modules/ai/ai.exception'; import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service'; import { finalizeDanglingToolParts } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/finalize-dangling-tool-parts.util'; import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const'; @@ -302,8 +306,9 @@ export class ChatExecutionService { ); if (pruningResult.isStillOverLimit) { - throw new Error( + throw new AiException( 'This conversation is too long for the model to process. Please start a new thread.', + AiExceptionCode.CONTEXT_WINDOW_EXCEEDED, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai.exception.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai.exception.ts index 919d8e7650..36a7d4b1d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai.exception.ts @@ -12,6 +12,7 @@ export enum AiExceptionCode { INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT', THREAD_NOT_FOUND = 'THREAD_NOT_FOUND', WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', + CONTEXT_WINDOW_EXCEEDED = 'CONTEXT_WINDOW_EXCEEDED', INVALID_CHAT_THREAD_TITLE = 'INVALID_CHAT_THREAD_TITLE', MESSAGE_NOT_FOUND = 'MESSAGE_NOT_FOUND', QUESTION_NOT_PENDING = 'QUESTION_NOT_PENDING', @@ -39,6 +40,8 @@ const getAiExceptionUserFriendlyMessage = (code: AiExceptionCode) => { return msg`Chat thread not found.`; case AiExceptionCode.WORKSPACE_NOT_FOUND: return msg`Workspace not found.`; + case AiExceptionCode.CONTEXT_WINDOW_EXCEEDED: + return msg`This conversation is too long for the model. Start a new thread to continue.`; case AiExceptionCode.INVALID_CHAT_THREAD_TITLE: return msg`Chat thread title cannot be empty.`; case AiExceptionCode.MESSAGE_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util.ts index 5b394de6c1..b54dff9c84 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util.ts @@ -27,6 +27,7 @@ export const aiGraphqlApiExceptionHandler = (error: Error) => { case AiExceptionCode.MESSAGE_NOT_FOUND: case AiExceptionCode.ROLE_NOT_FOUND: throw new NotFoundError(error); + case AiExceptionCode.CONTEXT_WINDOW_EXCEEDED: case AiExceptionCode.INVALID_AGENT_INPUT: case AiExceptionCode.INVALID_CHAT_THREAD_TITLE: case AiExceptionCode.QUESTION_NOT_PENDING: