Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Background tasks

An ordinary tool call holds the turn open until it returns. That is right for reading a file and wrong for a twenty-minute build: the agent cannot answer anything else while it waits, and the alternative it reaches for on its own, nohup … & plus polling, gets no notification when the work is done.

A background tool call returns immediately with a task id and delivers its result later while a resident host is running. A one-shot run waits at exit and prints the outcome without another model turn, so work needed for its answer should stay in the foreground.

Off by default. Turn it on with:

[background]
enabled = true
max_tasks = 10   # concurrent per session

When to enable it

This changes the contract of the primary interaction. Without it, you ask and the agent answers. With it, you ask, the agent answers, and something else may interrupt you several minutes later.

That is right for an assistant that runs unattended, keeps talking while work proceeds, and reports when it lands. It is usually wrong for the interactive case, someone at a terminal using the screen like a command line, where blocking is what you want and asynchrony is a surprise.

Every other capability block ([schedule], [skills], [memory]) defaults on. This one does not, because those add capability without changing when a turn ends.

How the agent uses it

Once enabled, every tool gains an optional background parameter, including tools from MCP servers, since a slow MCP call is exactly the kind worth detaching:

shell_execute({"command": "cargo test --all", "timeout_ms": 900000, "background": true})

Background execution does not extend a tool’s timeout. The example gives the command fifteen minutes; without timeout_ms, the shell’s usual thirty-second timeout still applies.

That returns something like:

Started in the background as task 7f3a1c22 (cargo test --all). Use `task_list` for status. Use `task_cancel` to stop it.

The agent then carries on. When the task ends between turns, its outcome arrives as a new turn:

[Background task reporting at 2026-08-12 14:31 +02:00]

7f3a1c22 (cargo test --all) finished after 12m 4s.

test result: ok. 1674 passed; 0 failed

That is the model’s prompt. The screen does not echo it, any more than it prints a tool’s output: a stage direction above the reply says what reported, Background task 7f3a1c22 finished after 12m 4s, and the reply carries the result. Under meka serve the message reaches the feed as message.appended with a background_report block. An ACP editor is told the same direction as a notice, with the tasks and their outcomes under _meta.background, or as a parenthesized chunk when it renders no notices; the prompt itself is shown to no editor.

When the task ends while a turn is running, the outcome lands inside that turn instead, at its next round boundary: behind the results of the round that was running, in the same message, or after the reply that would have ended the turn, which then goes on, the way a message sent to a working agent does. The header is the same, and the trailer tells the model to carry on rather than to answer. Nothing cuts a reply for a report: the outcome waits for the reply to end. The screen says the same stage direction, meka serve puts a task.reported event on the turn with the text the model read, and an ACP editor gets the same notice. A resumed session replays the direction where the report landed, since the log records a report as a block of its own rather than as words.

Running tasks also appear in the per-turn context under [Background], so the agent can see what it already started and does not launch a second copy:

[Background]
Tasks still running.

- **7f3a1c22**: cargo test --all

That section is rendered fresh every turn from live state, so it is always current rather than something the agent has to reconstruct. It carries no results: an outcome is permanent and belongs in the conversation, delivered as its own turn. The section disappears entirely when nothing is running.

Outcomes

Every task ends in one of four states, and every one of them is reported: inside a running turn, as a turn of its own otherwise, and printed by --oneshot when its one turn did not carry it:

StatusMeaning
completedThe tool returned successfully
failedThe tool returned an error
canceledStopped on request, via task_cancel, /task cancel, or a second Ctrl+C
interruptedThe session holding it went away: meka exited or was signaled, or the client closed the session

interrupted is the one that matters most. A task in flight when meka lets go of its session cannot be resumed, so it is stopped and recorded as interrupted on the way out, and reported the next time something takes ownership of that session: a screen resume, a meka serve reattach, or an ACP session/load. A process that dies writes nothing; the next owner does the retiring then, because holding the session lock is what proves the previous owner is gone. Without this the agent would wait forever on a result it had usually already promised someone.

