From 03b9480614b585f1f6dd817592fbe23b970473ad Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Wed, 25 Mar 2026 11:18:54 +0900 Subject: [PATCH] fix: prevent typing indicator leak on agent run exceptions and follow-up race Two leak paths caused Discord typing indicators to persist indefinitely: 1. If runAgent() threw before turnController.finish(), setTyping(false) was never called. Wrap the agent run in try/finally to guarantee cleanup. 2. Follow-up message piping called setTyping(true) fire-and-forget. If the Promise resolved after finish()'s setTyping(false), the typing interval was recreated with nobody to clear it. Await the call to serialize the on/off sequence. Co-Authored-By: Claude Opus 4.6 --- src/message-runtime.ts | 59 +++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/message-runtime.ts b/src/message-runtime.ts index 1b55897..a487bec 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -345,33 +345,40 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { await turnController.start(); - const output = await runAgent(group, prompt, chatJid, runId, (result) => - turnController.handleOutput(result), - ); - - const { deliverySucceeded, visiblePhase } = - await turnController.finish(output); - - if (!deliverySucceeded) { - logger.warn( - { chatJid, group: group.name, groupFolder: group.folder, runId }, - 'Persisted produced output for delivery retry without rerunning agent', + try { + const output = await runAgent(group, prompt, chatJid, runId, (result) => + turnController.handleOutput(result), ); - return false; + + const { deliverySucceeded, visiblePhase } = + await turnController.finish(output); + + if (!deliverySucceeded) { + logger.warn( + { chatJid, group: group.name, groupFolder: group.folder, runId }, + 'Persisted produced output for delivery retry without rerunning agent', + ); + return false; + } + + logger.info( + { + chatJid, + group: group.name, + groupFolder: group.folder, + runId, + visiblePhase, + }, + 'Queued run completed successfully', + ); + + return true; + } finally { + // Safety net: always clear typing even if runAgent() or finish() throws. + // Prevents stuck typing indicators when exceptions bypass the normal + // turnController.finish() → setTyping(false) path. + await channel.setTyping?.(chatJid, false); } - - logger.info( - { - chatJid, - group: group.name, - groupFolder: group.folder, - runId, - visiblePhase, - }, - 'Queued run completed successfully', - ); - - return true; } }; @@ -523,7 +530,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { endSeq, ); } - channel + await channel .setTyping?.(chatJid, true) ?.catch((err) => logger.warn(