The /background feature was fundamentally non-functional as shipped —
two coupled bugs kept results from ever reaching the user:
1. complete_background() was defined but NEVER called. The
_handle_background thread ran _run_agent_streaming and then exited;
no hook signalled the task tracker that the work was done. Every
background task stayed in status="running" forever and
get_results() (which filters to done-only) always returned [].
2. get_results() called _BACKGROUND_TASKS.pop(parent_sid, []) which
removed the ENTIRE list — including tasks still in flight. Even if
bug #1 were fixed, the first frontend poll during a long-running
task would drop the task from the tracker, and
complete_background()'s loop would iterate over an empty list when
the worker eventually finished — the result would still be lost.
Fix:
- api/background.py::get_results now retains running tasks in the
dict; only done ones are popped and returned.
- api/routes.py::_handle_background wraps _run_agent_streaming in an
inline worker (_run_bg_and_notify) that, after streaming completes,
reloads the hidden bg session, extracts the last non-error assistant
message, and calls complete_background(parent_sid, task_id, answer).
Worker also best-effort unlinks the hidden bg session file so
SESSION_DIR doesn't accumulate debris.
- Exception safety: any failure in _run_agent_streaming or the
post-processing path still calls complete_background with a fallback
sentinel so the frontend's polling loop doesn't hang forever.
Added 5 regression tests in tests/test_background_tasks.py:
- running tasks survive get_results polls
- done tasks are returned and removed
- poll → complete → poll round-trip surfaces the answer (this is the
original bug's reproduction path)
- empty parent is cleaned up
- static check: _handle_background's worker calls complete_background
and uses Session.load to extract the answer
Full suite: 2023 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>