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 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-03-25 11:18:54 +09:00
parent 83aedba7f3
commit 03b9480614

View File

@@ -345,6 +345,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
await turnController.start(); await turnController.start();
try {
const output = await runAgent(group, prompt, chatJid, runId, (result) => const output = await runAgent(group, prompt, chatJid, runId, (result) =>
turnController.handleOutput(result), turnController.handleOutput(result),
); );
@@ -372,6 +373,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
); );
return true; 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);
}
} }
}; };
@@ -523,7 +530,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
endSeq, endSeq,
); );
} }
channel await channel
.setTyping?.(chatJid, true) .setTyping?.(chatJid, true)
?.catch((err) => ?.catch((err) =>
logger.warn( logger.warn(