Bug Description
When a user sends a message while delegate_task is running, the subagent is killed. The user expects to be able to send follow-up messages without destroying in-flight delegation work.
Root Cause
The default busy_input_mode is "interrupt". When a message arrives for a busy session:
- Gateway calls
running_agent.interrupt(text) on the parent agent (gateway/run.py ~L2930-2932)
AIAgent.interrupt() sets _interrupt_requested = True on the parent AND recursively propagates to all _active_children (run_agent.py ~L1654-1661)
- The child subagent receives the interrupt → its conversation loop breaks → its tool calls abort
- The child future resolves →
_run_single_child returns status: "interrupted"
- Parent's conversation loop sees
_interrupt_requested = True at the next iteration boundary → breaks out
- Result: subagent work is killed, session ends, new message starts a fresh turn
The interrupt is unconditional — it does not distinguish between "I want to talk to the parent" vs "I want to cancel everything."
Why existing modes don't solve this
| Mode |
Behavior during delegate_task |
Problem |
interrupt (default) |
Kills parent + all children |
Destroys subagent work |
steer |
Calls parent.steer(text) — but parent is blocked on _child_future.result() |
Steer is never consumed; parent can't drain steer messages while synchronously blocked inside a tool call |
queue |
Stores message for next turn, no interrupt |
Subagent survives, but user has zero communication ability until delegation finishes |
The fundamental problem: the parent agent is synchronously blocked during delegate_task (delegate_tool.py ~L1514: _child_future.result(timeout=child_timeout)). Any mechanism targeting the parent either kills the child, is never consumed, or silently queues.
Interrupt propagation chain
User sends message
→ gateway._handle_busy_session()
→ running_agent.interrupt(text) # parent
→ parent._interrupt_requested = True
→ for child in _active_children: # run_agent.py L1655-1661
child.interrupt(text) # KILLS THE SUBAGENT
→ child._interrupt_requested = True
→ child tool calls abort
→ child conversation loop breaks
→ child future resolves
→ parent conversation loop breaks
→ delegate_task returns status="interrupted"
→ session ends, new message processed as fresh turn
Additional gap: delegate_tool.py has zero steer awareness
delegate_tool.py contains no references to the steer mechanism at all. Even if steer mode were configured, the delegation code has no path to forward steered messages to the child or handle them while blocked.
Steps to Reproduce
- Configure gateway with default settings (or explicitly
display.busy_input_mode: interrupt)
- Send a message that triggers
delegate_task (e.g., a task that spawns subagents)
- While the subagent is actively working (making API calls, running tools), send any follow-up message
- Observe: subagent is immediately interrupted, its work is lost, the session restarts with the new message
Expected Behavior
Sending a message while a subagent is running should NOT kill the subagent. The subagent should keep working. The user's message should either:
- Be queued and delivered after delegation completes (minimal viable fix)
- Be injectable into the parent's context for the post-delegation response
- Optionally be forwardable to the running subagent (steer-through)
Actual Behavior
The subagent is killed immediately. All in-flight work (API calls, tool executions, partial results) is lost. The user's follow-up message starts a completely fresh turn with no subagent context.
Requirement
The system needs a way for incoming messages during active delegation to not propagate the interrupt to child agents. Specifically:
interrupt() should not blindly cascade to children during delegation — or at minimum, the delegate_task tool should be interrupt-aware and protect its children from parent interrupts that are caused by new user messages (vs explicit /stop cancellation)
- A new busy-input mode or delegation-aware behavior that queues/steers messages without killing subagents — the parent is blocked in a tool call, so the message handling needs to happen at the gateway level, not the agent level
- Steer-through to children (stretch goal) — when the parent is blocked on
delegate_task, incoming steer messages could be forwarded to the active child agent instead of being silently dropped
Minimum spec for a fix
- When
busy_input_mode is interrupt and the parent is currently executing a delegate_task tool call:
- The incoming message should NOT trigger
parent.interrupt() (which cascades to children)
- Instead, the message should be queued (like
queue mode) for delivery after delegation completes
- The user should receive an ack like: "⏳ Subagent working — your message is queued for when it finishes"
- When the user explicitly sends
/stop or /new, THAT should still propagate the full interrupt chain and kill subagents
- This requires the gateway to distinguish between "user sent a conversational message" vs "user sent a cancel command" when deciding whether to interrupt
Stretch: delegation-aware steer
- If
busy_input_mode is steer and the parent is blocked on delegate_task:
- Forward the steer to the active child instead of the parent
- The child sees the user's message injected into its next tool result
- This would enable real-time course correction of subagents without killing them
Affected Components
gateway/run.py — busy session handler, interrupt dispatch (~L2886-2935)
run_agent.py — interrupt() method (~L1597-1663), _active_children propagation
tools/delegate_tool.py — _run_single_child (~L1321), no steer awareness
agent/conversation_loop.py — interrupt check at iteration boundary (~L602-608)
Related Issues
Environment
- Hermes Agent on main (commit 9e30ef2)
- Gateway with Telegram platform
- Default
busy_input_mode: interrupt
- Linux 6.8.0
Proposed Fix Direction
Phase 1 (minimal): Gateway-level guard — when the running agent has active children (_active_children is non-empty), treat incoming messages as queue mode regardless of configured busy_input_mode. Only explicit /stop or /new commands propagate the full interrupt chain.
Phase 2: Delegation-aware steer — delegate_tool.py registers a steer handler on the parent that forwards to the active child. When the parent is blocked on _child_future.result(), incoming steers are relayed to child.steer(text) instead of being dropped.
Phase 3: Async delegation (#11508) — the parent is no longer blocked, so steer/queue/interrupt all work naturally against the parent's conversation loop.
Bug Description
When a user sends a message while
delegate_taskis running, the subagent is killed. The user expects to be able to send follow-up messages without destroying in-flight delegation work.Root Cause
The default
busy_input_modeis"interrupt". When a message arrives for a busy session:running_agent.interrupt(text)on the parent agent (gateway/run.py~L2930-2932)AIAgent.interrupt()sets_interrupt_requested = Trueon the parent AND recursively propagates to all_active_children(run_agent.py~L1654-1661)_run_single_childreturnsstatus: "interrupted"_interrupt_requested = Trueat the next iteration boundary → breaks outThe interrupt is unconditional — it does not distinguish between "I want to talk to the parent" vs "I want to cancel everything."
Why existing modes don't solve this
delegate_taskinterrupt(default)steerparent.steer(text)— but parent is blocked on_child_future.result()queueThe fundamental problem: the parent agent is synchronously blocked during
delegate_task(delegate_tool.py~L1514:_child_future.result(timeout=child_timeout)). Any mechanism targeting the parent either kills the child, is never consumed, or silently queues.Interrupt propagation chain
Additional gap:
delegate_tool.pyhas zero steer awarenessdelegate_tool.pycontains no references to the steer mechanism at all. Even if steer mode were configured, the delegation code has no path to forward steered messages to the child or handle them while blocked.Steps to Reproduce
display.busy_input_mode: interrupt)delegate_task(e.g., a task that spawns subagents)Expected Behavior
Sending a message while a subagent is running should NOT kill the subagent. The subagent should keep working. The user's message should either:
Actual Behavior
The subagent is killed immediately. All in-flight work (API calls, tool executions, partial results) is lost. The user's follow-up message starts a completely fresh turn with no subagent context.
Requirement
The system needs a way for incoming messages during active delegation to not propagate the interrupt to child agents. Specifically:
interrupt()should not blindly cascade to children during delegation — or at minimum, thedelegate_tasktool should be interrupt-aware and protect its children from parent interrupts that are caused by new user messages (vs explicit/stopcancellation)delegate_task, incoming steer messages could be forwarded to the active child agent instead of being silently droppedMinimum spec for a fix
busy_input_modeisinterruptand the parent is currently executing adelegate_tasktool call:parent.interrupt()(which cascades to children)queuemode) for delivery after delegation completes/stopor/new, THAT should still propagate the full interrupt chain and kill subagentsStretch: delegation-aware steer
busy_input_modeissteerand the parent is blocked ondelegate_task:Affected Components
gateway/run.py— busy session handler, interrupt dispatch (~L2886-2935)run_agent.py—interrupt()method (~L1597-1663),_active_childrenpropagationtools/delegate_tool.py—_run_single_child(~L1321), no steer awarenessagent/conversation_loop.py— interrupt check at iteration boundary (~L602-608)Related Issues
delegate_task_streamwith mid-flight interrupt for synchronous delegation #9556 — delegate_task_stream with mid-flight interrupt for synchronous delegationEnvironment
busy_input_mode: interruptProposed Fix Direction
Phase 1 (minimal): Gateway-level guard — when the running agent has active children (
_active_childrenis non-empty), treat incoming messages asqueuemode regardless of configuredbusy_input_mode. Only explicit/stopor/newcommands propagate the full interrupt chain.Phase 2: Delegation-aware steer —
delegate_tool.pyregisters a steer handler on the parent that forwards to the active child. When the parent is blocked on_child_future.result(), incoming steers are relayed tochild.steer(text)instead of being dropped.Phase 3: Async delegation (#11508) — the parent is no longer blocked, so steer/queue/interrupt all work naturally against the parent's conversation loop.