Skip to main content
Start with one question: when an agent runs a bash command, should it wait for the command to finish?
Follow that question far enough and it sets off a chain reaction. I’ll walk through it one step at a time, the way we actually thought it through in MiniMax Code (MCode for short). How long should a bash command wait: from foreground blocking to background tasks

A scene everyone has seen

You ask an agent to “start the server and take a look.” It types npm run dev, and then nothing. A dev server never exits on its own, so the agent just sits there waiting. This isn’t a one-off. Someone on X listed the most common ways agents fall over, and this was number two: “Hanging processes. A dev server or watcher that never returns, and the loop just sits there.” The most obvious approach is to wait for the command to finish, feed the output back, and let the model keep thinking. To avoid hanging forever, add a timeout. MCode has always had this path, and today it’s the fallback: a 120-second timeout by default, 300 seconds at most. For commands like ls or git status that take a few hundred milliseconds, this is the best possible answer. But a dev server, a full test suite, or a complete build either never finishes or takes several minutes. Killing it when the timeout hits means the whole run was wasted.

How others do it

The mainstream approach has pretty much converged: run long commands in the background. Take Claude Code. Its docs spell it out: a background command immediately returns a task ID, its output goes to a file, and Claude reads it later; the user can press Ctrl+B to move a running command to the background (someone on X even put together a rundown of this and other shortcuts); and a command that hits its timeout without finishing is moved to the background automatically instead of being killed. That answers “what about the dev server”: it runs in the background and the agent keeps working. But one question is still open:
If a command takes 3 minutes and finishes normally, why not just raise the timeout and wait for it patiently?

Why you can’t just wait: the cache expires

Figure 1: While the foreground waits, there are no new requests; once the command runs past 5 minutes, the next request has to prefill the whole context again. After moving to the background, the model keeps working, and every request renews the cache Waiting has two costs. The first is visible: the user is waiting, and the agent isn’t doing anything. The second is invisible, but more expensive: the prompt cache expires. While a command runs in the foreground, the model side sends no new requests. And caches have a lifetime:
  • Anthropic caches for 5 minutes by default, renewed on every hit. The lifetime counts from the start of the request, so the time the model spends generating counts too. There’s also a 1-hour tier, but writes cost 2x the normal input price.
  • OpenAI’s cache duration varies by model, ranging from 5–10 minutes of inactivity to longer tiers.
If a command runs in the foreground for 6 minutes, by the time it returns, the tens of thousands of tokens of context before it are most likely no longer cached. The next request has to prefill the whole thing again: a slower first token, and a bigger bill. People on X are already doing this math. One person made a point of warning that when an agent waits too long on a build or tests, the cache expires; their fix was to switch to a faster machine and bring build times down. Another team measured a single 2.3-hour session: on their gateway the cache expires after 5 minutes idle, and 49% of the spend came from 6 calls, each one right after a pause of more than 5 minutes. They called it the “pause tax.” They also said themselves that this is an observation from a single session. A faster machine is one answer. We went another way: don’t let the model sit and wait.

What MCode does: wait in the foreground for a while, move to the background if it’s not done, never rerun

Like Claude Code, MCode’s bash has a run_in_background parameter: if the model expects a command to take a long time, it runs it in the background and gets a task_id back right away. That alone isn’t enough, because the model often guesses wrong. It thinks something will take a few seconds, and it runs for ten minutes. So we added another layer: “auto-backgrounding.” The key implementation detail is just one thing: every foreground command starts as a background task from the very beginning; the foreground just waits alongside it for a while. Figure 2: One process from start to finish. The foreground waits at most 60 seconds; if the command isn't done, it returns the same task_id. The 3600-second total timeout includes the foreground time and is never reset (timeline not to scale) Moving to the background doesn’t require killing and restarting anything; there’s only ever one process. The tool description also tells the model explicitly: once you have a task_id, the command is still running in the background, so don’t start it again. Our thinking is that the vast majority of commands finish within a minute, and getting the result directly in the foreground is simplest. And 60 seconds is far below a 5-minute cache lifetime, which leaves plenty of room for the model’s own generation time. Another detail is how the timeout is computed. For an auto-backgrounded command, the total timeout defaults to the upper limit, 3600 seconds, and the time spent waiting in the foreground counts toward it. Moving to the background or reading the output a few times never resets the deadline. Otherwise the model could keep a stuck command running forever just by repeatedly backgrounding it. At this point, “don’t sit and wait” is solved. But the command is no longer tied to the current turn, and everything that came for free in the synchronous model now has to be built by hand. Figure 3: Once a command moves to the background, everything synchronous execution gave you for free has to be rebuilt: completion notifications, anti-polling, an output budget, stopping and cleanup, reminders after compaction, and the precondition for backgrounding at all

Next question: when it finishes, how does the model find out?

