Summary
When a user messages a bot while the agent is still processing their previous message, the new message silently blocks on a per-user lock: no acknowledgement, no way to cancel the running turn, no way to steer it. On chat platforms this is a daily-use failure mode β long agentic turns (tool loops, web research) routinely take 30β120 seconds, and users naturally send follow-ups ("actually, make it shorter", "stop"). The core SDK already ships an InterruptController and message steering, but the bot layer never exposes them, so from a channel there is no way to interact with an in-flight run at all.
Current behaviour
BotSessionManager serialises turns with a per-user asyncio.Lock (src/praisonai/praisonai/bots/_session.py:113-118, acquired at line 216 around the executor-run agent.chat() call). A mid-run message simply awaits the lock β invisible queuing with no feedback, no merging, no ordering guarantees beyond lock fairness, and no cap.
InboundDebouncer (bots/_debounce.py) only coalesces rapid bursts before a run starts; once agent.chat() is executing it does nothing for follow-ups.
- Built-in commands are limited to
/status, /new, /help (gateway/server.py Telegram handler; bots/_commands.py) β there is no /stop or /queue; grep for interrupt|busy|pending across bots/ and gateway/ matches only KeyboardInterrupt handlers.
- Core already provides the contracts the bot layer needs:
praisonaiagents/agent/interrupt.py β InterruptController for cancelling/aborting a running agent;
SteeringMixin on Agent (praisonaiagents/agent/agent.py:261 mixin list) β mid-run message steering/prioritisation;
- per-session inbox queue on
GatewaySession (gateway/server.py:98-110) β but only for the WebSocket stepper path, not channel bots.
Desired behaviour
Per-session run control for every channel bot:
- Busy acknowledgement β a mid-run message gets immediate, debounced feedback ("β³ still working on your last message β it'll be considered next" ), instead of silence.
- Pending-message slot β follow-ups received mid-run are held (repeats merged) and delivered as the next turn after the current run drains, rather than blindly queuing on a lock.
/stop command β cancels the in-flight run via core InterruptController, releases the session, confirms to the user.
- Optional steer mode β configurable policy:
busy_mode: queue | interrupt | steer, where steer injects the follow-up into the running turn via SteeringMixin.
- Run-generation guard so a cancelled run's late completion cannot overwrite the fresh turn's state.
Layer placement
- Primary layer: wrapper (
src/praisonai/praisonai/bots/)
- Why not core: the run-control contracts (
InterruptController, steering, session protocols) already live in core; what is missing is channel-side wiring and UX, which is platform delivery behaviour.
- Why not tools: not agent-callable β it is how the framework handles inbound traffic around a running agent.
- Why not plugins: busy/queue/interrupt semantics must be a first-class, always-available bot behaviour, not an optional lifecycle add-on; plugins could later customise policies via hooks.
- Secondary touch (optional): core β add
busy_mode and busy_ack fields to praisonaiagents/bots/config.py BotConfig.
- 3-way surface (CLI + YAML + Python): yes β
praisonai bot start --busy-mode queue, channels.<name>.busy_mode: queue in gateway.yaml, BotConfig(busy_mode="queue").
Proposed approach
- Extension point: config (
BotConfig.busy_mode) + a bots/_run_control.py module wired into BotSessionManager.chat() and the shared command handler.
- Minimal API sketch:
# bots/_run_control.py
class SessionRunControl:
"""Per-session pending slot + interrupt handle around agent runs."""
def submit(self, user_id, text) -> RunDecision: # RUN_NOW | QUEUED | MERGED
async def stop(self, user_id) -> bool: # wires InterruptController
def next_pending(self, user_id) -> str | None: # drained after each turn
Resolution sketch
# Before (today)
user: "research X" β agent runs 90s
user: "stop" β silently blocks on per-user lock
user: "hello?" β also blocks; after 90s the agent answers
"research X", then processes "stop" and
"hello?" as fresh prompts, out of context
# After (proposed)
user: "research X" β agent runs
user: "actually only EU data" β bot: "β³ noted β applying to the next turn"
(merged into pending slot, or steered live)
user: "/stop" β run cancelled via InterruptController,
bot: "β
stopped. Send a new message."
Severity
High β affects every multi-turn conversation on every platform once turns exceed a few seconds; the current silent-lock behaviour reads as the bot being broken, and there is no escape hatch short of waiting out the run.
Validation
- Traced
BotSessionManager.chat() lock flow (bots/_session.py:113-118, 191-216): mid-run messages await the per-user lock with no feedback or merging.
- Confirmed
_debounce.py only operates pre-run; confirmed built-in commands lack /stop (bots/_commands.py, gateway Telegram handler).
- Confirmed
grep -rin "interrupt|busy|pending" src/praisonai/praisonai/bots/ src/praisonai/praisonai/gateway/ matches only KeyboardInterrupt handlers.
- Confirmed core
InterruptController (praisonaiagents/agent/interrupt.py) and SteeringMixin (mixin list at praisonaiagents/agent/agent.py:261) exist and are unused by the bot layer.
Summary
When a user messages a bot while the agent is still processing their previous message, the new message silently blocks on a per-user lock: no acknowledgement, no way to cancel the running turn, no way to steer it. On chat platforms this is a daily-use failure mode β long agentic turns (tool loops, web research) routinely take 30β120 seconds, and users naturally send follow-ups ("actually, make it shorter", "stop"). The core SDK already ships an
InterruptControllerand message steering, but the bot layer never exposes them, so from a channel there is no way to interact with an in-flight run at all.Current behaviour
BotSessionManagerserialises turns with a per-userasyncio.Lock(src/praisonai/praisonai/bots/_session.py:113-118, acquired at line 216 around the executor-runagent.chat()call). A mid-run message simply awaits the lock β invisible queuing with no feedback, no merging, no ordering guarantees beyond lock fairness, and no cap.InboundDebouncer(bots/_debounce.py) only coalesces rapid bursts before a run starts; onceagent.chat()is executing it does nothing for follow-ups./status,/new,/help(gateway/server.pyTelegram handler;bots/_commands.py) β there is no/stopor/queue; grep forinterrupt|busy|pendingacrossbots/andgateway/matches onlyKeyboardInterrupthandlers.praisonaiagents/agent/interrupt.pyβInterruptControllerfor cancelling/aborting a running agent;SteeringMixinonAgent(praisonaiagents/agent/agent.py:261mixin list) β mid-run message steering/prioritisation;GatewaySession(gateway/server.py:98-110) β but only for the WebSocket stepper path, not channel bots.Desired behaviour
Per-session run control for every channel bot:
/stopcommand β cancels the in-flight run via coreInterruptController, releases the session, confirms to the user.busy_mode: queue | interrupt | steer, wheresteerinjects the follow-up into the running turn viaSteeringMixin.Layer placement
src/praisonai/praisonai/bots/)InterruptController, steering, session protocols) already live in core; what is missing is channel-side wiring and UX, which is platform delivery behaviour.busy_modeandbusy_ackfields topraisonaiagents/bots/config.pyBotConfig.praisonai bot start --busy-mode queue,channels.<name>.busy_mode: queuein gateway.yaml,BotConfig(busy_mode="queue").Proposed approach
BotConfig.busy_mode) + abots/_run_control.pymodule wired intoBotSessionManager.chat()and the shared command handler.Resolution sketch
Severity
High β affects every multi-turn conversation on every platform once turns exceed a few seconds; the current silent-lock behaviour reads as the bot being broken, and there is no escape hatch short of waiting out the run.
Validation
BotSessionManager.chat()lock flow (bots/_session.py:113-118, 191-216): mid-run messages await the per-user lock with no feedback or merging._debounce.pyonly operates pre-run; confirmed built-in commands lack/stop(bots/_commands.py, gateway Telegram handler).grep -rin "interrupt|busy|pending" src/praisonai/praisonai/bots/ src/praisonai/praisonai/gateway/matches onlyKeyboardInterrupthandlers.InterruptController(praisonaiagents/agent/interrupt.py) andSteeringMixin(mixin list atpraisonaiagents/agent/agent.py:261) exist and are unused by the bot layer.