-
-
Notifications
You must be signed in to change notification settings - Fork 52.6k
Description
Telegram "typing..." indicator persists indefinitely after agent run completes
Description
The Telegram bot shows "typing..." indefinitely after certain agent interactions. The indicator never clears until the gateway is restarted. This happens because the typing keepalive loop can restart itself after the agent run completes.
Environment
- OpenClaw 2026.2.24
- Telegram channel with
streaming: true - Model: Qwen3.5-122B-A10B via openai-completions API (llama.cpp backend), but likely affects all models
Steps to Reproduce
- Send a message to the bot via Telegram that triggers tool use (e.g., "run
whoami") - Wait for the bot to respond
- Observe the "typing..." indicator — it may persist indefinitely after the response is delivered
The issue is intermittent and more likely to occur when:
- The agent run involves multiple tool-call turns (longer runs)
- There are LLM errors during the run (context exceeded, timeouts, 502/503)
- The slug-generator times out concurrently
Root Cause
Two issues in src/auto-reply/reply/typing.ts and the Telegram channel handler:
1. triggerTyping() does not check runComplete
In createTypingController, the triggerTyping function only checks sealed:
const triggerTyping = async () => {
if (sealed) return;
await onReplyStart?.();
};The typingLoop (6-second interval) calls triggerTyping, which calls onReplyStart from createTypingCallbacks. That onReplyStart restarts the keepalive loop:
const onReplyStart = async () => {
stopSent = false;
keepaliveLoop.stop();
await fireStart(); // sends sendChatAction("typing")
keepaliveLoop.start(); // restarts the 3-second keepalive
};The cleanup lifecycle requires both runComplete AND dispatchIdle to call cleanup() (which sets sealed = true). Between markRunComplete() and the dispatch becoming idle, the typingLoop can fire and restart the keepalive loop via onReplyStart. If cleanup() is delayed or never fires (e.g., due to a stuck message send), the keepalive loop runs forever.
2. Telegram createTypingCallbacks has no stop callback
Unlike the Slack implementation which has an explicit stop callback to clear the status:
// Slack (has stop)
const typingCallbacks = createTypingCallbacks({
start: async () => { await ctx.setSlackThreadStatus({ status: "is typing..." }); },
stop: async () => { await ctx.setSlackThreadStatus({ status: "" }); }, // clears typing
...
});
// Telegram (missing stop)
const typingCallbacks = createTypingCallbacks({
start: sendTyping,
// no stop callback
...
});In fireStop(), when stop is undefined, no cleanup action is taken:
const fireStop = () => {
keepaliveLoop.stop();
if (!stop || stopSent) return; // returns early for Telegram
stopSent = true;
stop().catch(...);
};Suggested Fix
Fix 1: Check runComplete in triggerTyping()
const triggerTyping = async () => {
if (sealed) return;
if (runComplete) return; // prevent restart after run completes
await onReplyStart?.();
};This is the critical fix — it prevents the typing loop from restarting the keepalive after the agent run finishes.
Fix 2: Add stop callback for Telegram
const typingCallbacks = createTypingCallbacks({
start: sendTyping,
stop: async () => {
try {
await bot.api.sendChatAction(chatId, "cancel", buildTypingThreadParams(replyThreadId));
} catch {}
},
...
});This explicitly cancels the typing indicator on cleanup, matching Slack's behavior.
Affected Files
The typing lifecycle spans multiple dist chunks. The active file for
Telegram message handling is reply-*.js (e.g., reply-Cx57rl6c.js), not
pi-embedded-*.js. Both files contain copies of createTypingController and
createTypingCallbacks. The fix must be applied to whichever chunk is loaded
at runtime for the Telegram channel handler.
Workaround
Restarting the gateway (systemctl --user restart openclaw-gateway) clears the stuck typing state. The patches above can be applied manually to the dist file (reply-*.js) but will be overwritten by updates.