AgentLoop does not retrieve exceptions from background tasks
Description
AgentLoop.schedule_background() tracks a task until completion, then removes it with set.discard:
task = asyncio.create_task(coro)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
The completion callback never calls task.result() or task.exception(). When a background coroutine fails, the task is removed from the owning set without its exception being retrieved, so asyncio can only emit the generic Task exception was never retrieved warning.
Reproduction
Tested on current main at d47efcc352abaab78b218a9e7c711bfb7a249925.
I installed an event-loop exception handler, scheduled a coroutine that raises RuntimeError, and let the task finish:
tracked_tasks_after_failure: 0
loop_errors: ["Task exception was never retrieved"]
exceptions: ["RuntimeError"]
The task registry is clean, but the failure is detached from the AgentLoop lifecycle and only reaches the generic asyncio handler.
Impact
This helper owns work such as post-turn consolidation, asynchronous session archival, WebUI title generation, and background command execution. Individual jobs may handle expected failures, but an unexpected exception currently has no common terminal observer and can be easy to miss in a long-running service.
Expected behavior
The task completion callback should:
- remove the completed task from
_background_tasks; - ignore normal cancellation;
- retrieve any exception;
- log unexpected failures with traceback and useful task context;
- preserve the existing shutdown behavior that drains pending background tasks.
Suggested regression coverage
- Successful tasks are removed without an error log.
- Failed tasks are removed, retrieved, and logged without an unhandled-loop warning.
- Cancelled tasks are removed without being reported as failures.
Source: HKUDS/nanobot