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
Audit Findings
- File:
src/tools/index.js — Tools are registered in two maps: TOOL_PERMISSIONS (name to required permission scopes) and TOOL_FACTORIES (name to factory function). A new tool would need entries in both.
- File:
src/tools/date.js — Example of a minimal tool pattern: implementation function (dateImpl), LangChain tool() definition with schema, and a factory function (createDateTool) that accepts runtime options. The scanAgents tool would follow this same pattern.
- File:
src/tools/filesystem.js — Contains createReadFileTool which handles file reading with path validation and size limits. The scanAgents tool would need similar path validation to ensure it only reads from allowed paths.
- Integration point:
src/tools/index.js:188-256 — The buildToolConfig function iterates over TOOL_PERMISSIONS, checks permissions, and calls the factory. A new tool with no required permissions (like date) would be registered in the switch statement at line 192-198 or handled by the default case at line 251-254.
- AGENTS.md location: The file exists at the project root (
/home/jason/Projects/madz/AGENTS.md, 15KB). The tool should check process.cwd() for AGENTS.md and read it if present, silently doing nothing if absent.
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. accidentalAudit Findings
src/tools/index.js— Tools are registered in two maps:TOOL_PERMISSIONS(name to required permission scopes) andTOOL_FACTORIES(name to factory function). A new tool would need entries in both.src/tools/date.js— Example of a minimal tool pattern: implementation function (dateImpl), LangChaintool()definition with schema, and a factory function (createDateTool) that accepts runtime options. ThescanAgentstool would follow this same pattern.src/tools/filesystem.js— ContainscreateReadFileToolwhich handles file reading with path validation and size limits. ThescanAgentstool would need similar path validation to ensure it only reads from allowed paths.src/tools/index.js:188-256— ThebuildToolConfigfunction iterates overTOOL_PERMISSIONS, checks permissions, and calls the factory. A new tool with no required permissions (likedate) would be registered in theswitchstatement at line 192-198 or handled by thedefaultcase at line 251-254./home/jason/Projects/madz/AGENTS.md, 15KB). The tool should checkprocess.cwd()forAGENTS.mdand read it if present, silently doing nothing if absent.