unbridled-41 opened a new pull request, #5440:
URL: https://github.com/apache/rocketmq-dashboard/pull/5440

   ## Problem / Evidence
   
   Stopping a run twice (a second tab, or a retried `POST 
/api/ai/runs/{id}/stop`, which the controller documents as idempotent) makes 
the server write its own terminal state while the first stop's worker is still 
aborting:
   
   - `AgentRunRegistry.stop` 
(`ops/ai/conversation/AgentRunRegistry.java:175-185`) returns 
`handle.stop(reason)`, which is **false** both when no handle exists and when 
the abort CAS was already decided (`AgentRunHandle.requestStop:213-216`). The 
javadoc even says "false when the run was already aborted, in which case this 
call did nothing at all".
   - `AiRunService.stop:379-387` reads that single boolean as "nothing in this 
process owns it" and calls `runExecutor.terminate(detachedContext(run), 
STOPPED, USER_STOP, ...)`.
   - `AiRunExecutor.terminate` builds a **new** context whose `beginTerminal()` 
CAS no worker holds, so it wins: it appends `TimelineEvent.RunStatus(STOPPED)` 
at `MAX(seq)+1`, rewrites the run row, and its `finally` calls 
`registry.finish(...)`, completing/closing every observer.
   - The first stop is still inside `AgentRunHandle.awaitStop` for the SIGTERM 
grace (up to `studio.ai...stop-grace`, 3s), so the worker's buffered final text 
then flushes into that same `MAX(seq)+1`; `AiEventSink.insert` rethrows 
`DuplicateKeyException` and `AiRunExecutor.execute` only logs `could not 
finalise agent run`. The detached row wins and the last chunk of the answer is 
lost.
   - On the client the duplicate terminal frame is visible: 
`web/src/pages/ai/render/foldTimeline.ts:126-137` closes the bubble on the 
first `run_status` and opens a new empty one for the second.
   
   ## Root cause / Fix
   
   One boolean carried two meanings. Distinguish them with the handle the 
registry already exposes and leave a run this process is stopping to its worker:
   
   ```java
   } else if (registry.handle(run.getId()).isPresent()) {
       log.info("agent run {} is already stopping", run.getId());
   } else {
       // unchanged detached path
   }
   ```
   
   `registry.stop` keeps its contract (and the tests pinning `true`/`false` for 
the handle cases are untouched).
   
   ## Priority & scoring
   
   PRIORITY 73 = 影响 30 (duplicate terminal row, phantom empty bubble, and the 
run's last output silently dropped) + 波及 12 (AI run lifecycle and every 
observer of the run) + 可复现 16 (deterministic two-tab/retried stop) + 维护价值 15 
(removes an ambiguity the javadoc already warned about).
   
   FIX_CONFIDENCE 85: the handle is already available; the detached path stays 
for exactly the case it was written for, and the new regression pins that a 
second stop writes nothing.
   
   ## Tests
   
   ```
   cd server && mvn -o test 
-Dtest='org.apache.rocketmq.studio.ops.ai.conversation.*Test'
   ```
   
   - Red before the fix: 
`secondStopOfAnAbortingRunShouldNotWriteItsOwnTerminalStateTest` fails — the 
service writes a `run_status` row and a run update for a run whose worker is 
already stopping.
   - Green after: `Tests run: 202, Failures: 0, Errors: 0, Skipped: 0` 
(includes `AgentRunHandleStopTest`, which keeps pinning the handle-level no-op).
   - `mvn -o checkstyle:check` passes.
   
   ## Risk
   
   Low. Only the branch taken when a handle exists but its abort was already 
decided changes; an unowned non-terminal run still gets the detached terminal 
write that keeps the stop button from spinning.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to