fix: eliminate duplicate text and tool calls in workflow execution view

Three fixes for message duplication during live workflow execution:

1. dag-executor: Add missing `tool_call_formatted` category to loop iteration
   tool messages. Without this, the web adapter sent tool text as both a regular
   SSE text event AND a structured tool_call event, causing each tool to appear
   twice (raw text + rendered card). Regular DAG nodes already had this metadata.

2. WorkflowLogs: Add text content dedup in SSE/DB merge. During live execution,
   the same text (e.g. "Starting workflow...") can appear in both DB (REST fetch)
   and SSE (event buffer replay). Collects DB text into a Set and skips matching
   SSE text messages.

3. orchestrator-agent: Suppress remainingMessage re-send in stream mode. The
   routing AI streams text chunks before /invoke-workflow is detected, then
   retracts them. Without suppression, remainingMessage re-sends the same text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cole Medin 2026-04-10 15:48:40 -05:00
parent 968dfadcf5
commit 4e56c86dff
3 changed files with 32 additions and 2 deletions

View file

@ -898,13 +898,20 @@ async function handleStreamMode(
if (platform.emitRetract) {
await platform.emitRetract(conversationId);
}
// In stream mode, pre-command text was already sent chunk-by-chunk
// and then retracted. Suppress remainingMessage to avoid re-sending
// it (which causes duplicate text in the chat).
const streamSafeInvocation = {
...commands.workflowInvocation,
remainingMessage: '',
};
await handleWorkflowInvocationResult(
platform,
conversationId,
conversation,
codebases,
workflows,
commands.workflowInvocation,
streamSafeInvocation,
originalMessage,
isolationHints,
issueContext

View file

@ -388,10 +388,31 @@ export function WorkflowLogs({
filteredDbMessages = dbMessages;
}
// Collect DB text content for dedup against SSE text messages.
// During live execution, the same text (e.g., "🚀 Starting workflow...") can appear
// in both DB (from REST fetch on mount) and SSE (from event buffer replay).
// Without dedup, the text shows up twice in the message list.
const dbTextContents = new Set<string>();
for (const dm of filteredDbMessages) {
if (dm.role === 'assistant' && dm.content) {
dbTextContents.add(dm.content);
}
}
// Strip SSE tool calls that already appear in DB messages (completed).
// Also strip SSE text messages that are already in DB (prevents duplicate text).
const dedupedSse: ChatMessage[] = [];
for (const m of sseMessages) {
if (!m.toolCalls?.length) {
// Skip SSE text-only messages whose content already exists in DB.
if (m.content && dbTextContents.has(m.content)) {
continue;
}
// Also skip if DB has a message that starts with the SSE content
// (SSE text was flushed to DB before SSE finished accumulating).
if (m.content && [...dbTextContents].some(dc => dc.startsWith(m.content))) {
continue;
}
if (m.isStreaming || m.content) dedupedSse.push(m);
continue;
}

View file

@ -1915,7 +1915,9 @@ async function executeLoopNode(
if (platform.getStreamingMode() === 'stream') {
const toolMsg = formatToolCall(msg.toolName, msg.toolInput);
if (toolMsg) {
await safeSendMessage(platform, conversationId, toolMsg, msgContext);
await safeSendMessage(platform, conversationId, toolMsg, msgContext, {
category: 'tool_call_formatted',
} as WorkflowMessageMetadata);
}
if (platform.sendStructuredEvent) {
await platform.sendStructuredEvent(conversationId, msg);