Skip to content

feat(telegram): queue messages sent mid-run instead of rejecting them - #238

Closed
tamircher wants to merge 2 commits into
moazbuilds:masterfrom
tamircher:feat/telegram-message-queue
Closed

feat(telegram): queue messages sent mid-run instead of rejecting them#238
tamircher wants to merge 2 commits into
moazbuilds:masterfrom
tamircher:feat/telegram-message-queue

Conversation

@tamircher

Copy link
Copy Markdown

Stacked on #237 — review the last commit only; I'll rebase once #237 lands.

A message arriving while the same topic (or the global session) is mid-run is rejected with a busy notice, forcing the user to resend. Since run() already serializes per thread via enqueue(), the message can simply be submitted — it executes next, in order, with full session context via --resume, like Claude Code's message queue.

While queued, the bot reacts 👀 to the incoming message as a lightweight received-and-waiting ack. The typing indicator already runs while queued, and the stream preview message is only created on the first output chunk, so nothing else changes visibly until the queued run starts.

Changes

  • src/commands/telegram.ts: drop the busy rejection; submit to the per-thread queue and 👀-react while waiting

🤖 Generated with Claude Code

tamircher added 2 commits June 6, 2026 01:01
Each Telegram topic already gets its own session and the runner keeps
independent per-thread serial queues (threadQueues), but the handler
gated every incoming message on the global isMainBusy() — any in-flight
run anywhere blocked all topics.

Add per-thread busy tracking in the runner (isThreadBusy/isGlobalBusy)
and check only the target topic's queue (or the global session for
non-topic chats), so each topic is a fully independent session running
in parallel. A second message to the same topic still gets the polite
busy reply.
A message arriving while the same topic (or the global session) is
mid-run was rejected with a busy notice, forcing the user to resend.
The runner already serializes runs per thread via enqueue(), so the
message can simply be submitted and execute next, in order, with full
session context via --resume — like Claude Code's message queue.

While queued, the bot reacts 👀 to the incoming message as a lightweight
received-and-waiting ack. The typing indicator already runs while
queued, and the stream preview message is only created on first output
chunk, so nothing else changes visibly until the queued run starts.

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Nice swap from reject to queue — session context is correctly preserved through enqueue → execClaude → --resume (the 0f8a351 rationale for the rejection was the auto-fork path, not queueing, so this isn't a regression).

Two correctness concerns worth flagging before merge, plus one note:

  1. Stale clock prefix on queued messages. runUserMessage builds the [YYYY-MM-DD HH:MM:SS UTC±X] prefix synchronously at call time, then enqueues the closure. A message that sits behind a 5-minute run is handed to Claude with a 5-minute-stale "now". Time-aware prompts ("what time is it?", "schedule in 30 min", "is it past my bedtime?") will silently get wrong answers. Pre-PR the reject-and-retry pattern always rebuilt a fresh prefix on the user's resend.

    claudeclaw/src/runner.ts

    Lines 1697 to 1700 in 37c95e0

    modelOverride?: string
    ): Promise<RunResult> {
    return run(name, prefixUserMessageWithClock(prompt), threadId, modelOverride, undefined, agentName, undefined, onChunk, onToolEvent);
    }

    Fix: move prefixUserMessageWithClock(prompt) inside the enqueued closure so it stamps at execution time instead of arrival time.

  2. /kill no longer means "stop everything". killActive() only SIGTERMs entries in mainActiveProcs, which are the currently-running subprocesses. Queued messages aren't in there yet — they're closures chained on threadQueues. So /kill after spamming a topic kills the in-flight run, then the queue happily fires each queued message one-by-one, spinning up a new subprocess for each. User intent is silently broken. This is distinct from the cross-topic kill issue on #237.

    claudeclaw/src/runner.ts

    Lines 283 to 295 in 37c95e0

    // outside the main queue and must not be killed by /kill.
    const mainActiveProcs = new Set<ReturnType<typeof Bun.spawn>>();
    /** Kill all running main-queue claude subprocesses. Returns true if anything was killed. */
    export function killActive(): boolean {
    if (mainActiveProcs.size === 0) return false;
    for (const proc of mainActiveProcs) {
    try { proc.kill(); } catch {}
    }
    mainActiveProcs.clear();
    return true;
    }

    Fix: when /kill fires, also drain or replace the matching threadQueues entry with Promise.resolve() so pending closures are short-circuited.

Note (not blocking): the per-thread queue has no length cap. For a personal bot gated by isAllowed this is fine; worth a follow-up if you ever broaden access.

Also: the runner.ts comment block introduced in #237 still says "while still rejecting a second message to the SAME thread" — stale after this PR:

claudeclaw/src/runner.ts

Lines 299 to 308 in 37c95e0

}
// Busy state per thread queue + global queue, so handlers can allow
// parallel runs across different topics/threads while still rejecting
// a second message to the SAME thread (or the global session).
const busyThreads = new Set<string>();
let busyGlobalCount = 0;
/** True while THIS thread's queue is processing a task. */
export function isThreadBusy(threadId: string): boolean {

Once #237 lands and you rebase, please run bun run bump:plugin-version + bun run bump:marketplace-version (will need the next-after-237's bump).

@TerrysPOV

Copy link
Copy Markdown
Collaborator

Closing in favour of #250, which implements the same fix — removing the busy-rejection so mid-run Telegram messages queue instead of being dropped — as a simpler standalone change that doesn't depend on the #237 stack. Consolidating the two competing PRs onto one.

Thanks for this, @tamircher — the approach was right, and #250 carries it forward. The per-thread parallel-topics work in #237 is a separate feature and stays open on its own track.

The same open items from the earlier review still apply and have been raised on #250 (queue backpressure, /kill not cancelling queued messages, stale clock prefix), so nothing is lost by consolidating here.

@TerrysPOV TerrysPOV closed this Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants