From be9dfa64b0332460547f5dba7d2cdb806afc95a7 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 23 Mar 2026 19:22:14 +0900 Subject: [PATCH] fix: deduplicate progress at runner level, not host revert Buffer intermediate assistant text in the runner. When result arrives with matching text, discard the buffered progress instead of emitting it. This prevents the duplicate from ever reaching Discord, eliminating the flash-then-revert behavior. --- runners/agent-runner/src/index.ts | 44 +++++++++++++++++++++++++------ src/message-turn-controller.ts | 12 --------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/runners/agent-runner/src/index.ts b/runners/agent-runner/src/index.ts index c4b7787..56f84bd 100644 --- a/runners/agent-runner/src/index.ts +++ b/runners/agent-runner/src/index.ts @@ -452,6 +452,7 @@ async function runQuery( let messageCount = 0; let resultCount = 0; let terminalResultObserved = false; + let pendingProgressText: string | null = null; // Discover additional directories const extraDirs: string[] = []; @@ -541,6 +542,18 @@ async function runQuery( const msgType = message.type === 'system' ? `system/${(message as { subtype?: string }).subtype}` : message.type; log(`[msg #${messageCount}] type=${msgType}`); + // Flush pending progress on any non-assistant message (ensures it's + // emitted before tool-activity or other events that depend on it). + if (message.type !== 'assistant' && pendingProgressText) { + writeOutput({ + status: 'success', + phase: 'progress', + result: pendingProgressText, + newSessionId, + }); + pendingProgressText = null; + } + if (message.type === 'system' && message.subtype === 'init') { newSessionId = message.session_id; log(`Session initialized: ${newSessionId}`); @@ -609,6 +622,14 @@ async function runQuery( resultCount++; const textResult = 'result' in message ? (message as { result?: string }).result : null; const isError = message.subtype?.startsWith('error'); + // Discard pending progress if it matches the final result (prevent duplicate) + if (pendingProgressText && textResult && pendingProgressText === textResult) { + log(`Discarding pending progress (matches result)`); + pendingProgressText = null; + } else if (pendingProgressText) { + writeOutput({ status: 'success', phase: 'progress', result: pendingProgressText, newSessionId }); + pendingProgressText = null; + } log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`); if (isError) { // Log full error details for debugging @@ -672,15 +693,22 @@ async function runQuery( stream.end(); break; } - // Intermediate assistant text between tool calls → progress + // Intermediate assistant text between tool calls → buffer as pending progress. + // Don't emit immediately — if the next message is a result with the same text, + // this would cause a duplicate. The pending text is flushed when the next + // non-result message arrives, or discarded if result matches. if (stopReason !== 'end_turn' && textResult) { - log(`Intermediate assistant text (${textResult.length} chars, stop=${stopReason})`); - writeOutput({ - status: 'success', - phase: 'progress', - result: textResult, - newSessionId, - }); + // Flush any previous pending progress first + if (pendingProgressText) { + writeOutput({ + status: 'success', + phase: 'progress', + result: pendingProgressText, + newSessionId, + }); + } + pendingProgressText = textResult; + log(`Intermediate assistant text buffered (${textResult.length} chars, stop=${stopReason})`); } } } diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index 5b715fb..5b8e8fc 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -154,18 +154,6 @@ export class MessageTurnController { // then discard the pending buffer so it never shows up. if (text) { await this.flushPendingProgress(text); - // If the displayed progress heading matches the final text, - // revert to the previous heading so it doesn't show twice. - if ( - this.latestProgressText === text && - this.previousProgressText && - this.progressMessageId && - this.options.channel.editMessage - ) { - this.latestProgressText = this.previousProgressText; - this.toolActivities = []; - await this.syncTrackedProgressMessage(); - } await this.finalizeProgressMessage(); await this.deliverFinalText(text); } else if (raw) {