Learn Claude Code
s08

Background Tasks

Concurrency

Background Threads + Notifications

198 LOC6 toolsBackgroundManager + notification queue
Overview

跑长面板回归、Bootstrap 置换、Placebo 检验这种要等几十秒到几分钟的命令,Claude Code 可以丢到后台跑,自己继续干别的。后台命令跑完时它会收到一条通知,回头看结果。

Running example
Research project
耐心资本对企业 ESG 表现的实证项目
What happens in this section

do10 稳健性表 7 的 Placebo 要跑 500 次随机置换、do8 机制的 Sobel z 值需要 Bootstrap 中介效应。Placebo 单次 reghdfe 不到一秒、500 次串行约六到八分钟。把 Placebo 丢后台跑,主会话继续整理 do9 异质性表的 4 维度分组结果,跑完通知回来再合成表 7 PSM+Placebo 摘要。

s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12

"Run slow operations in the background; the agent keeps thinking" -- daemon threads run commands, inject notifications on completion.

Harness layer: Background execution -- the model thinks while the harness waits.

Problem

Some commands take minutes: npm install, pytest, docker build. With a blocking loop, the model sits idle waiting. If the user asks "install dependencies and while that runs, create the config file," the agent does them sequentially, not in parallel.

Solution

Main thread                Background thread
+-----------------+        +-----------------+
| agent loop      |        | subprocess runs |
| ...             |        | ...             |
| [LLM call] <---+------- | enqueue(result) |
|  ^drain queue   |        +-----------------+
+-----------------+

Timeline:
Agent --[spawn A]--[spawn B]--[other work]----
             |          |
             v          v
          [A runs]   [B runs]      (parallel)
             |          |
             +-- results injected before next LLM call --+

How It Works

  1. BackgroundManager tracks tasks with a thread-safe notification queue.
class BackgroundManager:
    def __init__(self):
        self.tasks = {}
        self._notification_queue = []
        self._lock = threading.Lock()
  1. run() starts a daemon thread and returns immediately.
def run(self, command: str) -> str:
    task_id = str(uuid.uuid4())[:8]
    self.tasks[task_id] = {"status": "running", "command": command}
    thread = threading.Thread(
        target=self._execute, args=(task_id, command), daemon=True)
    thread.start()
    return f"Background task {task_id} started"
  1. When the subprocess finishes, its result goes into the notification queue.
def _execute(self, task_id, command):
    try:
        r = subprocess.run(command, shell=True, cwd=WORKDIR,
            capture_output=True, text=True, timeout=300)
        output = (r.stdout + r.stderr).strip()[:50000]
    except subprocess.TimeoutExpired:
        output = "Error: Timeout (300s)"
    with self._lock:
        self._notification_queue.append({
            "task_id": task_id, "result": output[:500]})
  1. The agent loop drains notifications before each LLM call.
def agent_loop(messages: list):
    while True:
        notifs = BG.drain_notifications()
        if notifs:
            notif_text = "\n".join(
                f"[bg:{n['task_id']}] {n['result']}" for n in notifs)
            messages.append({"role": "user",
                "content": f"<background-results>\n{notif_text}\n"
                           f"</background-results>"})
        response = client.messages.create(...)

The loop stays single-threaded. Only subprocess I/O is parallelized.

What Changed From s07

ComponentBefore (s07)After (s08)
Tools86 (base + background_run + check)
ExecutionBlocking onlyBlocking + background threads
NotificationNoneQueue drained per loop
ConcurrencyNoneDaemon threads

Try It

cd claude-code-for-researchers
python agents/s08_background_tasks.py
  1. Run Placebo 500 permutations in the background, then keep tidying do9 heterogeneity tables while it runs
  2. Start 3 background tasks: Placebo 500, Bootstrap mediation, IV1+IV2 joint Hansen J. Check their status.
  3. Run the do10 robustness 13-spec batch in the background and keep auditing terms in 07_论文写作/