From 77c761bcc14e81de90b301b9b447464b74742292 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 7 May 2026 19:34:19 +0800 Subject: [PATCH] fix: dedupe assistant chat messages --- ...pp_controller_desktop_thread_sessions.dart | 17 ++++- .../assistant_execution_target_test.dart | 67 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index f87fadb2..8cc8e7de 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -489,7 +489,22 @@ extension AppControllerDesktopThreadSessions on AppController { ), ); } - return items; + return dedupeGatewayChatMessagesByIdThreadSessionInternal(items); + } + + List dedupeGatewayChatMessagesByIdThreadSessionInternal( + List messages, + ) { + final seenIds = {}; + final deduped = []; + for (final message in messages) { + final id = message.id.trim(); + if (id.isNotEmpty && !seenIds.add(id)) { + continue; + } + deduped.add(message); + } + return deduped; } String normalizedAssistantSessionKeyInternal(String sessionKey) { diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index 777fbfe7..3ebf5dee 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -596,6 +596,73 @@ void main() { ); }, ); + + test( + 'chatMessages does not duplicate persisted local turn messages', + () async { + final controller = AppController( + environmentOverride: const {}, + ); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession('session-1'); + + final userMessage = GatewayChatMessage( + id: 'local-user-1', + role: 'user', + text: 'hi', + timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + toolCallId: null, + toolName: null, + stopReason: null, + pending: false, + error: false, + ); + final assistantMessage = GatewayChatMessage( + id: 'local-assistant-1', + role: 'assistant', + text: 'Bridge response', + timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + toolCallId: null, + toolName: null, + stopReason: null, + pending: false, + error: false, + ); + + controller.appendLocalSessionMessageInternal( + 'session-1', + userMessage, + persistInThreadContext: true, + ); + controller.appendLocalSessionMessageInternal( + 'session-1', + assistantMessage, + persistInThreadContext: true, + ); + controller.assistantThreadMessagesInternal['session-1'] = + List.from( + controller + .requireTaskThreadForSessionInternal('session-1') + .messages, + ); + + final visibleMessages = controller.chatMessages; + + expect( + visibleMessages.where((message) => message.id == userMessage.id), + hasLength(1), + ); + expect( + visibleMessages.where((message) => message.id == assistantMessage.id), + hasLength(1), + ); + expect( + visibleMessages.map((message) => message.text), + containsAllInOrder([userMessage.text, assistantMessage.text]), + ); + }, + ); }); }