From 562067352a553f2103d5dbcb8af01ca4f0da4e82 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 23 Mar 2026 03:54:19 +0900 Subject: [PATCH] fix: keep finals separate after silent retries --- src/message-runtime.test.ts | 28 +++++++----- src/message-runtime.ts | 88 ++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index c7776b2..3a05f14 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -697,7 +697,10 @@ describe('createMessageRuntime', () => { '중복 최종 답변입니다.', ); expect(channel.sendMessage).toHaveBeenCalledTimes(1); - expect(channel.sendMessage).toHaveBeenCalledWith(chatJid, '최종 답변입니다.'); + expect(channel.sendMessage).toHaveBeenCalledWith( + chatJid, + '최종 답변입니다.', + ); } finally { vi.useRealTimers(); } @@ -1537,7 +1540,10 @@ describe('createMessageRuntime', () => { '아직 진행 중.\n\n10초', ); expect(channel.sendMessage).toHaveBeenCalledTimes(1); - expect(channel.sendMessage).toHaveBeenCalledWith(chatJid, '아직 진행 중.'); + expect(channel.sendMessage).toHaveBeenCalledWith( + chatJid, + '아직 진행 중.', + ); } finally { vi.useRealTimers(); } @@ -1564,16 +1570,14 @@ describe('createMessageRuntime', () => { }, ]); - vi.mocked(agentRunner.runAgentProcess).mockImplementation( - async () => { - await vi.advanceTimersByTimeAsync(1_100); - return { - status: 'success', - result: null, - newSessionId: 'session-quiet-budget', - }; - }, - ); + vi.mocked(agentRunner.runAgentProcess).mockImplementation(async () => { + await vi.advanceTimersByTimeAsync(1_100); + return { + status: 'success', + result: null, + newSessionId: 'session-quiet-budget', + }; + }); const runtime = createMessageRuntime({ assistantName: 'Andy', diff --git a/src/message-runtime.ts b/src/message-runtime.ts index 1e37093..3f7a7ce 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -569,7 +569,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { const MAX_SILENT_ROLLOVERS = 2; const MAX_TOTAL_SILENT_WAIT_MS = QUIET_RUN_ROLLOVER_MS * (MAX_SILENT_ROLLOVERS + 1); - const FAILURE_FINAL_TEXT = '요청을 완료하지 못했습니다. 다시 시도해 주세요.'; + const FAILURE_FINAL_TEXT = + '요청을 완료하지 못했습니다. 다시 시도해 주세요.'; const queueItemBaseCursor = normalizeStoredSeqCursor( deps.getLastAgentTimestamps()[chatJid] || '0', chatJid, @@ -630,11 +631,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { !requiresTrigger || (hasTrigger && (msg.is_from_me || - isTriggerAllowed( - chatJid, - msg.sender, - loadSenderAllowlist(), - ))) + isTriggerAllowed(chatJid, msg.sender, loadSenderAllowlist()))) ); }, }, @@ -680,13 +677,11 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { ); type VisiblePhase = 'silent' | 'progress' | 'final'; - type PendingFollowUp = - | { - queuedAt: number; - textLength: number; - filename: string; - } - | null; + type PendingFollowUp = { + queuedAt: number; + textLength: number; + filename: string; + } | null; let idleTimer: ReturnType | null = null; let hadError = false; let producedDeliverySucceeded = true; @@ -934,38 +929,41 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { idleTimer = null; return; } - idleTimer = setTimeout(() => { - const closeReason = - !hasVisibleOutput() && pendingFollowUp - ? 'follow-up-no-output-preemption' - : 'idle-timeout'; - quietStopReason ??= - !hasVisibleOutput() && pendingFollowUp - ? 'follow-up-no-output-timeout' - : 'idle-timeout'; - if (!hasVisibleOutput() && pendingFollowUp) { - logger.warn( - { - group: group.name, - chatJid, - runId, - followUpQueuedAt: new Date( - pendingFollowUp.queuedAt, - ).toISOString(), - followUpQueuedTextLength: pendingFollowUp.textLength, - followUpQueuedFilename: pendingFollowUp.filename, - followUpWaitMs: Date.now() - pendingFollowUp.queuedAt, - }, - 'Idle timeout reached while a queued follow-up still had no visible output', - ); - } else { - logger.debug( - { group: group.name, chatJid, runId, closeReason }, - 'Idle timeout, closing agent stdin', - ); - } - deps.queue.closeStdin(chatJid, { runId, reason: closeReason }); - }, hasVisibleOutput() ? deps.idleTimeout : QUIET_RUN_ROLLOVER_MS); + idleTimer = setTimeout( + () => { + const closeReason = + !hasVisibleOutput() && pendingFollowUp + ? 'follow-up-no-output-preemption' + : 'idle-timeout'; + quietStopReason ??= + !hasVisibleOutput() && pendingFollowUp + ? 'follow-up-no-output-timeout' + : 'idle-timeout'; + if (!hasVisibleOutput() && pendingFollowUp) { + logger.warn( + { + group: group.name, + chatJid, + runId, + followUpQueuedAt: new Date( + pendingFollowUp.queuedAt, + ).toISOString(), + followUpQueuedTextLength: pendingFollowUp.textLength, + followUpQueuedFilename: pendingFollowUp.filename, + followUpWaitMs: Date.now() - pendingFollowUp.queuedAt, + }, + 'Idle timeout reached while a queued follow-up still had no visible output', + ); + } else { + logger.debug( + { group: group.name, chatJid, runId, closeReason }, + 'Idle timeout, closing agent stdin', + ); + } + deps.queue.closeStdin(chatJid, { runId, reason: closeReason }); + }, + hasVisibleOutput() ? deps.idleTimeout : QUIET_RUN_ROLLOVER_MS, + ); }; const noteFollowUpQueued = (meta?: {