Large output is written to a scratchpad entry and the delivered turn carries the beginning plus the entry name, so a long build log does not occupy the conversation permanently. A call that names a scratchpad entry has its whole output saved there instead, whatever its size, and the delivered turn says where it went rather than carrying it; a task stopped before its work came back writes nothing there.

Managing tasks

The agent has task_list and task_cancel. You have:

/task                    # list this session's tasks
/task show 7f3a1c22      # one task in full, including its whole id
/task cancel 7f3a1c22    # stop one
/task cancel --all       # stop all of them

A canceled task still reports back, so the agent learns it stopped rather than waiting on it, but it does not interrupt to say so. Every other outcome wakes the agent when it lands, because nobody chose it: a build finished, a tool failed, or a host died holding the task. A cancellation is always somebody’s deliberate act, and that somebody already knows, so it waits and is read at the next round boundary of a running turn, or at the top of whichever turn the session takes next (yours, or a scheduled job’s), as part of that message rather than as one of its own. Canceling several tasks costs no turns at all.

Under meka serve the turn that delivers an outcome is on the session’s event feed, opening with a turn.started whose source is background, so a client watching the session sees the report arrive and the agent act on it. One read inside a running turn is a task.reported event on that turn, carrying the text the model read.

Webhooks do not wait on any of that. Under meka serve, task.finished fires as soon as a task reaches a terminal state, rather than when a turn gets around to reporting it, so a canceled task is announced immediately, and one left running by a host that died is announced when the session is next opened, which it was not before.

An outcome that rides a turn, or a round of one, is part of that turn, so /rewind over that exchange takes the report with it. The task row is already stamped as reported and is not handed out again, so the outcome is gone rather than redelivered. meka session export still has it: the log is append-only (within [session].turn_retention, when set), and a rewind changes what the model sees rather than what was recorded.

Esc and Ctrl+C

The first Esc or Ctrl+C cancels the turn only. Background tasks keep running.

This is the shell’s contract, where Ctrl+C signals the foreground process group and &-ed jobs survive. Losing a twenty-minute build to a keystroke aimed at the answer on screen is unrecoverable, and it is not what the keystroke meant.

meka prints what survived so nothing is hidden:

! Interrupted
• 2 background task(s) still running; stop them with `/task cancel --all`

A second press during the same turn stops them. Between turns, /task cancel --all is the route.

Where it works

HostBehavior
ScreenFull. An outcome lands in a running turn at its next round boundary, else between turns
meka serveFull, for sessions currently resident
ACPFull, for sessions the editor has open
--oneshotA round boundary of the one turn carries what has ended; the run waits for the rest before exiting

A one-shot run exits with the turn, so there is no later turn to deliver into. A task that ends while the turn is still calling tools reaches the model at the next round boundary, as anywhere else. For the rest, rather than kill the work halfway through, the run waits for every outstanding task and then prints the outcomes on stderr; the agent does not see those, since its turn is already over. So a background call whose result the answer needs is worth keeping in the foreground under --oneshot.

Sub-agents (agent_spawn) deliberately cannot start background tasks. A sub-agent’s session ends with the single turn that spawned it, so it could neither outlive that turn nor be around to hear the result.

Concurrent edits

Background tasks make it ordinary for two agents to work in one directory at once. meka does not lock anything: coordination is the orchestrating agent’s job, exactly as it is between two people on one machine.

What it does do is make a lost race visible. file_edit records what a file looked like when it was read, and refuses an edit against a file that changed since:

Error: file 'src/main.rs' changed on disk after you read it. Something else
wrote to it (a shell command, another agent, or the user). Read it again
before editing so you are not overwriting that change, or set force=true to
edit anyway.

This applies whether or not background tasks are enabled: a shell sed -i, or your own editor, produces the same situation.

A file served by the editor under ACP is checked against the editor rather than the disk, since the bytes the agent saw were the editor’s. The check is the same; only the thing it compares against changes. See read-before-edit.