Problem
Both isolated-agent execution paths catch asyncio.CancelledError, persist an INTERRUPTED runtime state, and then return that state normally:
- app/tools/call_subagent.py::_run_subagent
- app/service/agent_runner.py::_run_subagent_task
Returning from the cancellation handler suppresses task cancellation. In asyncio, this can make the awaiting caller complete normally even though its cancellation request is still pending.
This has two user-visible consequences:
- A synchronous call_subagent invocation can turn a parent-run cancellation into a normal ToolResult, allowing the parent agent loop to continue processing after the user stopped or replaced the run.
- Cron wraps run_isolated_agent() in asyncio.wait_for(). When its timeout cancels the isolated agent, the inner runner absorbs the cancellation and returns None; the Cron executor therefore keeps status=ok instead of recording a timeout.
The runtime state is correctly marked as interrupted, but the control-flow outcome presented to callers is successful completion. This creates inconsistent terminal states across the scheduler, tool layer, and persisted sub-agent session.
Reproduction
A minimal asyncio reproduction follows the same control flow:
- Create a parent task that awaits a child task.
- Cancel the parent while the child is running.
- Catch CancelledError inside the child and return a value.
- Observe that the parent returns that value instead of raising CancelledError.
The same behavior occurs through run_isolated_agent() under asyncio.wait_for().
Proposed change
- Persist the existing INTERRUPTED state in both runners.
- Re-raise CancelledError after persistence instead of returning the state.
- Keep the current finally cleanup (agent.close() and session-handle clearing).
- Add regression tests for both tool-driven and service-driven isolated-agent execution.
SubagentSessionManager.interrupt_run() already handles a cancelled child task, and wait_for_subagents waits for task completion before reading the persisted state, so background status queries remain compatible.
Acceptance criteria
- Synchronous sub-agent cancellation propagates to the parent caller.
- asyncio.wait_for(run_isolated_agent(...)) reports a timeout instead of normal completion.
- Interrupted sub-agent state is persisted before cancellation propagates.
- Agent resources and session handles are still cleaned up.
- Background sub-agent status remains queryable as INTERRUPTED.
Problem
Both isolated-agent execution paths catch asyncio.CancelledError, persist an INTERRUPTED runtime state, and then return that state normally:
Returning from the cancellation handler suppresses task cancellation. In asyncio, this can make the awaiting caller complete normally even though its cancellation request is still pending.
This has two user-visible consequences:
The runtime state is correctly marked as interrupted, but the control-flow outcome presented to callers is successful completion. This creates inconsistent terminal states across the scheduler, tool layer, and persisted sub-agent session.
Reproduction
A minimal asyncio reproduction follows the same control flow:
The same behavior occurs through run_isolated_agent() under asyncio.wait_for().
Proposed change
SubagentSessionManager.interrupt_run() already handles a cancelled child task, and wait_for_subagents waits for task completion before reading the persisted state, so background status queries remain compatible.
Acceptance criteria