Summary
When a user interrupts a streaming response by sending a new message while the agent is still streaming, an error message is displayed to the user instead of gracefully handling the interruption.
Environment
- OS: Linux 7.0.2-7-pve
- Node.js: v25.8.1
- madz version: 1.18.2
- LLM provider: Unknown — user to confirm
Reproduction
Steps to reproduce the behavior:
- Start a new session
- Send "hello"
- While the agent is streaming a response, send another "hello"
- Observe the error message displayed to the user
Expected Behavior
The streaming response should be gracefully cancelled and the new message processed without showing an error message to the user.
Actual Behavior
An error message is shown to the user (similar to the message in issue 444, but with a different root cause).
Additional Context
This is distinct from issue 444 — the symptom (error message to user) is similar, but the underlying cause is different. The issue occurs specifically when a new message is sent while a streaming response is in progress, causing an interruption that is not handled gracefully.
Audit Findings
-
File: src/agent/react.js:263-276 — When the abort signal is triggered, callReactAgentStreaming() checks signal.aborted inside the for await loop and returns { content: originalMessage }. However, if agent.streamEvents() throws an error before this check is reached (e.g., during stream initialization or between event iterations), the error propagates up to the caller.
-
File: src/tui/app.js:904-927 — handleChat()'s catch block checks err.name === "AbortError" to determine whether to show a clean "Interrupted." status or an error message. If the thrown error has a different name (e.g., DOMException with name "AbortError" in some environments, or a custom error from LangChain), the error message is displayed to the user.
-
File: src/tui/app.js:947-985 — handleInterrupt() is called when the user sends a new message while streaming. It aborts the controller and awaits the same dispatchPromise that handleChat() is awaiting. When the promise rejects, BOTH catch blocks are triggered (since they await the same promise). handleInterrupt() swallows the error silently, but handleChat()'s catch block still displays the error to the user.
-
Root cause hypothesis: The error thrown when the abort signal is triggered may not have name === "AbortError", causing handleChat()'s catch block to treat it as a real error rather than a graceful interruption. Additionally, the race condition between handleInterrupt() and handleChat() means the error is shown even though the interruption was intentional.
-
Suggested fix:
- Check for abort-related errors more broadly (e.g., check
err.name for "AbortError", "Canceled", or check for abort-related properties)
- Or: Have
handleInterrupt() signal to handleChat() that the interruption was intentional, so handleChat() doesn't show an error message
- Or: Use a shared state variable to track whether the abort was intentional (from
handleInterrupt()) vs. accidental
Summary
When a user interrupts a streaming response by sending a new message while the agent is still streaming, an error message is displayed to the user instead of gracefully handling the interruption.
Environment
Reproduction
Steps to reproduce the behavior:
Expected Behavior
The streaming response should be gracefully cancelled and the new message processed without showing an error message to the user.
Actual Behavior
An error message is shown to the user (similar to the message in issue 444, but with a different root cause).
Additional Context
This is distinct from issue 444 — the symptom (error message to user) is similar, but the underlying cause is different. The issue occurs specifically when a new message is sent while a streaming response is in progress, causing an interruption that is not handled gracefully.
Audit Findings
File:
src/agent/react.js:263-276— When the abort signal is triggered,callReactAgentStreaming()checkssignal.abortedinside thefor awaitloop and returns{ content: originalMessage }. However, ifagent.streamEvents()throws an error before this check is reached (e.g., during stream initialization or between event iterations), the error propagates up to the caller.File:
src/tui/app.js:904-927—handleChat()'s catch block checkserr.name === "AbortError"to determine whether to show a clean "Interrupted." status or an error message. If the thrown error has a different name (e.g.,DOMExceptionwith name "AbortError" in some environments, or a custom error from LangChain), the error message is displayed to the user.File:
src/tui/app.js:947-985—handleInterrupt()is called when the user sends a new message while streaming. It aborts the controller and awaits the samedispatchPromisethathandleChat()is awaiting. When the promise rejects, BOTH catch blocks are triggered (since they await the same promise).handleInterrupt()swallows the error silently, buthandleChat()'s catch block still displays the error to the user.Root cause hypothesis: The error thrown when the abort signal is triggered may not have
name === "AbortError", causinghandleChat()'s catch block to treat it as a real error rather than a graceful interruption. Additionally, the race condition betweenhandleInterrupt()andhandleChat()means the error is shown even though the interruption was intentional.Suggested fix:
err.namefor "AbortError", "Canceled", or check for abort-related properties)handleInterrupt()signal tohandleChat()that the interruption was intentional, sohandleChat()doesn't show an error messagehandleInterrupt()) vs. accidental