fix(telegram): queue messages received while Claude is busy instead of dropping them - #250
Conversation
…f dropping them When a Telegram message arrived while a main-queue run was in flight, handleMessage() replied "Claude is busy — try again in a moment" and returned early, silently discarding the message. Users had to notice the reply and manually resend. The runner already serializes work per-thread: runUserMessage() routes through enqueue(), keyed by sessionKey, so messages from the same chat run in order. The busy branch was short-circuiting before ever reaching that queue. Remove the early return so a busy-time message falls through into the existing per-thread queue and runs after the current task finishes. The user still gets an immediate heads-up that their message is queued (and a pointer to /fork and /kill), and the typing indicator already stays alive across the queued wait, so the chat no longer looks frozen. No new concurrency: per-thread serialization is unchanged; this only stops dropping the enqueued work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TerrysPOV
left a comment
There was a problem hiding this comment.
Overlapping PRs: #238 (feat/telegram-message-queue) implements the same outcome — removing the Telegram busy-rejection so mid-run messages queue instead of being dropped. #238 is stacked on #237 and is currently CHANGES_REQUESTED; this PR is a simpler standalone version of the same fix (it just removes the early return and relies on the existing per-thread enqueue, without #237's per-thread busy tracking). The two are mutually exclusive — one should be picked and the other closed.
Vision: ALIGNED — silently dropping a user's message is a real defect, and queueing it through the existing per-thread serialization (with a heads-up + live typing indicator) is the right fix.
Code review
The mechanical change is clean: after removing the early return, the downstream reads (result, streamMsgId, hadToolLines) are all outer-scoped and unconditionally assigned on the queued path, so there's no fall-through bug.
Three things carry over from the same concerns raised on #238, plus the version:
- No backpressure on the per-thread queue. Removing the busy-drop removes the only bound on queued work — a chat that sends N messages while Claude is busy now enqueues N full subprocess runs with no cap, dedup, or coalescing. That cuts against the "lightweight / runs on low-spec machines" goal. Worth a queue-depth cap (or an explicit decision to accept it).
claudeclaw/src/commands/telegram.ts
Lines 1454 to 1457 in 3c6a730
-
/killdoesn't cancel a queued message, but the new busy notice points the user to it.killActive()only terminates the in-flight subprocess; a message already queued behind it still runs afterward (the queue chains via.then(fn, fn)). So a user who reads "use /kill to cancel what's running" and hits /kill will still see their queued message execute. Either the wording should clarify it only cancels the current run, or /kill should also drain the pending thread-queue entry. -
Stale clock prefix on queued messages.
runUserMessagebuilds the timestamp prefix (prefixUserMessageWithClock) at arrival time, before the queue wait, so a message queued behind a long run reaches Claude with a stale "now". Minor, but same defect as #238.
Also: the version bump is stale — this targets 1.0.41 but master is 1.0.43, which is the only thing making the PR conflict (the telegram.ts change merges clean). Rebase and re-bump both .claude-plugin/*.json to 1.0.44.
Minor: the "queued and will run after the current one finishes" copy is only accurate when the in-flight run is on the same session — the busy check (isMainBusy) is global while the queue is per-thread, so a message for a different chat/thread actually runs immediately in parallel, not "after".
Problem
When a Telegram message arrives while a main-queue run is already in flight,
handleMessage()replies "Claude is busy — try again in a moment" andreturns early — silently dropping the message. The user has to notice the reply and manually resend, and any message they'd already typed is lost.Why the queue was already there
The runner already serializes work per thread:
runUserMessage()routes throughenqueue()insrc/runner.ts, keyed bysessionKey, so messages from the same chat run in order:The
busybranch was short-circuiting before ever reaching that queue.Fix
Remove the early
returnso a busy-time message falls through into the existing per-thread queue and runs after the current task finishes./forkand/kill), so nothing looks silently swallowed.setInterval(... sendTyping ...), "Keep typing indicator alive while queued/running") and stays alive across the queued wait, so the chat no longer looks frozen.Discord (
src/commands/discord.ts) has no such busy-check and already flows straight into the queue, so this brings Telegram in line with Discord's behavior.Testing
bunx tsc --noEmitpasses clean.bun test: the only failures are pre-existingsessionFiles/sessionstests that assume POSIX path formats and fail on a Windows dev box — unrelated to this change (there are no Telegram tests, and this diff only touchestelegram.ts).Version
Bumped
.claude-plugin/plugin.jsonand.claude-plugin/marketplace.jsonto1.0.41per CONTRIBUTING (shippedsrc/change).