Skip to content

Discord typing indicator persists forever on silent/NO_REPLY runs #27011

@drew1204

Description

@drew1204

Bug Description

When an agent run ends with a silent reply (NO_REPLY / SILENT_REPLY_TOKEN), the Discord typing indicator ("X is typing...") persists indefinitely. The triggerTyping keepalive loop never stops.

Expected Behavior

When a run ends — regardless of whether the reply is silent, NO_REPLY, or normal — the typing keepalive loop should be stopped so the typing indicator clears after Discord's natural ~10 second timeout.

Actual Behavior

The typing keepalive loop continues sending triggerTyping() every 3 seconds forever, causing a permanent "is typing..." indicator in the Discord channel.

Root Cause (from source analysis)

File: src/discord/monitor/message-handler.process.ts → compiled as pi-embedded-BDhvoWGL.js

The issue is in the interaction between createTypingController and the Discord message handler's finally block:

  1. createTypingController (line ~42567) requires BOTH markRunComplete() AND markDispatchIdle() to trigger cleanup() via maybeStopOnIdle():

    const maybeStopOnIdle = () => {
      if (!active) return;
      if (runComplete && dispatchIdle) cleanup();
    };
  2. processDiscordMessage's finally block (line ~49946) only calls markDispatchIdle():

    } finally {
      await draftStream?.stop();
      if (!finalizedViaPreviewMessage) await draftStream?.clear();
      markDispatchIdle();  // ← only this is called
      ...
    }
  3. When a run ends with NO_REPLY/silent token, markRunComplete() is never called on the typing controller, so maybeStopOnIdle() returns early (runComplete is still false), and the keepalive loop runs forever.

Suggested Fix

In the finally block of processDiscordMessage, explicitly call typing.cleanup() (or ensure markRunComplete() is always called when the run ends) so the keepalive loop stops regardless of the reply outcome:

} finally {
  await draftStream?.stop();
  if (!finalizedViaPreviewMessage) await draftStream?.clear();
  markDispatchIdle();
  typingCallbacks.onCleanup?.();  // ← add explicit cleanup
  ...
}

Alternatively, the createTypingController's markDispatchIdle could unconditionally stop the keepalive loop, since if dispatch is idle, there's no reason to keep typing.

Workaround

Setting agents.defaults.typingMode: "never" disables the typing indicator entirely, preventing the bug but losing visual feedback.

Environment

  • OpenClaw version: 2026.2.24
  • Channel: Discord (Bot API)
  • macOS, gateway via launchd LaunchAgent

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions