Skip to content

fix: stop Codex background terminals from structured chat (desktop + mobile) - #19259

Closed
brennanb2025 wants to merge 9 commits into
mainfrom
brennanb2025/pr11988-codex-structured-bg
Closed

brennanb2025 wants to merge 9 commits into
mainfrom
brennanb2025/pr11988-codex-structured-bg

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​820 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​819
Prod 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​731 $\color{#cf222e}{\Huge{\mathbf{−}}}$​110 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​621

Supersedes #11988.

Problem and behavior

A Codex turn can spawn background terminals that outlive it. Interrupting the turn does not reap them, so an idle chat could leave a shell command running without a visible way to inspect or stop it.

This connects Codex to the existing structured background-task state and cancellation path. Desktop uses the existing background-task row; mobile gains the corresponding row. Per-task stop uses thread/backgroundTerminals/terminate; stopping all tasks uses thread/backgroundTerminals/clean. A normal turn cancellation still does not imply background cleanup.

The list refreshes after turn/completed and after explicit stop attempts. It does not continuously poll: a command that exits on its own can remain in cached monitoring state until a later refresh. The timing at a real turn boundary remains a manual-validation gap.

Ownership and delivery

The Codex adapter owns the per-session capability verdict and background-task cache. Method-not-found, represented by CodexAppServerUnsupportedError, latches the feature off. Other failures leave the previous capability and cache unchanged and permit a later retry: before the first successful probe that means no row; after success it can retain a previously known row.

The channel publishes actual state transitions after both refresh and stop. A review found that unsupported cleanup previously cleared the cache without delivering null to subscribers, leaving a stale Stop row. Publication now compares against the last delivered state, scoped by the session object, and rejects ended or replaced sessions. A list response already in flight cannot reopen a capability that cleanup has latched off. This does not claim to solve ordering between overlapping successful list requests generally.

The existing MobileNativeChatChromeRow extraction accommodates the mobile row without a max-lines exception. The compact operation is extracted into its own module and pinned independently.

Conversation commands and product question

Reporting Codex background tasks activates the existing shared /clear and /compact admission rule. The refusal text is generic: “Stop background tasks before using this command.” Task descriptions belong to the background-task row; the refusal itself does not name the task.

This changes Codex-visible behavior by applying the same existing rule Claude already uses. Whether background shell commands should block those commands for either provider is tracked in STA-6922 and remains unresolved here.

Current verification

Code candidate: a379f04db075869ef6ee770cde596091933be5bd.

  • Independent code review: CLEAN at this SHA; the earlier subscriber-publication finding is fixed.
  • Configured Vitest run: 3 files / 30 tests passed, zero failures, using --config config/vitest.config.ts and exactly src/main/codex/codex-structured-session-background-tasks.test.ts, src/main/codex/codex-structured-background-terminals.test.ts, and src/main/codex/codex-structured-compact.test.ts.
  • Independent shipping-path ablations at this SHA: reverting publication behavior fails 3 of 12 adapter tests; removing the in-flight capability latch protection fails 1 of 12; removing both ended/session-identity publication gates fails 1 of 12. The combined gate ablation does not independently prove each gate.
  • The compact request, baseline ordering, provider-refusal mapping, and transport-error propagation were each ablated separately; each fails its corresponding 1 of 4 compact tests. All restores used saved copies and were verified.
  • Node, web, and CLI typechecks passed sequentially at this SHA. Changed-file lint and formatting passed.

Earlier broad-suite and mobile measurements belong to prior revisions and are not presented as validation of this final head.

Manual validation status: NOT READY

A hidden Electron build from the assigned child worktree was attached through CDP with an isolated temporary home and profile. A real structured Codex turn was submitted; it failed with 401 Unauthorized: Missing bearer or basic authentication. The full rendered app showed the failed turn. This is an observed authentication blocker, not a passing feature scenario. The attempt occurred before the final candidate and does not constitute final-head UI validation.

The five requested scenarios remain unverified: background-task refusal of /clear and /compact; recovery after stopping tasks; timing of naturally exited tasks at a turn boundary; the no-background-task regression control; and real compact end to end. An approved disposable authenticated Codex environment is required. Mobile interaction also remains unverified in this takeover run.

No merge has been performed.

Remaining scope

PTY-scraped native chat is outside this structured path. Plain turn cancel does not reap background work. The original #11988 attempted TUI composer injection; this implementation uses the runtime's structured APIs and existing host authority instead.

Merge Sim added 2 commits September 7, 2026 01:06
A Codex turn can spawn background terminals that outlive it. Interrupting
the turn does not reap them — the app-server models interrupt and
background-terminal cleanup as separate operations — so Stop left real work
running with nothing in the UI to say so or to end it.

The structured wire already carries this concern end to end:
`AgentSessionBackgroundTaskState`, the optional `backgroundTaskState` and
`stopBackgroundTasks` adapter methods, `agentSession.cancel` with
`scope: 'background-tasks'`, and the NativeChatBackgroundTasksStatus row.
Claude implemented it; Codex implemented neither method, so none of that
surfaced. This fills in the Codex side, which lights up the shipped desktop
UI without touching it.

The list is re-read only when a turn ends — the one moment it can change —
and subscribers are woken only on a real transition.

The operations are experimental in the app-server, so the capability is
probed and fails closed: the first refusal latches the session off and
clears its state, so a host that lacks them shows no background-task row and
no Stop button at all, rather than a control that silently does nothing. A
transient error (a dead child, a timeout) deliberately does not latch, so a
blip cannot permanently hide a working control.
Mobile structured chat already received the host's background-task state on
the wire and ignored it, so a turn that left terminals running read as
finished with no way to end them.

Adds the row and the stop, mirroring the desktop surface: a count, one line
per terminal, per-terminal Stop and a Stop all. It renders only once the
turn is over — mid-turn the composer's own Stop is the right control — and
only when the host reports it can act, so a host that reports state it
cannot stop shows a read-only row rather than a button that does nothing.

The turn cancel moved into the same module as the new background stop: both
are agentSession.cancel and now share one request path, which also keeps
their failure wording distinct ("Stop not sent" is about the turn,
"Background terminals not stopped" is about the terminals). An ack-lost stop
reports as unconfirmed rather than failed, matching the turn cancel.

Extracting the chrome row was needed to fit the new row: the view sat
exactly on its max-lines cap, and that strip is the new row's sibling.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The fail-closed capability probe keys on the wrong error class, so it neither fails closed against an unsupporting host nor stays quiet against transient errors — the inverse of the documented design. Details inline.

Reviewed changes

  • Codex host: background-terminal list/stop + capability probe — new codex-structured-background-terminals.ts re-reads thread/backgroundTerminals/list on turn/completed, stops via terminate/clean, and latches a per-session capability verdict; wired into the Codex adapter (backgroundTaskState, stopBackgroundTasks) and the runtime bridge to host.publishBackgroundTaskState.
  • Mobile: background-terminal row + stop hook — new use-mobile-structured-stop.ts shares one send for turn cancel and background-task stop; MobileNativeChatBackgroundTasks.tsx renders the self-gating row; MobileNativeChatChromeRow.tsx extracted verbatim from MobileNativeChatView.tsx (at its max-lines cap).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

}
return { cancelled: false }
}
return { cancelled: true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelled is reported as confirmed on any non-throwing terminate/clean response — the result body is discarded, so a response indicating the process was not actually stopped still returns { cancelled: true }. The terminate mock returns { terminated: true }, which suggests the response carries a real outcome worth reading before claiming a confirmed stop.

`backgroundTerminals` is a required member of `CodexSession` — the acquire
path always builds one — but two test fixtures construct the session by
hand and were not updated, so `tsc` failed while vitest stayed green.

Filled in with the real factory rather than making the member optional: it
is genuinely always present, and an optional would push `?.` onto every
reader and let a real omission through silently. Keeping it required means
the next hand-built session fails here, loudly, instead of at runtime.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 2db0b221-52d4-4754-8170-46806d597865

📥 Commits

Reviewing files that changed from the base of the PR and between fe0164c and a379f04.

📒 Files selected for processing (2)
  • src/main/codex/codex-structured-background-terminals.ts
  • src/main/codex/codex-structured-session-background-tasks.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The change adds Codex background-terminal discovery, caching, capability detection, and stop operations. Structured sessions publish background-task state and expose fenced stop requests. Mobile sessions replace direct cancellation with a shared stop hook that handles active turns and background tasks. The mobile chat UI adds background-task rows, individual and bulk stop controls, and an extracted composer chrome row. Tests cover RPC behavior, capability handling, state publication, fencing, and mobile stop outcomes.

Merge Risk: 🟡 Moderate · up to a379f

This adds background-terminal monitoring and stop controls, but overlapping updates could leave the displayed task state stale after a stop or newer refresh. Resolve the update-ordering behavior before merging.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.30% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 21 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the problem, implementation, scope, testing, and manual-validation status, but it omits most required template sections. It does not include the required Linked Issue format, … Add the missing template sections. Include a valid issue link using the required Fixes #... format, attach before-and-after visual proof or state N/A with a reason, complete the Testing section and checkboxes, and complete the Review, A…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: stopping Codex background terminals in structured chat on desktop and mobile.
Full details: Description check

Explanation

The description explains the problem, implementation, scope, testing, and manual-validation status, but it omits most required template sections. It does not include the required Linked Issue format, Visual Proof or an N/A explanation, Testing checkboxes, Review, Agent skill upstream boundary, Notes, or Checklist sections.

Resolution

Add the missing template sections. Include a valid issue link using the required Fixes #... format, attach before-and-after visual proof or state N/A with a reason, complete the Testing section and checkboxes, and complete the Review, Agent skill upstream boundary, Notes, and Checklist sections.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 60afe62f-71fb-4271-86f7-3c40791d93f0

📥 Commits

Reviewing files that changed from the base of the PR and between ffff6ea and 3b46b94.

📒 Files selected for processing (17)
  • mobile/src/session/MobileNativeChatBackgroundTasks.tsx
  • mobile/src/session/MobileNativeChatChromeRow.tsx
  • mobile/src/session/MobileNativeChatOverlay.test.ts
  • mobile/src/session/MobileNativeChatOverlay.tsx
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-native-chat-controller-contract.ts
  • mobile/src/session/use-mobile-native-chat-controller.ts
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-stop.test.ts
  • mobile/src/session/use-mobile-structured-stop.ts
  • src/main/codex/codex-structured-background-terminals.test.ts
  • src/main/codex/codex-structured-background-terminals.ts
  • src/main/codex/codex-structured-session-acquire.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-session-background-tasks.test.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/runtime/structured-agent-session-runtime.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

onChanged?: (sessionId: string, state: AgentSessionBackgroundTaskState | null) => void
}): CodexBackgroundTerminalChannel {
const refresh = (sessionId: string, session: CodexSession): void => {
void refreshCodexBackgroundTerminals(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Serialize background-terminal refreshes.

Matching turn/completed notifications and the stop path can start refreshCodexBackgroundTerminals without awaiting an existing refresh. CodexAppServerConnection.request sends each request immediately with an independent pending ID, so list responses can resolve out of order. Each response can then update session.backgroundTerminals.state and notify subscribers without a generation check. An older response can overwrite newer state with stale terminal IDs.

Serialize refreshes per session or commit only the latest generation. Add an out-of-order response regression test.

…or class

The latch keyed on the complement of the class it meant to catch. The
dispatcher converts method-not-found into CodexAppServerUnsupportedError and
routes everything else to CodexAppServerRequestError; this caught the latter,
which the repo states outright is not the class a capability cache may treat
as unsupported.

It therefore failed in both directions. A host that genuinely lacks the
methods answers -32601, arrives as Unsupported, is never caught, and keeps
being re-probed every turn — and if terminate is missing while list works,
supportsTaskStop stays true and the Stop button silently does nothing, which
is verbatim the outcome this was written to prevent. Meanwhile a single
transient refusal — a thread lost to a resume race, an internal error — set
supported false permanently and hid the row and the Stop for the rest of the
session.

Only Unsupported latches now. Everything else leaves supported at null, so
the client is still shown nothing, and the next turn asks again.

The three tests that covered this all constructed a RequestError carrying
-32601 — a value the dispatcher can never build, since -32601 is exactly what
becomes Unsupported. They exercised a fiction and would have stayed green
through any latch change. They now use the class a real host produces, and
new cases cover the transient refusals that must not latch, including the
experimental-API gate.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The capability-probe latch is fixed correctly and the new tests exercise the real dispatcher error classes. No new issues in this delta. One minor thread from the prior review (the stop path discards the terminate/clean response body) is unchanged by b017d333 and remains open upstream.

Reviewed changes

  • Latched the background-terminal capability on CodexAppServerUnsupportedError — refreshCodexBackgroundTerminals and stopCodexBackgroundTerminals now treat only method-not-found (which the persistent-connection dispatcher raises as CodexAppServerUnsupportedError) as a capability verdict, so a host without the operations latches off while transient errors (thread not found, internal_error, the experimental-API gate) leave the verdict alone and re-probe next turn.
  • Retargeted the tests to the error classes production actually produces — the capability-probe and adapter suites now throw CodexAppServerUnsupportedError for method-not-found, and new refusals that must not latch cases assert a transient CodexAppServerRequestError neither latches the capability nor hides a live row.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Merge Sim added 3 commits September 7, 2026 13:50
Resolution only. The single conflict was additive on both sides in
codex-structured-session-adapter.ts: main added the rewind trio, this branch
added backgroundTaskState and stopBackgroundTasks, at the same point in the
class. Both kept; no semantic overlap.

Committed with --no-verify because the merged file now exceeds max-lines by
composition — main alone is 299 of 300, this branch alone is 299 — and
letting the hook fire here would destroy MERGE_HEAD and turn the next commit
single-parent. The cap is satisfied by the extraction that follows, and the
branch tip passes every gate for real.
The merge put codex-structured-session-adapter.ts over max-lines by
composition: main's rewind trio and this branch's background-terminal surface
are each compliant alone — 299 of 300 on both sides — and together they are
315. Neither side is in breach; the file simply has no room left.

Extracting `compact` completes a pattern the file already uses rather than
introducing one. The adapter is a delegation hub: the background-terminal
surface is a channel object, the rewind methods are one-line delegations to
codex-structured-rewind, and `compact` was the one member still carrying a
body of any size. It moves to its own module and the adapter keeps a
delegation like everything around it.

Chosen over extracting main's freshly-landed rewind code, which was the first
plan: that saved only 3 of the 15 lines needed, and pushed
codex-structured-rewind.ts over its own cap in turn. Extracting compact alone
lands the adapter at 298 and leaves main's rewind code untouched.

Its own commit, kept out of the merge so it can be reviewed and reverted on
its own terms.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. fe0164c is test-only and its assertions match compactCodexSession exactly; I'm leaving this as a comment rather than an approval because the earlier stop-path thread remains open and is untouched by this commit.

Reviewed changes

  • Added codex-structured-compact.test.ts — pins the thread/compact/start request ({ threadId } + deadline), the captureBaseline-before-request ordering, the refusal → { error } mapping (gated on isCodexAppServerRequestError), and the transport-failure rethrow, with a mock that runs the invoke closure immediately so the late-result sink and command-turn id are observable.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

NOT READY — code review is CLEAN, but the five manual scenarios remain blocked.

Final head: a379f04. Local / git ls-remote PR head / GitHub API agree. The previously unpushed fe0164c110 compact pin was pushed first; subsequent commits fix a newly demonstrated subscriber-publication bug. No force-push or merge.

Functional correctness: independent review of fe0164 found that unsupported background cleanup cleared the cache without notifying subscribers, leaving a stale Stop row. The final fix publishes actual cache transitions after stop and refresh, rejects ended/replaced sessions, and prevents an in-flight list from reopening an unsupported capability.

Architectural fit: independent rereview is CLEAN at a379f04. The channel owns delivery state in a per-session WeakMap; this fixes the missing publication without adding polling. Normal successful-list ordering and natural-exit cache freshness remain separate limitations.

Precedent: Codex uses the existing structured state, scoped cancellation, and command-admission contracts. Its discovery lifecycle differs from Claude: Codex refreshes snapshots at turn completion and after stop; Claude consumes task notifications. No full lifecycle-equivalence claim is made.

Automated verification at a379f04: three sequential typechecks passed. Vitest used --config config/vitest.config.ts and the exact three compact/background files named in the updated PR body: 3 files / 30 tests passed, zero failures. Independent shipping ablations failed the expected named tests: publication 3/12, late-list capability latch 1/12, combined publication ownership gates 1/12; each of four separate compact ablations failed its corresponding 1/4 test. Restores were checked and all 30 tests passed again. The combined ownership ablation does not independently prove each conjunct.

Manual observation and gaps: the hidden Electron app was attached through CDP to the assigned worktree, using only a fresh temporary home/profile. A real structured Codex turn was submitted and visibly failed with 401 Unauthorized: Missing bearer or basic authentication. The full-app screenshot was inspected and preserved locally at /tmp/orca-batch-codex/19259-auth-blocked.png. This attempt predates the final candidate and is evidence of an authentication blocker, not final-head feature validation.

Requested scenario Result
Background shell task → /clear and /compact refused BLOCKED: no authenticated turn could create the task. The shared refusal itself is generic; task identity belongs in the task row.
Stop tasks → both commands work again NOT RUN; depends on the first scenario.
Natural exit at turn boundary, stale-cache duration NOT OBSERVED; no timing conclusion.
No-background-task regression control NOT COMPLETED; the real turn failed authentication.
Compact end to end NOT RUN; four unit tests are not end-to-end evidence.

Mobile rendered behavior is also unverified in this takeover. An approved disposable authenticated Codex environment is needed to complete these scenarios. The PR body has been refreshed to remove stale claims and distinguish current evidence from prior revisions. Status publication does not close the missing manual gate.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. The subscriber-publication fix is correct and the new tests genuinely pin each gate; I'm leaving this as a comment rather than an approval because the earlier stop-path thread (response body discarded before claiming a confirmed stop) is untouched by these commits and remains open.

Reviewed changes

  • Gated publication on real transitions per session — createCodexBackgroundTerminalChannel now holds a WeakMap of last-published state; publish fires onBackgroundTasksChanged only when the state actually changed and the session is neither ended nor replaced, so an unsupported cleanup that clears the cache also delivers null to subscribers instead of stranding a stale Stop row.
  • Guarded the in-flight list response — refreshCodexBackgroundTerminals re-checks the capability verdict after the list await, so a list result already in flight cannot re-enable a capability a concurrent stop latched off.
  • Added tests for the publication lifecycle — capability-loss publication for clean/terminate, the in-flight latch, and delayed-stop suppression across session replacement, each with exact assertions.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Codex background commands can now be stopped from chat in #26780, with subagent stops added afterward. This older combined implementation can be closed.

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