feat(ai): typed CONTEXT_WINDOW_EXCEEDED error that hides the pointless Retry (#22488)
## 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)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22488?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+6
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user