diff --git a/runners/agent-runner/src/index.ts b/runners/agent-runner/src/index.ts index c4c3945..427a4dd 100644 --- a/runners/agent-runner/src/index.ts +++ b/runners/agent-runner/src/index.ts @@ -558,7 +558,7 @@ async function runQuery( if (summary) { writeOutput({ status: 'success', - phase: 'tool-activity', + phase: 'progress', result: `πŸ“‹ ${summary}`, newSessionId, }); diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index 8d5e06b..1af4b31 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -1451,14 +1451,15 @@ describe('createMessageRuntime', () => { result: '컀밋은 정상 λ“€μ–΄κ°”κ³  pre-commit도 ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.', newSessionId: 'session-progress-only', }); - await vi.advanceTimersByTimeAsync(10_000); - // Third progress: flushes second (edits tracked message with new text) + // Third progress: updates tracked message heading directly await onOutput?.({ status: 'success', phase: 'progress', result: 'ν…ŒμŠ€νŠΈλ„ ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.', newSessionId: 'session-progress-only', }); + // Advance timer so the ticker fires and syncs the tracked message + await vi.advanceTimersByTimeAsync(5_000); await onOutput?.({ status: 'success', result: null, @@ -1506,17 +1507,17 @@ describe('createMessageRuntime', () => { chatJid, P('검증 μ€‘μž…λ‹ˆλ‹€.\n\n0초'), ); - // Second progress flushed when third arrives β€” edits tracked message + // Ticker fires after advanceTimersByTime β€” edits tracked message with latest heading expect(channel.editMessage).toHaveBeenCalledWith( chatJid, 'progress-1', - P('컀밋은 정상 λ“€μ–΄κ°”κ³  pre-commit도 ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.\n\n10초'), + P('ν…ŒμŠ€νŠΈλ„ ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.\n\n5초'), ); // finish() promotes the last flushed progress text to a final message expect(channel.sendMessage).toHaveBeenCalledTimes(1); expect(channel.sendMessage).toHaveBeenCalledWith( chatJid, - '컀밋은 정상 λ“€μ–΄κ°”κ³  pre-commit도 ν†΅κ³Όν–ˆμŠ΅λ‹ˆλ‹€.', + '검증 μ€‘μž…λ‹ˆλ‹€.', ); } finally { vi.useRealTimers(); @@ -1561,14 +1562,15 @@ describe('createMessageRuntime', () => { result: '아직 μ§„ν–‰ 쀑.', newSessionId: 'session-progress-recreate', }); - await vi.advanceTimersByTimeAsync(10_000); - // Third progress: flushes second (edit fails once, then succeeds on retry in syncTrackedProgressMessage) + // Third progress: updates heading directly (edit fails once on ticker, then succeeds on retry) await onOutput?.({ status: 'success', phase: 'progress', result: '거의 μ™„λ£Œ.', newSessionId: 'session-progress-recreate', }); + // Advance timer so the ticker fires and syncs (first edit fails, second succeeds) + await vi.advanceTimersByTimeAsync(5_000); await onOutput?.({ status: 'success', result: null, @@ -1627,7 +1629,7 @@ describe('createMessageRuntime', () => { expect(channel.sendMessage).toHaveBeenCalledTimes(1); expect(channel.sendMessage).toHaveBeenCalledWith( chatJid, - '아직 μ§„ν–‰ 쀑.', + 'μ§„ν–‰ μ€‘μž…λ‹ˆλ‹€.', ); } finally { vi.useRealTimers(); diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index a3bd5b0..e6dddbc 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -29,6 +29,7 @@ export class MessageTurnController { private previousProgressText: string | null = null; private pendingProgressText: string | null = null; private toolActivities: string[] = []; + private progressCreating = false; private latestProgressRendered: string | null = null; private progressMessageId: string | null = null; private progressStartedAt: number | null = null; @@ -105,9 +106,18 @@ export class MessageTurnController { } if (result.phase === 'tool-activity') { - // Flush pending progress so the message exists for tool activity sub-lines - if (this.pendingProgressText && !this.progressMessageId) { - void this.sendProgressMessage(this.pendingProgressText); + // Ensure a progress message exists for tool activity sub-lines + if (!this.progressMessageId && !this.progressCreating) { + this.progressCreating = true; + const heading = this.pendingProgressText || 'μž‘μ—… 쀑...'; + this.sendProgressMessage(heading).then(() => { + this.progressCreating = false; + this.ensureProgressTicker(); + // Replay any queued tool activities now that the message exists + if (this.toolActivities.length > 0 && this.progressMessageId) { + void this.syncTrackedProgressMessage(); + } + }); this.pendingProgressText = null; } if (text) { @@ -121,7 +131,14 @@ export class MessageTurnController { if (result.phase === 'progress') { if (text) { - this.bufferProgress(text); + if (this.progressMessageId) { + // Progress message already visible β€” update heading directly + this.latestProgressText = text; + this.toolActivities = []; + void this.syncTrackedProgressMessage(); + } else { + this.bufferProgress(text); + } } if (!this.poisonedSessionDetected) { this.resetIdleTimer(); @@ -223,7 +240,7 @@ export class MessageTurnController { const elapsedSeconds = this.progressStartedAt === null ? 0 - : Math.floor((Date.now() - this.progressStartedAt) / 10_000) * 10; + : Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5; const hours = Math.floor(elapsedSeconds / 3600); const minutes = Math.floor((elapsedSeconds % 3600) / 60); const seconds = elapsedSeconds % 60; @@ -241,9 +258,7 @@ export class MessageTurnController { const isLast = i === this.toolActivities.length - 1; const connector = isLast ? 'β””' : 'β”œ'; const isSummary = a.startsWith('πŸ“‹'); - return isSummary - ? `${connector} ${a}` - : `${connector} ${a}`; + return isSummary ? `${connector} ${a}` : `${connector} ${a}`; }) .join('\n') : ''; @@ -267,6 +282,7 @@ export class MessageTurnController { private resetProgressState(): void { this.clearProgressTicker(); this.pendingProgressText = null; + this.progressCreating = false; this.toolActivities = []; this.latestProgressText = null; this.previousProgressText = null; @@ -299,11 +315,9 @@ export class MessageTurnController { if (this.toolActivities.length > MAX_ACTIVITIES) { this.toolActivities = this.toolActivities.slice(-MAX_ACTIVITIES); } - // Update the displayed progress message with tool activity sub-lines - if (this.latestProgressText && this.progressMessageId) { - void this.syncTrackedProgressMessage(); - this.ensureProgressTicker(); - } + // Don't sync here β€” let the ticker handle periodic updates + // to avoid flooding Discord with edits. + this.ensureProgressTicker(); } /** @@ -327,9 +341,6 @@ export class MessageTurnController { } const rendered = this.renderProgressMessage(this.latestProgressText); - if (rendered === this.latestProgressRendered) { - return; - } try { await this.options.channel.editMessage( @@ -361,17 +372,15 @@ export class MessageTurnController { } private ensureProgressTicker(): void { - if ( - !this.progressMessageId || - !this.options.channel.editMessage || - this.progressTicker - ) { + if (this.progressTicker || !this.options.channel.editMessage) { return; } this.progressTicker = setInterval(() => { - void this.syncTrackedProgressMessage(); - }, 10_000); + if (this.progressMessageId && this.latestProgressText && !this.progressCreating) { + void this.syncTrackedProgressMessage(); + } + }, 5_000); } private async finalizeProgressMessage(): Promise {