This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 1a2affb3c fix(ai): keep stale stream cleanup off the active send guard 
(#4861)
1a2affb3c is described below

commit 1a2affb3c473ce4d2a24ac5a85cf8dc2dda68eb6
Author: Hanabi <[email protected]>
AuthorDate: Thu Sep 24 18:19:09 2026 +0800

    fix(ai): keep stale stream cleanup off the active send guard (#4861)
    
    fix(ai): keep stale stream cleanup off the active send guard
---
 web/src/pages/ai/hooks/useAgentRun.test.ts | 27 +++++++++++++++++++++++++++
 web/src/pages/ai/hooks/useAgentRun.ts      |  4 ++--
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/ai/hooks/useAgentRun.test.ts 
b/web/src/pages/ai/hooks/useAgentRun.test.ts
index 768b05197..3e49bc4d0 100644
--- a/web/src/pages/ai/hooks/useAgentRun.test.ts
+++ b/web/src/pages/ai/hooks/useAgentRun.test.ts
@@ -494,4 +494,31 @@ describe('useAgentRun', () => {
     expect(result.current.error).toBe('');
     expect(onError).not.toHaveBeenCalled();
   });
+
+  it('keepsTheNewStreamGuardWhenAnAbortedStreamSettlesTest', async () => {
+    const { result, rerender } = render();
+    let firstSend!: Promise<void>;
+    await act(async () => {
+      firstSend = result.current.send(7, { message: 'first conversation' });
+    });
+
+    await act(async () => {
+      rerender({ id: 8 });
+    });
+    expect(openedStreams[0].signal.aborted).toBe(true);
+
+    await act(async () => {
+      void result.current.send(8, { message: 'new conversation' });
+    });
+    await act(async () => {
+      openedStreams[0].fail(new DOMException('Aborted', 'AbortError'));
+      await firstSend;
+    });
+
+    await act(async () => {
+      void result.current.send(8, { message: 'duplicate send' });
+    });
+    expect(openedStreams).toHaveLength(2);
+    expect(result.current.isStreaming).toBe(true);
+  });
 });
diff --git a/web/src/pages/ai/hooks/useAgentRun.ts 
b/web/src/pages/ai/hooks/useAgentRun.ts
index 03f0c1a57..cf7f466de 100644
--- a/web/src/pages/ai/hooks/useAgentRun.ts
+++ b/web/src/pages/ai/hooks/useAgentRun.ts
@@ -251,12 +251,12 @@ export function useAgentRun(
   const finishStream = useCallback(
     async (requestId: number, controller: AbortController, streamFailure: 
unknown | null) => {
       if (abortControllerRef.current === controller) 
abortControllerRef.current = null;
+      // A newer stream (or a navigation) owns the guard and UI now.
+      if (requestId !== streamRequestIdRef.current) return;
       // Released before the refetch is awaited on purpose: a reload that 
hangs must not leave the
       // composer unable to send. A send that overtakes this finally block 
bumps the request id, and
       // the guard after the await below then keeps this run's cleanup off the 
newer one's state.
       chatInFlightRef.current = false;
-      // A newer stream (or a navigation) owns the UI now; touching state here 
would clobber it.
-      if (requestId !== streamRequestIdRef.current) return;
 
       setIsStreaming(false);
       // The stream is over, so there is nothing left to wait for whichever 
way it ended: clearing

Reply via email to