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 b739b6a7b fix(ai): stamp gmt_modified when forgetting the runtime 
session (#7050)
b739b6a7b is described below

commit b739b6a7bf58643932dd16cc0d0fcdd070a7568f
Author: wizcraft_kris <[email protected]>
AuthorDate: Mon Oct 26 16:46:05 2026 +0800

    fix(ai): stamp gmt_modified when forgetting the runtime session (#7050)
    
    * fix(ai): stamp gmt_modified when forgetting the runtime session
    * test(ai): pin the executor clock so the forgotten-session stamp is 
asserted
    Both resume-recovery cases verified clearRuntimeSessionId with
    any(LocalDateTime.class), which also passes a stamp read from the database
    session's timezone — the exact skew the caller-supplied clock exists to 
avoid.
    Give the shared executor a fixed clock and assert the stamp it must carry:
    replacing now(clock) with now() in AiRunExecutor now turns both cases red.
---
 .../ops/ai/conversation/AiConversationRepository.java  |  6 ++++--
 .../studio/ops/ai/conversation/AiRunExecutor.java      |  2 +-
 .../MybatisPlusAiConversationRepository.java           |  8 ++++++--
 .../studio/ops/ai/conversation/AiRunExecutorTest.java  | 17 ++++++++++++++---
 .../ops/ai/conversation/AiTimelineRepositoryTest.java  | 18 ++++++++++++++++--
 5 files changed, 41 insertions(+), 10 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationRepository.java
index 98580081f..961971e8b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiConversationRepository.java
@@ -47,9 +47,11 @@ public interface AiConversationRepository {
 
     /**
      * Forgets the remembered {@code --resume} session id. A method of its own 
because {@link #update}
-     * writes non-null fields only, so it cannot express "forget this value".
+     * writes non-null fields only, so it cannot express "forget this value". 
The caller supplies
+     * {@code now} so the write can stamp {@code gmt_modified} from its own 
clock — the column is
+     * {@code ON UPDATE CURRENT_TIMESTAMP}, and the list in {@link #findPage} 
orders by it.
      */
-    void clearRuntimeSessionId(Long id);
+    void clearRuntimeSessionId(Long id, LocalDateTime now);
 
     int deleteById(Long id);
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutor.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutor.java
index f5e85e8e6..ac2206b3c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutor.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutor.java
@@ -616,7 +616,7 @@ public class AiRunExecutor {
             return;
         }
         try {
-            conversationRepository.clearRuntimeSessionId(conversation.getId());
+            conversationRepository.clearRuntimeSessionId(conversation.getId(), 
LocalDateTime.now(clock));
             conversation.setRuntimeSessionId(null);
         } catch (RuntimeException exception) {
             log.warn("could not forget the runtime session id of conversation 
{}: {}",
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
index 3448dcf46..359849b94 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/conversation/MybatisPlusAiConversationRepository.java
@@ -81,14 +81,18 @@ public class MybatisPlusAiConversationRepository implements 
AiConversationReposi
     }
 
     @Override
-    public void clearRuntimeSessionId(Long id) {
+    public void clearRuntimeSessionId(Long id, LocalDateTime now) {
         if (id == null) {
             return;
         }
         // updateById skips null fields, so "forget this value" has to name 
the column itself.
+        // gmt_modified is named too: it is ON UPDATE CURRENT_TIMESTAMP, so a 
SET clause without it
+        // lets the database stamp it in the session's timezone — the wrong 
clock for the
+        // gmt_modified ordering findPage promises. The caller owns the clock, 
as with the alert mappers.
         conversationMapper.update(null, new UpdateWrapper<RmqAiConversation>()
                 .eq("id", id)
-                .set("runtime_session_id", null));
+                .set("runtime_session_id", null)
+                .set("gmt_modified", now));
     }
 
     @Override
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutorTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutorTest.java
index f8e33c0bc..56004a90f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiRunExecutorTest.java
@@ -42,6 +42,9 @@ import org.mockito.quality.Strictness;
 
 import java.time.Clock;
 import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -86,6 +89,14 @@ class AiRunExecutorTest {
     private static final long CONVERSATION_ID = 7L;
     private static final long RUN_ID = 11L;
     private static final String ENGINE = LlmConfigVO.ENGINE_CLAUDE_CODE;
+    /**
+     * The executor's clock, pinned so a write can be asserted against the 
exact stamp it should
+     * carry: {@code any(LocalDateTime.class)} would also pass a stamp taken 
from the database
+     * session's timezone, which is the bug this write exists to avoid.
+     */
+    private static final Instant FIXED_INSTANT = 
Instant.parse("2026-09-18T10:00:00Z");
+    private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, 
ZoneOffset.UTC);
+    private static final LocalDateTime FIXED_NOW = 
LocalDateTime.ofInstant(FIXED_INSTANT, ZoneOffset.UTC);
 
     @Mock
     private AiRunRepository runRepository;
@@ -155,7 +166,7 @@ class AiRunExecutorTest {
         executor = new AiRunExecutor(new 
AgentProviderRegistry(List.of(provider)), llmClient, promptEnhancer,
                 registry, runRepository, conversationRepository, 
eventRepository, properties(), objectMapper,
                 AiRunTestSupport.directExecutor(), 
AiRunTestSupport.recordingInto(emitters), scheduler,
-                Clock.systemUTC());
+                FIXED_CLOCK);
         conversation = AiRunTestSupport.conversation(CONVERSATION_ID, 
"tester");
         run = AiRunTestSupport.run(RUN_ID, CONVERSATION_ID, 1, 
RunStatus.QUEUED);
     }
@@ -432,7 +443,7 @@ class AiRunExecutorTest {
         assertThat(provider.calls).isEqualTo(2);
         assertThat(resumedSessions).containsExactly("gone-session", null);
         assertThat(provider.lastPrompt).isEqualTo("hello");
-        verify(conversationRepository).clearRuntimeSessionId(CONVERSATION_ID);
+        
verify(conversationRepository).clearRuntimeSessionId(eq(CONVERSATION_ID), 
eq(FIXED_NOW));
         // The retry's session replaces the dead one, and the user is told the 
context is gone.
         assertThat(runRow().getStatus()).isEqualTo(RunStatus.COMPLETED.name());
         assertThat(runRow().getRuntimeSessionId()).isEqualTo("new-session");
@@ -462,7 +473,7 @@ class AiRunExecutorTest {
         // A retry that fails must not put the dead id back: every turn after 
it would otherwise resume a
         // session that is still gone. Null is the honest state, and the next 
turn starts fresh.
         assertThat(provider.calls).isEqualTo(2);
-        verify(conversationRepository).clearRuntimeSessionId(CONVERSATION_ID);
+        
verify(conversationRepository).clearRuntimeSessionId(eq(CONVERSATION_ID), 
eq(FIXED_NOW));
         assertThat(runRow().getStatus()).isEqualTo(RunStatus.FAILED.name());
         assertThat(runRow().getRuntimeSessionId()).isNull();
         assertThat(conversation.getRuntimeSessionId()).isNull();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiTimelineRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiTimelineRepositoryTest.java
index b305858e4..67362d056 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiTimelineRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/conversation/AiTimelineRepositoryTest.java
@@ -485,7 +485,7 @@ class AiTimelineRepositoryTest {
      */
     @Test
     void forgettingTheRuntimeSessionShouldNullTheColumnByNameTest() {
-        conversations.clearRuntimeSessionId(CONVERSATION_ID);
+        conversations.clearRuntimeSessionId(CONVERSATION_ID, 
LocalDateTime.parse("2026-09-18T10:00:00"));
 
         UpdateWrapper<RmqAiConversation> update = capturedConversationUpdate();
         assertThat(update.getSqlSet()).contains("runtime_session_id");
@@ -495,9 +495,23 @@ class AiTimelineRepositoryTest {
         
assertThat(update.getParamNameValuePairs().values()).contains(CONVERSATION_ID);
     }
 
+    @Test
+    void 
forgettingTheRuntimeSessionShouldStampGmtModifiedFromTheCallerClockTest() {
+        LocalDateTime now = LocalDateTime.parse("2026-09-18T10:00:00");
+
+        conversations.clearRuntimeSessionId(CONVERSATION_ID, now);
+
+        UpdateWrapper<RmqAiConversation> update = capturedConversationUpdate();
+        // gmt_modified is ON UPDATE CURRENT_TIMESTAMP: leaving it out of the 
SET clause does not
+        // leave it alone, it hands the write to the database session's 
timezone — and the list
+        // this table feeds orders by the column as if it carried one clock.
+        assertThat(update.getSqlSet()).contains("gmt_modified");
+        assertThat(update.getParamNameValuePairs()).containsValue(now);
+    }
+
     @Test
     void forgettingTheRuntimeSessionShouldNeedAnIdTest() {
-        conversations.clearRuntimeSessionId(null);
+        conversations.clearRuntimeSessionId(null, 
LocalDateTime.parse("2026-09-18T10:00:00"));
 
         verify(conversationMapper, never()).update(any(), any());
     }

Reply via email to