Repository navigation
Let a Codex chat stop a command it left running in the background - #26780
Conversation
A command Codex started and left running (a dev server, a watcher) showed in the background-tasks strip above the composer with no Stop, because Codex reported no stop it could honour. Codex 0.140+ has one: the app-server's experimental thread/backgroundTerminals/terminate names the process by the processId on the command's item, and thread/backgroundTerminals/list shows what still runs. The command tracker now keeps each command's processId. The first time a running command carries one, the session probes its own app-server once with a one-row list; a listing marks the app-server's backgrounded commands stoppable and the session as offering per-row Stop and Stop all, through the fields the strip already reads. Any refusal (an older Codex answers "unknown variant"), timeout or malformed answer leaves it as today: no Stop. A Stop terminates each named process, then confirms against the thread's own list. A thread Codex says it unloaded counts as stopped; a dropped connection does not, since lost contact is no proof a process ended. An app-server that times out is not asked again. The killed process's own item/completed removes its row, as before.
…nd Stop fails A background-terminal probe that timed out or lost its request used to hide Stop for the rest of the app-server's life. Only a refusal by name (unknown variant, method not found) or a malformed listing now settles it as unsupported; anything else is asked again on the next update. A background Stop whose agent failed (a process still listed after its stop, a timeout) used to answer "nothing stopped" and the button simply came back. The host now refuses it, so both clients show their usual notice: "The background task wasn't stopped." This covers Claude's background Stop too.
…ll running A timeout or lost contact with the agent leaves the outcome unknown, so it now answers with the existing outcome-unknown refusal instead of "The background task wasn't stopped."
A confirmed Stop re-enabled its button for the moment between the host's answer and the roster dropping the row. The button now holds while the host says it stopped the task and the strip still lists it, on desktop and phone; a failed, unconfirmed or nothing-stopped answer, or the row coming back, offers Stop again.
…nd-stop-all-agents
…r subagent unconfirmed
Grok answers an ended or unknown subagent with a normal reply, so a JSON-RPC refusal means it did nothing and the subagent still runs.
Review summaryThe problem. In a native chat with Codex, a command the agent left running in the background (a dev server, a watcher) showed in the background-tasks card above the message box with no Stop button. Only Claude chats had one, so a Codex user could not stop it from Orca, and anything waiting on it (for example a What changes for you. In Codex chats on Codex 0.140 or later, each command Codex left running gets its own Stop in that card, on desktop and phone. Pressing it ends the command; the row goes away only after Codex confirms it is gone. If Codex confirms the command is still running, you see "The background task wasn't stopped." If Orca can't tell (a timeout, or contact with Codex is lost), you see "Orca couldn't confirm what happened. Check the chat." on desktop instead of a false "stopped" or "wasn't stopped". Older Codex versions keep today's card with no Stop. Claude's background Stop gets the same honest feedback when it fails or can't be confirmed (it used to be silent). How. Orca asks Codex's own server to end the command and then checks Codex's list of running commands to confirm it is gone. Whether a Codex version supports this is asked once per Codex process, on the machine that runs Codex (so SSH hosts answer for themselves); only a definite "not supported" answer hides Stop, and a timeout is asked again later. Fixed during review (three review rounds, QA, readiness checklist): the support check no longer gives up for good after one timeout; a dropped connection never counts as stopped; a failed Stop now says so; an unconfirmed Stop says it couldn't confirm instead of claiming failure; the Stop button stays disabled until the row leaves (it briefly re-enabled before); main's new Grok sub-agent Stop now reports Grok's own refusal honestly as "wasn't stopped". Deferred / known limits:
Verified: live with a real signed-in Codex 0.159.0 in a hidden test app: per-command Stop ends the process (checked by its process id) and removes the row; stopping one of two leaves the other; the turn's Stop and background Stop don't affect each other; a paused Codex server gives one "couldn't confirm" toast and the row clears correctly once Codex resumes; main shows no Stop for the same command. Screenshots are in the PR body. CI on the final head passed (16 passed, 16 skipped by path, none failed); the readiness checklist passed with no P0/P1. Not verified: a live "wasn't stopped" case (not stageable honestly), the phone and SSH live, and Codex versions below 0.140 live. |
📝 WalkthroughWalkthroughThe changes add Codex background-terminal support, including capability probing, process-ID tracking, and termination confirmation. Structured cancellation now distinguishes confirmed surviving tasks from unknown stop outcomes. Mobile and renderer task interfaces retain confirmed stops while task rows remain listed and use typed stop results. Priority: ➖ Normal Merge Risk: 🔵 Low · up to A malformed Codex response could make Stop appear successful while the command keeps running. Validate the listing response before confirming a stop. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
9a78da34-0d79-40e8-94d7-f3694ee39be7
📒 Files selected for processing (23)
mobile/src/session/MobileNativeChatBackgroundTasks.test.tsxmobile/src/session/MobileNativeChatBackgroundTasks.tsxmobile/src/session/use-mobile-background-task-stops.tsmobile/src/session/use-mobile-native-chat-composer-tray.test.tsxmobile/src/session/use-mobile-structured-background-tasks.tssrc/main/acp/acp-structured-child-stop.tssrc/main/acp/acp-structured-child-work.test.tssrc/main/codex/codex-background-command-tracker.tssrc/main/codex/codex-background-task-tracker.tssrc/main/codex/codex-background-terminals.test.tssrc/main/codex/codex-background-terminals.tssrc/main/codex/codex-child-work-evidence.tssrc/main/codex/codex-structured-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns.test.tssrc/main/runtime/structured-foreground-command-strip.test.tssrc/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.tsxsrc/renderer/src/components/native-chat/structured-child-records-switch.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/shared/structured-session-background-tasks-view.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
| async function stillRunning( | ||
| rpc: CodexTerminalRpc, | ||
| threadId: string, | ||
| timeoutMs: number | ||
| ): Promise<Set<string>> { | ||
| const running = new Set<string>() | ||
| let cursor: string | null = null | ||
| for (let page = 0; page < MAX_LIST_PAGES; page++) { | ||
| const reply = readRecord( | ||
| await rpc.request( | ||
| 'thread/backgroundTerminals/list', | ||
| { threadId, ...(cursor === null ? {} : { cursor }) }, | ||
| { timeoutMs } | ||
| ) | ||
| ) | ||
| for (const terminal of Array.isArray(reply.data) ? reply.data : []) { | ||
| const processId = readRecord(terminal).processId | ||
| if (typeof processId === 'string') { | ||
| running.add(processId) | ||
| } | ||
| } | ||
| cursor = typeof reply.nextCursor === 'string' ? reply.nextCursor : null | ||
| if (cursor === null) { | ||
| return running | ||
| } | ||
| } | ||
| throw new Error('codex app-server listed more background terminals than Orca reads') | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '25,175p' src/main/codex/codex-background-terminals.ts
rg -n 'backgroundTerminals/list|nextCursor|cursor' src/main/codexRepository: stablyai/orca
Length of output: 22778
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- background terminals file ---'
nl -ba src/main/codex/codex-background-terminals.ts | sed -n '1,240p'
printf '%s\n' '--- bindings ---'
rg -n -F --glob '*.ts' -- 'type CodexTerminalRpc' src
rg -n -F --glob '*.ts' -- 'interface CodexTerminalRpc' src
rg -n -F --glob '*.ts' -- 'function readRecord' src
rg -n -F --glob '*.ts' -- 'const readRecord' src
rg -n -F -- 'readRecord' src/main/codex src/main | head -80
printf '%s\n' '--- request implementations and callers ---'
rg -n -F --glob '*.ts' -- 'request(method' src/main/codex src/main
rg -n -F --glob '*.ts' -- 'terminateCodexBackgroundTerminals' src
printf '%s\n' '--- tests around malformed/list behavior ---'
nl -ba src/main/codex/codex-background-terminals.test.ts | sed -n '1,460p'Repository: stablyai/orca
Length of output: 44168
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- record reader ---'
nl -ba src/main/codex/codex-item-field-readers.ts | sed -n '1,100p'
printf '%s\n' '--- connection declaration and implementation references ---'
rg -n -F --glob '*.ts' -- 'export interface CodexAppServerConnection' src/main/codex
rg -n -F --glob '*.ts' -- 'export type CodexAppServerConnection' src/main/codex
rg -n -F --glob '*.ts' -- 'class CodexAppServerConnection' src/main/codex
rg -n -F --glob '*.ts' -- 'CodexAppServerConnection' src/main/codex/codex-app-server-connection.ts src/main/codex
printf '%s\n' '--- request result parsing ---'
rg -n -F --glob '*.ts' -- 'JSON.parse' src/main/codex
rg -n -F --glob '*.ts' -- 'response.result' src/main/codex
rg -n -F --glob '*.ts' -- 'result:' src/main/codex/codex-app-server*
printf '%s\n' '--- app-server connection files ---'
rg --files src/main/codex | rg 'app-server|rpc|protocol'Repository: stablyai/orca
Length of output: 41455
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- connection types ---'
nl -ba src/main/codex/codex-app-server-connection-types.ts | sed -n '1,100p'
printf '%s\n' '--- session request implementation ---'
nl -ba src/main/codex/codex-app-server-session.ts | sed -n '220,315p'
printf '%s\n' '--- record dispatch ---'
nl -ba src/main/codex/codex-app-server-record-dispatch.ts | sed -n '1,120p'
printf '%s\n' '--- connection request construction ---'
nl -ba src/main/codex/codex-app-server-connection.ts | sed -n '180,250p'
printf '%s\n' '--- PR diff for target file ---'
git diff 39a01f9c0be291695168d3437ba79a28b04736e7 896fece4d335544b64c00d5f73d426a5509a176d -- src/main/codex/codex-background-terminals.tsRepository: stablyai/orca
Length of output: 23172
🌐 Web query:
Codex app-server thread/backgroundTerminals/list response nextCursor data terminals schema
💡 Result:
For `thread/backgroundTerminals/list`, the response `result` is paginated:
```json
{
"data": [
{
"itemId": "item_456",
"processId": "42",
"command": "python3 -m http.server",
"cwd": "/workspace",
"osPid": null,
"cpuPercent": null,
"rssKb": null
}
],
"nextCursor": null
}
```
`data` is an array of terminal records. `itemId`, `processId`, `command`, and `cwd` are strings; `osPid`, `cpuPercent`, and `rssKb` may be `null`. `nextCursor` is a cursor for the next page, or `null` when there are no more pages. Pass a non-null cursor back as `cursor` in the next request. The method is experimental. ([upd.dev](https://upd.dev/openai/codex/src/commit/11faf9af94fa345ba090701f071c8d3107fbca45/codex-rs/app-server/README.md?utm_source=openai))
Request params include `threadId` and optional pagination fields `cursor` and `limit`. ([github.com](https://github.com/openai/codex/blob/main/codex-rs/app-server-protocol/src/protocol/common.rs?utm_source=openai))
Citations:
- 1: https://upd.dev/openai/codex/src/commit/11faf9af94fa345ba090701f071c8d3107fbca45/codex-rs/app-server/README.md?utm_source=openai
- 2: https://github.com/openai/codex/blob/main/codex-rs/app-server-protocol/src/protocol/common.rs?utm_source=openai
Reject malformed terminal-list pages instead of treating them as empty.
CodexAppServerConnection.request returns unvalidated unknown results. A successful response without data, with an invalid nextCursor, or with a terminal lacking processId therefore produces an empty or incomplete set. The confirmation loop then counts the target as stopped even when it may still run.
Suggested fix
const reply = readRecord(
await rpc.request(
'thread/backgroundTerminals/list',
{ threadId, ...(cursor === null ? {} : { cursor }) },
{ timeoutMs }
)
)
- for (const terminal of Array.isArray(reply.data) ? reply.data : []) {
+ if (!Array.isArray(reply.data)) {
+ throw new Error('codex app-server returned an invalid background-terminal list')
+ }
+ const nextCursor = reply.nextCursor
+ if (nextCursor !== null && typeof nextCursor !== 'string') {
+ throw new Error('codex app-server returned an invalid background-terminal cursor')
+ }
+ for (const terminal of reply.data) {
const processId = readRecord(terminal).processId
- if (typeof processId === 'string') {
- running.add(processId)
+ if (typeof processId !== 'string') {
+ throw new Error('codex app-server returned an invalid background-terminal record')
}
+ running.add(processId)
}
- cursor = typeof reply.nextCursor === 'string' ? reply.nextCursor : null
+ cursor = nextCursor📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function stillRunning( | |
| rpc: CodexTerminalRpc, | |
| threadId: string, | |
| timeoutMs: number | |
| ): Promise<Set<string>> { | |
| const running = new Set<string>() | |
| let cursor: string | null = null | |
| for (let page = 0; page < MAX_LIST_PAGES; page++) { | |
| const reply = readRecord( | |
| await rpc.request( | |
| 'thread/backgroundTerminals/list', | |
| { threadId, ...(cursor === null ? {} : { cursor }) }, | |
| { timeoutMs } | |
| ) | |
| ) | |
| for (const terminal of Array.isArray(reply.data) ? reply.data : []) { | |
| const processId = readRecord(terminal).processId | |
| if (typeof processId === 'string') { | |
| running.add(processId) | |
| } | |
| } | |
| cursor = typeof reply.nextCursor === 'string' ? reply.nextCursor : null | |
| if (cursor === null) { | |
| return running | |
| } | |
| } | |
| throw new Error('codex app-server listed more background terminals than Orca reads') | |
| } | |
| async function stillRunning( | |
| rpc: CodexTerminalRpc, | |
| threadId: string, | |
| timeoutMs: number | |
| ): Promise<Set<string>> { | |
| const running = new Set<string>() | |
| let cursor: string | null = null | |
| for (let page = 0; page < MAX_LIST_PAGES; page++) { | |
| const reply = readRecord( | |
| await rpc.request( | |
| 'thread/backgroundTerminals/list', | |
| { threadId, ...(cursor === null ? {} : { cursor }) }, | |
| { timeoutMs } | |
| ) | |
| ) | |
| if (!Array.isArray(reply.data)) { | |
| throw new Error('codex app-server returned an invalid background-terminal list') | |
| } | |
| const nextCursor = reply.nextCursor | |
| if (nextCursor !== null && typeof nextCursor !== 'string') { | |
| throw new Error('codex app-server returned an invalid background-terminal cursor') | |
| } | |
| for (const terminal of reply.data) { | |
| const processId = readRecord(terminal).processId | |
| if (typeof processId !== 'string') { | |
| throw new Error('codex app-server returned an invalid background-terminal record') | |
| } | |
| running.add(processId) | |
| } | |
| cursor = nextCursor | |
| if (cursor === null) { | |
| return running | |
| } | |
| } | |
| throw new Error('codex app-server listed more background terminals than Orca reads') | |
| } |
…6780) * Let a Codex chat stop a command it left running in the background A command Codex started and left running (a dev server, a watcher) showed in the background-tasks strip above the composer with no Stop, because Codex reported no stop it could honour. Codex 0.140+ has one: the app-server's experimental thread/backgroundTerminals/terminate names the process by the processId on the command's item, and thread/backgroundTerminals/list shows what still runs. The command tracker now keeps each command's processId. The first time a running command carries one, the session probes its own app-server once with a one-row list; a listing marks the app-server's backgrounded commands stoppable and the session as offering per-row Stop and Stop all, through the fields the strip already reads. Any refusal (an older Codex answers "unknown variant"), timeout or malformed answer leaves it as today: no Stop. A Stop terminates each named process, then confirms against the thread's own list. A thread Codex says it unloaded counts as stopped; a dropped connection does not, since lost contact is no proof a process ended. An app-server that times out is not asked again. The killed process's own item/completed removes its row, as before. * Ask Codex again after a probe gets no answer, and say when a background Stop fails A background-terminal probe that timed out or lost its request used to hide Stop for the rest of the app-server's life. Only a refusal by name (unknown variant, method not found) or a malformed listing now settles it as unsupported; anything else is asked again on the next update. A background Stop whose agent failed (a process still listed after its stop, a timeout) used to answer "nothing stopped" and the button simply came back. The host now refuses it, so both clients show their usual notice: "The background task wasn't stopped." This covers Claude's background Stop too. * Say a background Stop wasn't stopped only when the agent shows it still running A timeout or lost contact with the agent leaves the outcome unknown, so it now answers with the existing outcome-unknown refusal instead of "The background task wasn't stopped." * Keep a background task's Stop in its stopping state until the row leaves A confirmed Stop re-enabled its button for the moment between the host's answer and the roster dropping the row. The button now holds while the host says it stopped the task and the strip still lists it, on desktop and phone; a failed, unconfirmed or nothing-stopped answer, or the row coming back, offers Stop again. * Keep Grok's own subagent Stop refusal quiet, and a reply about another subagent unconfirmed * Report Grok's own refusal of a subagent Stop as not stopped Grok answers an ended or unknown subagent with a normal reply, so a JSON-RPC refusal means it did nothing and the subagent still runs. (cherry picked from commit e2234a3)
ELI5
When Codex starts something that keeps running after its reply (a dev server, a file watcher), native chat shows it in the background-tasks strip above the message box. In a Claude chat that strip has a Stop button. In a Codex chat it had none, so the only way to stop the dev server was to ask Codex to stop it or to close the chat. This PR gives Codex chats the same Stop, as long as the installed Codex can actually do it, and makes a Stop that fails say so instead of silently doing nothing.
What Changed
Before: a Codex chat's background-tasks strip listed the running command with no Stop button and no "Stop all". Anything waiting for that work to finish (for example a
/clearqueued behind it) waited until the command exited on its own.Separately, in any chat (Claude's too), when a background Stop failed (the process was still running afterwards) or Orca lost touch with the agent while stopping, nothing told you: the Stop button just became clickable again and the row stayed.
After: on Codex 0.140 or newer, each command Codex left running gets its own Stop button on its row in the strip, the same way Claude chats show it. There is no separate "Stop all" button: the strip shows one only for an agent that can stop everything at once but not one task at a time, and neither Claude nor Codex is that kind. Pressing Stop ends the process, and the row leaves the strip when Codex reports the process ended. From the press until the row leaves, its Stop stays greyed out (desktop and phone), so it never looks clickable on a row that is already stopping. On an older Codex nothing changes: no Stop is shown, because there is nothing honest it could do.
If Codex shows the command still running after its Stop, you now see one line through the chat's usual notice (a toast on desktop, the error line on the phone): "The background task wasn't stopped." The row stays and its Stop works again.
If Orca can't tell what happened, because the agent didn't answer in time or the connection to it dropped (in a Codex or a Claude chat), it never claims the task wasn't stopped, since the stop may well have landed. The desktop says "Orca couldn't confirm what happened. Check the chat.", and the row itself shows the truth: it leaves the strip if the task ended, or keeps its Stop if not. The phone shows no line for this case, as it already does for any Stop whose answer it can't confirm, and its row tells the truth the same way.
Child agents (subagents) that Codex spawns still have no Stop of their own in the strip; only commands do.
Mechanism:
thread/backgroundTerminals/terminate {threadId, processId}ends one running command, andthread/backgroundTerminals/list {threadId}lists what still runs. TheprocessIdis the one Codex puts on each command'scommandExecutionitem. Orca already opts into Codex's experimental methods at startup.processId.processId, the chat asks its app-server once for a one-rowlist, off the message path. A real listing means this Codex can stop commands, so its backgrounded commands are marked stoppable and each gets a Stop. An older Codex that refuses the method (it answers "unknown variant" or "method not found"), or a malformed answer, means no Stop, as today, for the life of that app-server. A probe that gets no answer (a timeout, a dropped request) decides nothing: the next update about a running command asks again, one probe at a time. Chats that never leave a command running never send the probe.listto confirm it is gone. A thread Codex says it already unloaded counts as stopped. A dropped connection does not, because losing contact is never proof that a process ended. If a process is still listed after its stop, the Stop reports that it wasn't stopped. A timeout or dropped connection reports only that the outcome is unknown. Each request is capped at 10 seconds, and an app-server that times out is not asked again within the same Stop.supportsTaskStop/supportsStopAll) plus a per-rowstoppable. Codex now fills those in the way Claude does (both true), which gives each stoppable row its own Stop. No wire or protocol change: older phones and older remote clients see the same fields they already understand.stillRunningwhen the agent's own answer shows the task survived its stop (Codex's list after terminate), and throws only when the outcome is unknown (a timeout, or lost contact with the app-server or CLI on the host that runs it). The host turns the first into a refusal with the existing "not done" code (agent_session_operation_invalid), worded "The background task wasn't stopped.", and the second into a refusal with the existing "outcome unknown" code (agent_session_operation_unknown), worded "Orca couldn't confirm what happened. Check the chat." on desktop and shown as nothing on the phone. Both codes and both sentences already exist; no new text and no wire change.cancelled: true) and keeps that row's button disabled while the strip still lists the task. The hold is derived from the stop answer plus the row being listed, with no timer. A failed, unconfirmed or nothing-stopped answer re-enables it at once, and so does a row that leaves and comes back, since that task was not ended. Same rule on desktop and phone; no wire change.Why
The common pattern is one Stop in the background-work banner for every agent, with each agent's adapter stopping that agent's background work its own way. For Codex the common pattern uses exactly these two app-server methods (terminate, then confirm with list, with a 10-second bound). This PR does the same for Codex.
Alternatives considered:
Differences from the common pattern
subagent_finishedevent never reaches Orca, the row stays with its Stop disabled until the strip remounts (switching sessions or reopening the chat). Before, a second press could settle it through Grok'salready_finishedreply. This needs Grok to drop its own finish event; a bounded release timer would cover it at the cost of the timer this design avoids.Other agents: what this PR leaves out, and why
The ask was a Stop for every agent. Research found:
_x.ai/subagent/cancel. This PR only makes a failed sub-agent Stop follow the same rule as Codex: Grok answers an ended or unknown sub-agent with a normal reply (already_finished/not_found), so Grok's own refusal of the stop means it did nothing and the sub-agent still runs, which now reads "The background task wasn't stopped." A reply about a different sub-agent, a timeout or a lost connection reads as unconfirmed, never as "wasn't stopped". (The start-up probe already checks that Grok accepts this request, so a refusal should not happen in practice.)x.ai/task/kill {sessionId, taskId, source}(answerskilled/already_exited/not_found) andx.ai/task/list {sessionId}. I read both in Grok's source and found them in the installed grok 1.0.50 binary. Shipping it needs a new producer that turns Grok's background commands into the strip's records, then routes Stop tox.ai/task/kill. Which Grok version added it, and the exact reply envelope, are unverified without a signed-in Grok run. (The common pattern instead kills Grok's whole process and restarts it on Stop. The per-task kill is finer-grained and keeps the session.) Temporary.Linked Issue
No issue. Maintainer request: native-chat background Stop for agents other than Claude.
Visual Proof
Live run with a real, signed-in Codex (codex-cli 0.159.0, model gpt-6.1-sol), driven by scripted clicks on a hidden Orca window. The "after" shots were taken at this PR's head 8949f93. The commits since then only keep a pressed Stop greyed out until its row leaves, report Grok's own refusal of a sub-agent Stop as "wasn't stopped", and merge main; none of them changes what these shots show. The "before" shot was taken on this PR's main base (d7e985c) with the same prompt.
Not part of this change: the red "Codex isn't signed in. Run
codex login." line above the message box in every shot is a test-setup artifact. Orca's sign-in check ran against the test rig's private, empty Codex folder, while the chat itself used the real sign-in (Codex answered every prompt). The "before" shot from main shows the same line. The "Orca now runs Codex without its shared server" toast is an existing first-run notice in a fresh profile.Before (main): a Codex command left running in the background has no Stop.
After: the same command's row has its own Stop.
After pressing Stop: the process ends and the row leaves the strip. The strip was gone about 0.1 s after the press, and
psshowed thesleep 900process gone. The first shot was taken right after the press and the second a moment later; both already show the strip gone, because on a healthy Codex the stopping state lasts only milliseconds.Two commands, Stop pressed on one: only that one ends. Codex left
sleep 901andsleep 902running ("2 shells — 2 working"). After Stop on 901, the strip lists only 902, still with its Stop, andpsshowed 902 still running.A Stop Orca can't confirm says so once, and the row tells the truth. Staged by pausing the chat's own Codex app-server before pressing Stop on
sleep 903. After the 10-second limit, one toast reads "Orca couldn't confirm what happened. Check the chat.", and the row keeps its Stop. Once the app-server was resumed it finished the stop and the row left the strip.Testing
Real Codex 0.159.0, run in an isolated
HOME/CODEX_HOMEwith no sign-in (the real~/.codexconfig and auth hashes were unchanged):thread/backgroundTerminals/listanswers{"data":[],"nextCursor":null}, a terminate of an unknown process answers{"terminated":false}, an unknown thread answers -32600thread not found: <id>, and an unknown method answers -32600Invalid request: unknown variant …. The code reads each of these answers.Live run with a real, signed-in Codex (codex-cli 0.159.0) at head 8949f93, on hidden Orca windows driven by scripted clicks, with an isolated Orca profile and home (only the chat's own Codex app-server used the real sign-in). Passed: a command Codex left running shows its own Stop; Stop ends the process and the row leaves the strip (about 0.1 s); with two commands, stopping one leaves the other running with its Stop; stopping the turn leaves the background command and its Stop in place, and a background Stop during a running turn leaves that turn and its typed follow-up running. The "couldn't confirm" case was staged by pausing the chat's Codex app-server: one toast after 10 s, the row kept its Stop, and the row left once the app-server resumed and finished the stop. Not staged live: the "The background task wasn't stopped." case, because a real Codex's terminate really ends the process; it is covered by the unit and end-to-end tests below with a fake app-server. Also not run live: the phone strip, an SSH host, and a Codex older than 0.140.
Version boundary: read
common.rsat the Codex release tags.listandterminatefirst appear in rust-v0.140.0 and are absent in 0.139.0.New tests:
src/main/codex/codex-background-terminals.test.ts: Stop offered after the probe, terminate by process id plus list confirm, older Codex gets no Stop, a late probe answer restates already-backgrounded rows as stoppable, one probe per app-server, stale fence stops nothing, list pagination, a listed survivor is answered as still running (even when another request in the same Stop timed out), unloaded thread counts as stopped, a dropped connection (during terminate or during the confirming list) never counts as stopped, timeout stops asking, and a probe that timed out or lost its request is asked again on the next update instead of hiding Stop for good.src/main/runtime/structured-foreground-command-strip.test.ts: end to end through the real host and agent-status store. The roster offers Stop on the command,cancelwith the background scope reachesterminate, and the item's end clears the strip. A command still listed after its stop comes back to the client as a "not done" refusal, a Stop that loses the connection comes back as "outcome unknown", and in both the row keeps its Stop.src/main/acp/acp-structured-child-work.test.ts(from main's Grok sub-agent Stop): Grok's own refusal of a sub-agent Stop is refused as "wasn't stopped"; a reply about a different sub-agent is refused as "outcome unknown". Neither settles the parent, its sibling or the connection.structured-agent-session-turns.test.ts: a background Stop the agent answers as still running is refused as "not done"; one that timed out or lost the agent is refused as "outcome unknown" (the path Claude's background Stop shares). Neither writes a transcript row.NativeChatStructuredSession.test.tsx) and phone (MobileNativeChatBackgroundTasks.test.tsx) strip: a Stop the host confirms keeps that row's button disabled until the row leaves, while a failed one beside it re-enables; the same task listed again offers Stop again; an unconfirmed or nothing-stopped answer re-enables at once. Both fail on the previous head.All
src/main/codextests (193 files), background-task channel/capability/admission/turns tests, Claude and Codex structured integration suites, mobile strip tests, typecheck (node, web), andorca-ci-checks.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Agent skill upstream boundary
Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)