Skip to content

[Bug]: Sending a message while delegate_task is running kills the subagent — interrupt propagates unconditionally to children #30170

@alt-glitch

Description

@alt-glitch

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:

  1. Gateway calls running_agent.interrupt(text) on the parent agent (gateway/run.py ~L2930-2932)
  2. AIAgent.interrupt() sets _interrupt_requested = True on the parent AND recursively propagates to all _active_children (run_agent.py ~L1654-1661)
  3. The child subagent receives the interrupt → its conversation loop breaks → its tool calls abort
  4. The child future resolves → _run_single_child returns status: "interrupted"
  5. Parent's conversation loop sees _interrupt_requested = True at the next iteration boundary → breaks out
  6. 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

  1. Configure gateway with default settings (or explicitly display.busy_input_mode: interrupt)
  2. Send a message that triggers delegate_task (e.g., a task that spawns subagents)
  3. While the subagent is actively working (making API calls, running tools), send any follow-up message
  4. 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:

  1. 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)
  2. 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
  3. 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.pyinterrupt() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundcomp/agentCore agent loop, run_agent.py, prompt buildercomp/gatewayGateway runner, session dispatch, deliverytool/delegateSubagent delegationtype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions