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 806072f30 fix(ai): drop an agent stop rejection whose run ownership 
went stale (#4960)
806072f30 is described below

commit 806072f3033e38a26a801eb31d41b7a67b9b5165
Author: aias00 <[email protected]>
AuthorDate: Thu Sep 24 03:21:52 2026 -0700

    fix(ai): drop an agent stop rejection whose run ownership went stale (#4960)
    
    fix(studio): ignore stale agent stop failures
    
    Signed-off-by: liuhy <[email protected]>
---
 web/src/pages/ai/hooks/useAgentRun.test.ts | 44 ++++++++++++++++++++++++++++++
 web/src/pages/ai/hooks/useAgentRun.ts      |  5 ++++
 2 files changed, 49 insertions(+)

diff --git a/web/src/pages/ai/hooks/useAgentRun.test.ts 
b/web/src/pages/ai/hooks/useAgentRun.test.ts
index 3e49bc4d0..b742f98f6 100644
--- a/web/src/pages/ai/hooks/useAgentRun.test.ts
+++ b/web/src/pages/ai/hooks/useAgentRun.test.ts
@@ -83,6 +83,16 @@ function stubStreamFunction(fn: unknown): StreamCall[] {
   return calls;
 }
 
+function deferred<T>() {
+  let resolve!: (value: T | PromiseLike<T>) => void;
+  let reject!: (reason?: unknown) => void;
+  const promise = new Promise<T>((res, rej) => {
+    resolve = res;
+    reject = rej;
+  });
+  return { promise, resolve, reject };
+}
+
 /** Let the requestAnimationFrame-coalesced tick fire (jsdom paints at ~16ms). 
*/
 async function flushFrame(): Promise<void> {
   await new Promise((resolve) => setTimeout(resolve, 32));
@@ -407,6 +417,40 @@ describe('useAgentRun', () => {
     expect(openedStreams[0].signal.aborted).toBe(false);
   });
 
+  it('ignoresAStopFailureAfterNavigatingToAnotherConversationTest', async () 
=> {
+    const onError = vi.fn();
+    const pendingStop = deferred<Awaited<ReturnType<typeof stopRun>>>();
+    vi.mocked(stopRun).mockReturnValue(pendingStop.promise);
+    const { result, rerender } = render({ onError });
+
+    await act(async () => {
+      void result.current.send(7, { message: 'hi' });
+      openedStreams[0].emit(runStarted());
+    });
+
+    let stopPromise!: Promise<void>;
+    await act(async () => {
+      stopPromise = result.current.stop();
+      await Promise.resolve();
+    });
+    expect(result.current.stopRequested).toBe(true);
+
+    await act(async () => {
+      rerender({ id: 8 });
+    });
+    expect(result.current.stopRequested).toBe(false);
+    expect(result.current.error).toBe('');
+
+    await act(async () => {
+      pendingStop.reject(new Error('conversation 7 stop failed late'));
+      await stopPromise;
+    });
+
+    expect(result.current.error).toBe('');
+    expect(result.current.stopRequested).toBe(false);
+    expect(onError).not.toHaveBeenCalled();
+  });
+
   it('attachesToAnAlreadyRunningRunWithTheReplayCursorTest', async () => {
     const { result } = render();
 
diff --git a/web/src/pages/ai/hooks/useAgentRun.ts 
b/web/src/pages/ai/hooks/useAgentRun.ts
index cf7f466de..c0b148501 100644
--- a/web/src/pages/ai/hooks/useAgentRun.ts
+++ b/web/src/pages/ai/hooks/useAgentRun.ts
@@ -390,6 +390,8 @@ export function useAgentRun(
     // window with nothing to address. The button stays disabled until then 
(`canStop`).
     if (targetRunId === null) return;
 
+    const requestId = streamRequestIdRef.current;
+    const generation = generationRef.current;
     setStopRequested(true);
     try {
       // The response is the run row, which the stream reports authoritatively 
anyway; the point of
@@ -398,6 +400,9 @@ export function useAgentRun(
       // Deliberately NOT aborting the fetch: the open stream is what delivers 
the terminal
       // `run_status` frame and `done`, and aborting would leave the button in 
`stopping` forever.
     } catch (stopError) {
+      // Navigation or a newer stream invalidated this Stop request. Its 
failure belongs to the old
+      // run and must not paint an error onto the conversation that owns the 
hook now.
+      if (requestId !== streamRequestIdRef.current || generation !== 
generationRef.current) return;
       setStopRequested(false);
       setError(describeThrownMessage(stopError));
       optionsRef.current.onError?.(stopError);

Reply via email to