Repository navigation
Let a Grok chat list and stop the commands it left running in the background - #27022
Conversation
…kground A command Grok backgrounded (a dev server, a watcher, a monitor) showed only as a transcript line: it never reached the background-tasks card above the composer, so it could not be seen as running or stopped from Orca. Grok's subagents and Claude's and Codex's background commands already list and stop there. The ACP lane that publishes Grok's subagents as child records now also publishes its background-task rows: a running task as a live background command or monitor, an ended one as removed, derived from the rows the journal admitted. A Stop for one of them goes through the same adapter route to Grok's own per-task kill (x.ai/task/kill); Grok's completion notice ends the row, and a task Grok no longer holds is removed at once. A refusal reports the task as not stopped; no answer or an unreadable one leaves the outcome unknown. Each Grok process is checked once at start with a no-target kill, beside the existing subagent check. Only Grok naming the method unknown hides Stop.
When Grok answered a Stop with already_exited or not_found, the card row was removed out of band while the transcript row kept saying it was running, and a later restated running notice could bring the card row back with Stop. The answer now settles the transcript row through the translator, like the subagent Stop, so the card row leaves because its row settled, the existing no-relight rule keeps it gone, and a completion Grok still sends after already_exited writes the real result. Also covers /clear being refused while a Grok background command runs.
Review summaryThe problem. In a Grok chat, a command Grok left running in the background (a dev server, a watcher, a monitor) showed only as a transcript line. It never reached the background-tasks card above the message box, so there was no way to see it there or stop it from Orca. Claude and Codex background commands, and Grok's own sub-agents, already have per-row Stop buttons in that card. What changes for the user. On the desktop, each Grok background command or monitor is now a row in the card with its own Stop. Stop kills just that task: the row leaves when Grok reports the task ended, and the transcript line reads "stopped". If Grok says the task was already gone, the row leaves at once and the line reads "status unavailable". If Grok refuses, the existing "The background task wasn't stopped." notice shows and the row stays. While such a task runs, What the review fixed.
Deferred (P2/P3, not blocking).
What was verified.
Not verified. The phone (blocked by "Update to view", as above); Windows, Linux, WSL and SSH hosts (no platform-specific code; Grok does the kill on its own host); the exact error text Grok returns for the capability check (read from its parser, not observed). |
…kground (#27022) * Let a Grok chat list and stop the commands it left running in the background A command Grok backgrounded (a dev server, a watcher, a monitor) showed only as a transcript line: it never reached the background-tasks card above the composer, so it could not be seen as running or stopped from Orca. Grok's subagents and Claude's and Codex's background commands already list and stop there. The ACP lane that publishes Grok's subagents as child records now also publishes its background-task rows: a running task as a live background command or monitor, an ended one as removed, derived from the rows the journal admitted. A Stop for one of them goes through the same adapter route to Grok's own per-task kill (x.ai/task/kill); Grok's completion notice ends the row, and a task Grok no longer holds is removed at once. A refusal reports the task as not stopped; no answer or an unreadable one leaves the outcome unknown. Each Grok process is checked once at start with a no-target kill, beside the existing subagent check. Only Grok naming the method unknown hides Stop. * Settle a Grok task's transcript row when Stop finds it already gone When Grok answered a Stop with already_exited or not_found, the card row was removed out of band while the transcript row kept saying it was running, and a later restated running notice could bring the card row back with Stop. The answer now settles the transcript row through the translator, like the subagent Stop, so the card row leaves because its row settled, the existing no-relight rule keeps it gone, and a completion Grok still sends after already_exited writes the real result. Also covers /clear being refused while a Grok background command runs. (cherry picked from commit d91921a)
ELI5
When Grok starts something that keeps running in the background, like a dev server, Orca now lists it in the background-tasks card above the message box and gives it a Stop button, the same as Claude and Codex chats already do. This is the desktop app; the phone app doesn't open Grok chats yet.
What Changed
The problem. In a Grok chat, a command Grok left running in the background (a dev server, a file watcher, a monitor) only showed up as a line in the transcript ("Started background command …"). It never appeared in the background-tasks card above the composer, so there was no way to see what was still running or to stop it from Orca. Grok's own sub-agents already appear there with a Stop button (#26508), and Claude and Codex chats list and stop their background commands (#26780), so Grok was the odd one out.
Before: a Grok chat that started
pnpm devin the background showed nothing in the card and offered no Stop; the server ran until Grok or the chat ended.After: on the desktop, the command appears in the card as a background command (or a monitor) with its own Stop button. (The phone's card shows these rows too, but today's phone app lists every Grok chat as "Update to view" and doesn't open it, on main as well. Grok rows reach the phone once it opens Grok chats.) Pressing Stop asks Grok to kill exactly that task. The row then:
After a Stop, Grok may start a short reply on its own: it tells the model the task was killed, and Grok can wake the model to answer that.
While a Grok background command or monitor runs,
/clearand/compactare refused, as they already are for Codex and Claude background work. On the desktop the refusal is the existing line "Wait for pending work and messages to finish before using this command." Before this PR they went through, because Grok's background commands never reached the card. A Grok build that doesn't have the kill call still lists the command, without a Stop button, and those rows also hold off/clearand/compactuntil they end.The mechanism. Everything goes through the routes that already exist; there is no new kind, flag, or wire field.
x.ai/task_backgroundedandx.ai/task_completednotices into transcript rows. The same ACP lane that already publishes Grok sub-agents as child records for the card now also publishes those background-task rows: a running one as a live child record (kind: commandormonitor,residency: background), an ended one as removed. Records are derived from the rows the journal has admitted, so nothing new is stored and the card can't disagree with the transcript. The record id isacp-task:<Grok task id>, which tells a Stop which route to take.stopBackgroundTasks(host turn-cancel → adapter router → adapter) now routesacp-task:ids to Grok'sx.ai/task/kill {sessionId, taskId}and every other id to the existing sub-agent cancel. Grok answerskilled,already_exitedornot_found; a refusal comes back inside its reply envelope or as a JSON-RPC error. Forkilledthe row waits for Grok's own completion notice. Foralready_exited/not_foundOrca settles the transcript row itself, through the same translator path the sub-agent Stop already uses, and the card row leaves because its transcript row settled. The settled state is the one that records "Orca can't say how it ended" and is the only settled state a later real ending may overwrite; the journal's existing rule that a settled row is never set running again keeps a late "still running" notice from bringing the card row back.x.ai/task/killwith no target, alongside the existing sub-agent check. Only Grok naming the method unknown (-32601) hides Stop; a timeout or any other answer leaves Stop up, and a real Stop then reports what actually happened (on a Grok that turns out to lack the call, "The background task wasn't stopped."). Rows carrystoppableand the session'ssupportsTaskStop, the fields both clients already read.supportsStopAllstaysfalsefor Grok, as before.Why
This uses the route Claude, Codex and Grok's sub-agents already share instead of adding a Grok-specific card or button. It reads Grok's own start/end notices, which Grok sends for every backgrounded task (its source sends the completion "unconditionally", including after a kill), rather than polling
x.ai/task/list. It stops one task with Grok's own per-task kill, which leaves the chat and its other tasks running, rather than killing Grok's process.Differences from the common pattern, labelled:
sourceon the kill, so Grok treats it as its own per-task UI kill. Grok then tells the model the task was killed, as its own task pane does.background_taskslist. Orca doesn't use it: older Grok builds don't send it, its load-time copy arrives during the replay Orca already drops, and its finished rows don't say whether a task was killed (so they would turn a "was stopped" row into "failed").supportsStopAllstaysfalse, as before).already_exited/not_found, Orca settles the row from the Stop's answer instead of waiting, because afternot_foundGrok sends no ending. Afteralready_exitedGrok may still send one, and it then writes the real result.-32601), and doesn't ask again: a wrong "yes" only makes a Stop answer "wasn't stopped". The existing sub-agent check is unchanged.Source facts were checked first-hand in Grok's source (
x.ai/task/killrequest/reply shapes, theExtMethodResultenvelope, kill → completion notice, theBash | Monitortask kinds) and in the recorded Grok fixtures6-background.jsonl(wire spelling_x.ai/task_backgrounded/_x.ai/task_completed). The kill call is present in every Grok build installed here (July build, 1.0.46, 1.0.50). A real kill round trip against a live, signed-in Grok 1.0.50 confirmed it end to end (see Visual Proof). Not verified: the exact error text Grok returns for the no-target check (derived from its parser, not observed).Linked Issue
N/A (maintainer follow-up to #26780 and #26508).
Visual Proof
The card component is unchanged; the change is which Grok work reaches it. All runs used a hidden, isolated copy of the desktop app built from this branch.
A real, signed-in Grok (1.0.50). Asked to run
sleep 6027in the background, Grok's command shows in the card as one Shell row with its own Stop:After one click on Stop, the
sleep 6027process was gone and the row left the card 1.2 s later, with no notice. The transcript line reads "stopped", and Grok wrote a short reply on its own ("The backgroundsleep 6027was stopped and I did not restart it."):A scripted stand-in Grok, used for the cases a real Grok can't be made to produce on demand:
A background command and a monitor, each with its own Stop and no "Stop all":
Grok refuses the kill: the existing "The background task wasn't stopped." notice appears once, and both rows keep an enabled Stop:
A killed task's line reads "stopped" and only the monitor is left in the card:
Grok answers "not found": the row leaves at once and its line reads "status unavailable":
/clearwhile a command runs is refused with the desktop's existing line:Testing
New
src/main/acp/acp-structured-background-task-stop.test.ts(22 tests). Kill reply contract. Through the real host and adapter with a scripted Grok peer: a backgrounded command or monitor appears with Stop, a natural ending removes it, a targeted Stop sendsx.ai/task/killfor only that task and the row leaves on Grok's completion,already_exited/not_foundsettle the transcript row and remove the card row, a later "still running" notice for that task doesn't bring it back, a completion afteralready_exitedstill writes Grok's result,/clearis refused while a command runs, and a refusal / reply-envelope error / mismatched reply keeps it with the right notice. Probe policy:-32601hides Stop but still lists the command; another error or no answer keeps Stop.Desktop card (
NativeChatBackgroundTasksStatus.test.tsx) and phone card (MobileNativeChatBackgroundTasks.test.tsx): a Grok command row and a Grok sub-agent row each stop by their own id, with no Stop all.Existing ACP suites updated for the second check. On the final head (after the one merge of main): the ACP Stop, child-work, host and adapter suites plus the desktop card test pass (6 files, 96 tests), and the phone card test passes (1 file, 20 tests).
tscfor node and web is clean. CI passes on the final head. Localorca-ci-checkshas two reds that aren't this PR's: mobileno-cyclewarnings in files this PR doesn't touch, which only appear whenmobile/node_modulesis installed locally; and react-doctor run against the old base, which passes when run against current main.Manual runs in a hidden, isolated copy of the app (macOS): a real signed-in Grok 1.0.50 kill round trip, plus a scripted Grok for kill, refusal, "not found", no-answer (Stop comes back after 30 s with the existing "couldn't confirm" notice),
/clearrefusal, and a Grok without the kill call (rows listed with no Stop). The phone run stopped at "Update to view", which is main's existing behaviour for Grok chats on the phone.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
Notes
stoppable,supportsTaskStop), so older clients and hosts are unaffected.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)