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]