Skip to content

Discord typing indicator persists after agent run completes #27926

@kengwei

Description

@kengwei

Bug Summary

Discord's "typing" indicator continues showing after an agent has completed its reply and posted the final message. In some cases, the typing indicator persists indefinitely — well beyond the 2-minute TTL failsafe — giving users the false impression the agent is still working when no tokens are being consumed.

Steps to Reproduce

  1. Send a message to any agent via Discord
  2. Agent processes the request, streams/posts its reply
  3. After the final message is posted, observe that the "typing..." indicator continues showing in the Discord channel
  4. No tokens are being burned (confirmed via openclaw status — session shows stale "last active" timestamp)

Expected Behavior

The typing indicator should stop immediately (or within a few seconds) after the agent posts its final reply.

Actual Behavior

  • Short case: Typing persists for up to 2 minutes after completion (the hard-coded TTL in typing-lifecycle.ts)
  • Severe case: Typing persists indefinitely, surviving past the 2-minute TTL. Observed with agent "friday" — typing showed continuously with zero token burn, session file untouched for 15+ minutes. Required openclaw gateway restart to clear.

Root Cause Analysis

Traced through the bundled source (reply-Cx57rl6c.js). The typing system has a three-layer architecture:

1. Race condition in createTypingController (source: src/auto-reply/reply/typing.ts)

Typing cleanup requires two flags to both be true:

const maybeStopOnIdle = () => {
    if (!active) return;
    if (runComplete && dispatchIdle) cleanup(); // BOTH must be true
};
  • markRunComplete() is called in runPreparedReply's finally block
  • markDispatchIdle() is called in the Discord message handler's finally block

These are separate code paths that don't fire atomically. When the agent run ends but the dispatcher hasn't flushed all outbound messages, the setInterval keepalive loop (every 6 seconds) continues calling channel.triggerTyping().

2. No explicit Discord "stop typing" API

Discord's typing indicator expires after ~10 seconds naturally, but the keepalive loop refreshes it every 6 seconds (typingIntervalSeconds default). The fireStop() method in createTypingCallbacks checks for a stop callback, but the Discord channel adapter doesn't provide one (unlike e.g. Telegram), so the only way to stop is clearing the setInterval.

3. TTL failsafe may not always fire

The 2-minute TTL (typingTtlMs = 2 * 6e4) sets a setTimeout for unconditional cleanup. However, in the severe case observed, typing persisted well beyond 2 minutes. Hypothesis: Discord WebSocket disconnects/resumes (observed every ~15-20 minutes in gateway logs, codes 1005/1006) may re-trigger or re-initialize typing state for sessions that had pending typing when the connection dropped.

Environment

  • OpenClaw: 2026.2.24 (stable)
  • Node: v24.13.0 (nvm)
  • Platform: macOS 26.3 (arm64)
  • Channel: Discord
  • Gateway: LaunchAgent, local loopback

Suggested Fixes

  1. Decouple cleanup from dual-flag gate: Stop the typing keepalive as soon as the final message is posted, rather than waiting for both runComplete and dispatchIdle
  2. Clear typing state on Discord resume: When the Discord WebSocket reconnects after a 1005/1006 close, ensure no stale typing loops survive from the previous connection
  3. Make TTL configurable: The 2-minute TTL is hard-coded; exposing it in config would let users reduce it
  4. Add explicit cleanup on session idle: If no model call has been made for N seconds but the typing loop is still running, force-stop it

Workaround

openclaw gateway restart  # clears all stuck typing state

Or configure typingMode: "message" (or "never") in openclaw.json to reduce/eliminate the issue:

{
  agents: {
    defaults: {
      typingMode: "message"  // only shows typing when text starts streaming
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions