Usage

Overlap

When three messages land while the bot is still answering the first, the rule names the policy. The runner owns the queue. Ingress (acknowledge the platform webhook) is separate and always happens first.

Name it on the handler

@cx.on_message({"channel": "telegram", "overlap": "queue", "bound": 16})
def handle(thread, msg, ctx):
    thread.post(tutor(msg, ctx.skipped))

@cx.on_action({"channel": "telegram", "data": "done", "overlap": "drop"})
def on_done(thread, action, ctx):
    thread.edit(action.message_id, "done.")
cx.onMessage(
  { channel: "telegram", overlap: "queue", bound: 16 },
  async (thread, msg, { skipped }) => {
    await thread.post(await tutor.run(msg, skipped))
  },
)

cx.onAction(
  { channel: "telegram", data: "done", overlap: "drop" },
  async (thread, action) => {
    await thread.edit(action.message_id, "done.")
  },
)

Omitting overlap does not mean “no policy.” Messages default to queue with bound 16. TypeScript onAction defaults to drop. Python on_action currently defaults to queue — pass overlap: "drop" on buttons and voice so a tap never waits behind a paragraph.

Policies

The core is a small state machine per overlap key: idle or busy, a queued count, and a skipped count. A new event is execute, enqueue, or drop. When a handler finishes, queue and debounce may drain the next turn.

PolicyWhile idleWhile busyUse
queue Run it Hold it. If the queue is already at bound, drop it. Default for text. The agent should see the burst, not interleave replies.
debounce Run it Keep only the latest (queued stays 1). Earlier waiting events are collapsed. Typing bursts, “wait until they stop sending.”
drop Run it Discard the new event. No queue. Buttons, voice, typing indicators. Ack now.
parallel Run it Run it anyway. No slot is taken. Independent work on the same key (a story beat, a side job).
stream Run it Same as parallel: always execute. Python only. TypeScript has no stream policy yet — use parallel for the same “don’t wait” behavior (see the Telegram example).

bound is the max queued count for queue (default 16). It does not apply to drop, parallel, or stream. Debounce never grows past 1.

When to pick which

Same options in both SDKs. The Telegram, Discord, and Messenger examples use drop on typing so a “still thinking” indicator never queues behind a long reply.

# queue — default for text. Hold extras; ctx.skipped is the burst.
@cx.on_message({"channel": "telegram", "overlap": "queue"})
def on_text(thread, msg, ctx):
    thread.post(tutor(msg, ctx.skipped))

# drop — run now or skip. Buttons, voice (barge-in), typing.
@cx.on_message({"channel": "telegram", "command": "typing", "overlap": "drop"})
def on_typing(thread, msg, ctx):
    thread.typing()
    thread.post("done thinking.")

@cx.on_action({"channel": "telegram", "data": "done", "overlap": "drop"})
def on_done(thread, action, ctx):
    thread.edit(action.message_id, "done.")

@cx.on_message({"channel": "voice", "overlap": "drop"})
def on_speech(thread, msg, ctx):
    thread.post(msg.text)

# debounce — keep only the latest while busy, then fire once.
@cx.on_message({"channel": "telegram", "overlap": "debounce"})
def on_burst(thread, msg, ctx):
    thread.post(tutor(msg, ctx.skipped))

# parallel — overlapping turns on the same key (do not wait).
# Python also has overlap "stream" (same FSM: always execute).
@cx.on_action({"channel": "telegram", "data": "story", "overlap": "stream"})
def on_story(thread, action, ctx):
    with thread.stream(min_chars=1, throttle=0.25) as out:
        for chunk in STORY:
            out.append(chunk)
// queue — default for text. Hold extras; skipped is the burst.
cx.onMessage({ channel: "telegram", overlap: "queue" }, async (thread, msg, { skipped }) => {
  await thread.post(await tutor.run(msg, skipped))
})

// drop — run now or skip. Buttons, voice (barge-in), typing.
cx.onMessage({ channel: "telegram", command: "typing", overlap: "drop" }, async (thread) => {
  await thread.typing()
  await thread.post("done thinking.")
})

cx.onAction({ channel: "telegram", data: "done", overlap: "drop" }, async (thread, action) => {
  await thread.edit(action.message_id, "done.")
})

cx.onMessage({ channel: "voice", overlap: "drop" }, async (thread, msg) => {
  await thread.post(msg.text)
})

// debounce — keep only the latest while busy, then fire once.
cx.onMessage({ channel: "telegram", overlap: "debounce" }, async (thread, msg, { skipped }) => {
  await thread.post(await tutor.run(msg, skipped))
})

// parallel — overlapping turns on the same key (do not wait).
// TypeScript has no "stream" policy yet — same FSM as parallel.
cx.onAction({ channel: "telegram", data: "story", overlap: "parallel" }, async (thread) => {
  const out = thread.stream({ minChars: 1, throttle: 0.25 })
  for (const chunk of STORY) await out.append(chunk)
  await out.close()
})

ctx.skipped

How many inbound events overlap collapsed before this turn ran. It is not a count of events your handler ignored. Pass it to the agent so a queued or debounced burst is one answer, not three overlapping ones.

@cx.on_message({"channel": ["discord", "telegram"], "overlap": "queue"})
def handle(thread, msg, ctx):
    thread.post(tutor(msg, ctx.skipped))
cx.onMessage(
  { channel: ["discord", "telegram"], overlap: "queue" },
  async (thread, msg, { skipped }) => {
    await thread.post(await tutor.run(msg, skipped))
  },
)

drop does not increment skipped — the event never ran. parallel / stream do not queue, so skipped stays 0 unless a previous policy left a count.

The key is per adapter

Overlap is not global. Each adapter names a key; two events with different keys never block each other. In the SDK the key is usually the thread id:

ChannelTypical key
TelegramThe chat (one lock for the whole chat)
SlackThe conversation thread (slack:C…:ts)
DiscordThe channel / thread id
EmailThe address (email:alice@example.com)
SMS / WhatsApp / iMessageThe phone number
MessengerThe page-scoped id

Telegram users in different chats do not serialize. Slack replies in different threads do not serialize. You do not set the key from a handler.

Ingress is not overlap

Always persist and acknowledge the platform webhook first. That ACK is not a handler option and is not ack (the user-visible reply before the handler). Overlap only decides whether the handler runs now, later, or not at all.