Skip to content

Let a Grok chat list and stop the commands it left running in the background - #27022

Merged
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/grok-background-command-stop
Oct 10, 2026
Merged

brennanb2025 merged 3 commits into
mainfrom
brennanb2025/grok-background-command-stop

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​393 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​389
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​248 $\color{#cf222e}{\Huge{\mathbf{−}}}$​36 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​212

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 dev in 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:

  • leaves when Grok reports the task ended (the transcript line reads "was stopped"), with the button held in its stopping state until then, as for Claude and Codex rows;
  • leaves at once, with no notice, if Grok says the task had already exited or it no longer knows it. The transcript line stops saying it is running and reads "status unavailable". Grok can still send the task's ending after "already exited" (the task exited but Grok had not yet processed it); that ending then replaces the line with the real result;
  • stays, with the existing "The background task wasn't stopped." notice, if Grok refuses the kill;
  • stays, with the existing "couldn't confirm" notice, if Grok doesn't answer or answers something Orca can't read.

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, /clear and /compact are 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 /clear and /compact until they end.

The mechanism. Everything goes through the routes that already exist; there is no new kind, flag, or wire field.

  • Listing. Orca already turns Grok's x.ai/task_backgrounded and x.ai/task_completed notices 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: command or monitor, 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 is acp-task:<Grok task id>, which tells a Stop which route to take.
  • Stopping. The ACP adapter's existing stopBackgroundTasks (host turn-cancel → adapter router → adapter) now routes acp-task: ids to Grok's x.ai/task/kill {sessionId, taskId} and every other id to the existing sub-agent cancel. Grok answers killed, already_exited or not_found; a refusal comes back inside its reply envelope or as a JSON-RPC error. For killed the row waits for Grok's own completion notice. For already_exited / not_found Orca 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.
  • Capability. When each Grok process starts, Orca sends x.ai/task/kill with 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 carry stoppable and the session's supportsTaskStop, the fields both clients already read. supportsStopAll stays false for 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:

  • Intended: Orca sends no source on 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.
  • Intended: Grok also sends a full background_tasks list. 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").
  • Intended: Stop kills one task. The common pattern has one Stop that ends all background work at once. A per-task kill leaves the chat and Grok's other tasks running, and matches Grok's own task pane and Orca's per-row Stop for Codex and Claude. There is no "Stop all" for Grok yet (supportsStopAll stays false, as before).
  • Intended: for already_exited / not_found, Orca settles the row from the Stop's answer instead of waiting, because after not_found Grok sends no ending. After already_exited Grok may still send one, and it then writes the real result.
  • Intended: the capability check differs from Let a Codex chat stop a command it left running in the background #26780's Codex check. Codex shows no Stop until Codex proves the call exists, and asks again after a timeout. Grok here shows Stop unless Grok refuses the call by name (-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/kill request/reply shapes, the ExtMethodResult envelope, kill → completion notice, the Bash | Monitor task kinds) and in the recorded Grok fixture s6-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 6027 in the background, Grok's command shows in the card as one Shell row with its own Stop:

real-grok-card-with-stop.jpg

After one click on Stop, the sleep 6027 process 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 background sleep 6027 was stopped and I did not restart it."):

real-grok-after-stop.jpg

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":

card-shell-and-monitor.jpg

Grok refuses the kill: the existing "The background task wasn't stopped." notice appears once, and both rows keep an enabled Stop:

refused-kill-notice.jpg

A killed task's line reads "stopped" and only the monitor is left in the card:

s3-killed-row-gone.jpg

Grok answers "not found": the row leaves at once and its line reads "status unavailable":

s5-not-found-row-gone.jpg

/clear while a command runs is refused with the desktop's existing line:

s6-clear-refused.jpg

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 sends x.ai/task/kill for only that task and the row leaves on Grok's completion, already_exited / not_found settle the transcript row and remove the card row, a later "still running" notice for that task doesn't bring it back, a completion after already_exited still writes Grok's result, /clear is refused while a command runs, and a refusal / reply-envelope error / mismatched reply keeps it with the right notice. Probe policy: -32601 hides 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). tsc for node and web is clean. CI passes on the final head. Local orca-ci-checks has two reds that aren't this PR's: mobile no-cycle warnings in files this PR doesn't touch, which only appear when mobile/node_modules is 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), /clear refusal, 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

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH/remote: structured providers run on their execution host, so a remote workspace's Grok is killed over that host's own connection; clients see only the existing optional fields (stoppable, supportsTaskStop), so older clients and hosts are unaffected.
  • Cross-platform: no platform-specific code; Grok owns the kill.
  • Performance: one extra small request per Grok process start, sent in parallel with the existing sub-agent check (2 s bound).

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

