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,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(