When a background command finishes, the runtime injects a hidden message, <background-task-finished>, into the session to wake it up. The message carries only the task_id, status, description, and end time, not the output. If the model wants the result, it has to call task_output itself. There are two reasons for this. First, notifications need to be short: at most 32 tasks and 8 KiB per batch, with descriptions over 256 bytes truncated. Second, a notification is a new message appended at the end, so it never changes the cached prefix. If lots of tasks in one session are finishing at the same time, notifications are rate-limited too: each session can be woken at most 3 times per 60 seconds, and anything beyond that is deferred. Failed deliveries are retried after 1, 2, and 5 seconds, then back off to 60 seconds, for up to 5 minutes.

The question after that: will the model keep asking on its own?

Yes, and it’s expensive. Once it has a task_id, the model can easily write a loop like this: check, not done yet, check again… Every check is a full model request carrying the entire context. Others have hit this too. Someone on X described a fix in another open-source agent: one heavy user’s agent issued roughly 1,900 wait calls in a month against a single quiet background process, each one a request of more than 200K tokens; after switching to automatic notifications, token consumption on the small model dropped by 45%.
That matches our own conclusion: let results come back on their own, rather than having the model go ask.
We block this from three directions:
  • The tool description says it outright: you’ll be notified automatically when it finishes, so don’t poll frequently.
  • task_output can take a wait_ms to block until new output arrives, but for at most 30 seconds; anything longer is treated as 30 seconds.
  • If the model reads the same status and offset twice in a row, the result comes with a hint: if you want to wait for new output, pass wait_ms; don’t keep checking immediately; go do something else and wait for the completion notification.

What if what comes back is too big?

Output returned to the model in the foreground has a 24 KiB budget. Anything over that keeps the beginning and the end, elides the middle, and includes the location of the full log. Background task output is written to a log file, and task_output reads it incrementally by byte offset, so the model can read only what’s new. If the log wasn’t fully written, the model is told that separately, so it doesn’t mistake incomplete output for the whole thing.

Who stops a background task nobody is watching?

The biggest risk with background tasks is that nobody is looking after them. Users feel this very directly. A Claude Code user on X put it bluntly: “Please fix it that whenever you stop the main claude code agent that it also stops all background tasks including sub agents.” When users press stop, they want everything to stop; but when they just step away for a moment, they don’t want long tasks killed for nothing. We handle each scenario separately:
  • Watchdog: an explicit background command with no timeout runs for at most 1 hour by default. In the public code, this value was raised from 30 minutes to 1 hour.
  • User presses Esc to stop: this also cancels the background bash commands and subagents owned by that session (#381). Tasks stopped this way don’t wake the session when they end. Their results are still kept, and the model is told about them through a reminder on the next turn.
  • /clear or switching sessions: only the current turn stops; background tasks keep running. A user stepping away doesn’t mean they don’t want the result anymore.
  • When a headless one-shot run exits: all background bash commands in that process are cleaned up; otherwise a long loop would keep the CLI hanging instead of exiting.
  • Process restart: on startup, tasks left behind by the previous process that no longer have an owner are marked lost, so they don’t show as “running” forever.
Others make similar trade-offs. Claude Code’s docs say background tasks are cleaned up automatically on exit; background commands have no time limit in interactive terminal use, and unattended sessions stop them once a time limit is reached.

After the context is compacted, does the model still remember there’s an unread result?

When the context gets too long, compaction squashes early history into a summary. If “a background task finished and nobody has looked at it yet” lives only in the history, one compaction and it’s gone. Our approach: the source of truth for unread results is the task table, not the history. Each time a request is prepared, the runtime checks the task table for tasks that have finished but haven’t been read. If there are any, it adds a <background-task-completion-reminder> listing the oldest few. When a new task finishes, the reminder goes out right away; otherwise the model is reminded once every 15 steps. Once the model successfully reads task_output, there are no more reminders for that turn.

One last precondition: if you can’t read the result, don’t go to the background

Bash is allowed to move to the background only if task_output is among this turn’s tools. Otherwise it falls back to the pure foreground mode from the beginning (120 seconds by default, 300 at most), and the run_in_background parameter doesn’t even appear in the schema. If a command goes to the background but the model has no way to get the result back, it’s better to just wait for it.

Looking back

Figure 4: The foreground handles fast, the background handles long, and a single process connects the two In one line: the foreground handles fast, the background handles long, and a single process connects the two. All the extra machinery in the background is, at heart, rebuilding what “synchronous” used to give you for free: how results come back, when things end, who cleans up, and whether anything is still remembered after compaction. One more thing to be clear about: moving to the background can’t keep the cache warm forever. If the model has nothing else to do, the turn ends, and the task finishes and wakes it twenty minutes later, that request can still miss the cache. What backgrounding really saves is the time spent “waiting even though there was other work to do.”

A self-check list for agent builders

All of this lives in MiniMax Code’s public repo, mainly under packages/local-runtime/src/background-task/ and packages/agent-tools/src/desktop/.
We’d love to hear from you too: how does your agent handle long-running commands?

References