The 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, /clear and /compact are refused, as they already are for Codex and Claude background work.

What the review fixed.

  • When Grok answered "already exited" or "not found", the card row left but the transcript line kept saying "running", and a later "still running" notice could bring the row back. Now Orca settles the transcript line itself, through the same path the sub-agent Stop uses, and the card follows the transcript.
  • Code comments and the description said no ending follows "already exited". Grok can still send one, and it now writes the real result.
  • The description: corrected the /clear wording (the desktop shows its existing "Wait for pending work and messages to finish before using this command." line), removed a sentence contrasting designs, said plainly that the phone doesn't open Grok chats yet, and replaced stale test totals with the counts from this run.

Deferred (P2/P3, not blocking).

  • The capability check shows Stop unless Grok names the kill call unknown. A timeout or other error leaves Stop up, and a Stop then answers honestly ("wasn't stopped"). This is intentionally looser than the Codex check.
  • Grok has no "Stop all" yet; each row stops on its own.
  • A multi-task Stop doesn't re-check the turn between its kill requests. The sub-agent path, which already exists, has the same gap.
  • If one Grok process sends more than 128 task updates and the user then stops a task Orca already forgot, the settled line shows the task id instead of its label.
  • The desktop and phone card tests reuse the Codex test shape with a Grok id.
  • No answer from Grok: after 30 s Stop comes back with the shared "couldn't confirm" notice and nothing new in the chat. That copy is shared and comes from main.
  • The phone: today's phone app lists every Grok chat as "Update to view" and doesn't open it, on main as well. The phone's card already renders these rows (unit test), but no phone build can reach them until the phone opens Grok chats.

What was verified.

  • A real, signed-in Grok 1.0.50 in a hidden, isolated copy of the app (macOS laptop), with its own login used in place. The real command was listed with Stop. One Stop click ended the process, and the row left 1.2 s later with no notice. The line reads "stopped", and Grok wrote its own short reply. Grok's config, hooks, skills and trusted-folders files were unchanged. Grok wrote its usual session, memory, log and cache files for the test folder, and refreshed its login token once. The real Codex, Claude and Orca settings were unchanged.
  • A scripted Grok on a second Mac, covering: two rows with separate Stops and no "Stop all"; a kill; a refusal (one notice, rows kept); "not found" (row gone, "status unavailable"); no answer (Stop returns after 30 s with the shared notice); /clear refused; and a Grok without the kill call (rows listed with no Stop).
  • Final head 4cd44e8 (after the one merge of main): node and web typechecks are clean; the ACP Stop and child-work suites and the desktop card test pass (6 files, 96 tests), and the phone card test passes (20 tests). CI: every check that ran passed (15). The 17 skipped jobs are the same path-gated ones skipped on the earlier head.

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).

@brennanb2025
brennanb2025 marked this pull request as ready for review October 10, 2026 08:54
@brennanb2025
brennanb2025 merged commit d91921a into main Oct 10, 2026
56 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/grok-background-command-stop branch October 10, 2026 09:03
brennanb2025 added a commit that referenced this pull request Oct 15, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant