diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 8089fdbdef4..74dba6ba52c 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -9,6 +9,7 @@ import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools" import { Package } from "../../shared/package" import { t } from "../../i18n" +import { AskIgnoredError } from "../task/AskIgnoredError" import { fetchInstructionsTool } from "../tools/FetchInstructionsTool" import { listFilesTool } from "../tools/ListFilesTool" @@ -224,6 +225,11 @@ export async function presentAssistantMessage(cline: Task) { } const handleError = async (action: string, error: Error) => { + // Silently ignore AskIgnoredError - this is an internal control flow + // signal, not an actual error. It occurs when a newer ask supersedes an older one. + if (error instanceof AskIgnoredError) { + return + } const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` await cline.say( "error", @@ -610,6 +616,11 @@ export async function presentAssistantMessage(cline: Task) { } const handleError = async (action: string, error: Error) => { + // Silently ignore AskIgnoredError - this is an internal control flow + // signal, not an actual error. It occurs when a newer ask supersedes an older one. + if (error instanceof AskIgnoredError) { + return + } const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` await cline.say( diff --git a/src/core/task/AskIgnoredError.ts b/src/core/task/AskIgnoredError.ts new file mode 100644 index 00000000000..9c7f0f0ffeb --- /dev/null +++ b/src/core/task/AskIgnoredError.ts @@ -0,0 +1,15 @@ +/** + * Error thrown when an ask promise is superseded by a newer one. + * + * This is used as an internal control flow signal - not an actual error. + * It occurs when multiple asks are sent in rapid succession and an older + * ask is invalidated by a newer one (e.g., during streaming updates). + */ +export class AskIgnoredError extends Error { + constructor(reason?: string) { + super(reason ? `Ask ignored: ${reason}` : "Ask ignored") + this.name = "AskIgnoredError" + // Maintains proper prototype chain for instanceof checks + Object.setPrototypeOf(this, AskIgnoredError.prototype) + } +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d084bf4b924..1cd3f03c263 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4,6 +4,8 @@ import os from "os" import crypto from "crypto" import EventEmitter from "events" +import { AskIgnoredError } from "./AskIgnoredError" + import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import delay from "delay" @@ -983,7 +985,7 @@ export class Task extends EventEmitter implements TaskLike { // whole array in new listener. this.updateClineMessage(lastMessage) // console.log("Task#ask: current ask promise was ignored (#1)") - throw new Error("Current ask promise was ignored (#1)") + throw new AskIgnoredError("updating existing partial") } else { // This is a new partial message, so add it with partial // state. @@ -992,7 +994,7 @@ export class Task extends EventEmitter implements TaskLike { console.log(`Task#ask: new partial ask -> ${type} @ ${askTs}`) await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial, isProtected }) // console.log("Task#ask: current ask promise was ignored (#2)") - throw new Error("Current ask promise was ignored (#2)") + throw new AskIgnoredError("new partial") } } else { if (isUpdatingPreviousPartial) { @@ -1146,7 +1148,7 @@ export class Task extends EventEmitter implements TaskLike { // command_output. It's important that when we know an ask could // fail, it is handled gracefully. console.log("Task#ask: current ask promise was ignored") - throw new Error("Current ask promise was ignored") + throw new AskIgnoredError("superseded") } const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }