Skip to content

[codex] Fix desktop terminal stop cancellation#464

Merged
ross0x01 merged 3 commits into
mainfrom
codex/fix-desktop-terminal-stop
May 16, 2026
Merged

[codex] Fix desktop terminal stop cancellation#464
ross0x01 merged 3 commits into
mainfrom
codex/fix-desktop-terminal-stop

Conversation

@ross0x01
Copy link
Copy Markdown
Contributor

@ross0x01 ross0x01 commented May 16, 2026

Summary

Fixes the Stop path for desktop/free-plan local terminal runs by relaying command cancellation through the local command bridge and killing the active streaming process group.

What changed

  • Added a command_cancel relay message for local Centrifugo commands.
  • Passed the abort signal from run_terminal_cmd into the local sandbox command path.
  • Updated the desktop bridge to handle command_cancel and call a new Tauri cancel_stream_command command.
  • Tracked active streaming commands by commandId in Tauri and killed the process group so shell pipelines stop as a unit.
  • Added matching process-tree cancellation behavior to the standalone local sandbox client.
  • Added regression coverage for publishing command_cancel on Centrifugo abort.

Root cause

Desktop/free-plan local runs were relying on fragile process discovery for cancellation. That could leave parts of a shell pipeline running after Stop was pressed.

Validation

  • pnpm test -- lib/ai/tools/utils/__tests__/centrifugo-sandbox.test.ts --runInBand
  • pnpm test -- lib/ai/tools/__tests__/run-terminal-cmd.test.ts --runInBand
  • pnpm exec tsc --noEmit --pretty false
  • cargo check in packages/desktop/src-tauri
  • Commit hook also completed repo typecheck and Jest before committing.

Summary by CodeRabbit

  • New Features

    • Command cancellation for streaming terminal commands — users can abort running commands across platforms; aborted runs now consistently resolve with exit code 130.
    • Abort signals are respected for remote sandbox runs, and streaming commands are cleaned up reliably on timeout or cancel.
  • Tests

    • Added automated tests covering command cancellation and abort race conditions.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hackerai Ready Ready Preview, Comment May 16, 2026 11:35pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8eaa72dd-fd60-430e-ba21-9bd0416954ef

📥 Commits

Reviewing files that changed from the base of the PR and between 11db8e6 and fa203b0.

📒 Files selected for processing (1)
  • packages/desktop/src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/desktop/src-tauri/src/lib.rs

📝 Walkthrough

Walkthrough

This PR implements end-to-end cancellation for streaming terminal commands: adds command_cancel types, propagates AbortSignal into sandbox runs, publishes cancel messages from Centrifugo sandbox, tracks and terminates streaming PIDs in Tauri/local backends (cross-platform), and wires cancellation through the desktop bridge.

Changes

Streaming Command Cancellation

Layer / File(s) Summary
Type contracts for command cancellation
lib/centrifugo/types.ts, lib/ai/tools/utils/sandbox-types.ts
CommandCancelMessage interface added and CommonSandboxInterface.commands.run opts gain optional signal?: AbortSignal.
Centrifugo sandbox cancellation implementation
lib/ai/tools/utils/centrifugo-sandbox.ts
Adds command_cancel parsing, accepts signal?: AbortSignal for commands.run, tracks publish state, registers abort handlers that publish cancel messages, and resolves canceled runs with exitCode: 130.
Centrifugo sandbox cancellation tests
lib/ai/tools/utils/__tests__/centrifugo-sandbox.test.ts
Adds tests for command cancellation and publish-race handling, asserting command_cancel publish, cleanup, and exitCode: 130.
Terminal command abort handling
lib/ai/tools/run-terminal-cmd.ts
Special-cases Centrifugo sandbox aborts to resolve with current output and exitCode: 130, and propagates abortSignal via runOptions into retry-wrapped sandboxInstance.commands.run calls.
Platform process-group helpers
packages/desktop/src-tauri/src/platform.rs
Adds setsid in build_command, introduces terminate_process_group helper, and updates graceful_kill/cancel_process_tree to use process-group SIGTERM→SIGKILL escalation.
Tauri backend streaming & cancel IPC
packages/desktop/src-tauri/src/lib.rs
Adds wait_with_output_or_kill_on_timeout, StreamCommandState PID map, records/removes PIDs for execute_stream_command, exposes cancel_stream_command, and wires manage/invoke changes.
Local sandbox streaming cancellation
packages/local/src/index.ts
Tracks activeStreamCommands, handles incoming command_cancel publications to terminate tracked ChildProcesses via terminateProcessTree (Windows taskkill, Unix process-group SIGTERM then SIGKILL), spawns detached processes on non-Windows, and cleans up on shutdown.
Desktop bridge cancellation wiring
app/services/desktop-sandbox-bridge.ts
Adds activeCommands tracking, subscribes to command_cancel publications, and invokes Tauri cancel_stream_command only for tracked commandIds.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CentrifugoSandbox as CentrifugoSandbox.commands.run
  participant AbortSignal
  participant Centrifugo as Centrifugo.publish
  Caller->>CentrifugoSandbox: run(command, {signal})
  CentrifugoSandbox->>AbortSignal: register abort listener
  CentrifugoSandbox->>Centrifugo: publish {type: command}
  CentrifugoSandbox->>CentrifugoSandbox: set publishedCommand=true
  AbortSignal->>CentrifugoSandbox: abort event
  CentrifugoSandbox->>Centrifugo: publish {type: command_cancel}
  CentrifugoSandbox->>Caller: resolve {exitCode: 130}
Loading
flowchart LR
  ExecuteCmd["execute_stream_command"] --> SpawnChild["Spawn child & capture PID"]
  SpawnChild --> StateMap["Insert PID into StreamCommandState"]
  CancelCmd["cancel_stream_command"] --> LookupPID["Lock state & lookup PID"]
  LookupPID --> TerminateTree["platform::cancel_process_tree(pid)"]
  TerminateTree --> SigTerm["SIGTERM process group"]
  SigTerm --> Wait["wait 500ms / check"]
  Wait --> SigKill["SIGKILL on timeout"]
  ExecuteCmd --> Cleanup["Remove PID from StreamCommandState"]
Loading
sequenceDiagram
  participant Centrifugo
  participant LocalClient as LocalSandboxClient
  participant ProcessTree
  Centrifugo->>LocalClient: publish {type: command_cancel, commandId}
  LocalClient->>LocalClient: handleCommandCancel -> find ChildProcess
  LocalClient->>ProcessTree: terminateProcessTree(pid)
  alt Windows
    ProcessTree->>ProcessTree: taskkill /T /F
  else Unix
    ProcessTree->>ProcessTree: kill(-pid, SIGTERM)
    ProcessTree->>ProcessTree: wait 500ms
    ProcessTree->>ProcessTree: kill(-pid, SIGKILL)
  end
  LocalClient->>LocalClient: remove from activeStreamCommands
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit nibble on the cancel key, 🐇
Sends a whisper through the Centrifugo sea,
SIGTERM bows, SIGKILL stomps the scene,
Processes end tidy, neat, and clean,
Exit 130 — the user's soft decree.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "[codex] Fix desktop terminal stop cancellation" directly relates to the PR's main objective of fixing the cancellation mechanism for desktop terminal commands when Stop is pressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-desktop-terminal-stop

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

@ross0x01 ross0x01 marked this pull request as ready for review May 16, 2026 23:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/services/desktop-sandbox-bridge.ts (1)

333-349: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't drop a cancel that lands before Rust has registered the PID.

activeCommands is marked before the Tauri side stores the spawned PID. If command_cancel arrives during that window, cancel_stream_command returns false; this handler ignores that result, so the command still runs even though Stop was pressed. Please persist a pending-cancel state in Rust or retry when the invoke reports false.

Also applies to: 362-369

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/desktop-sandbox-bridge.ts` around lines 333 - 349, The
activeCommands flag is set before the Tauri/Rust side has registered the spawned
PID, so a cancel arriving in that race can be lost; update the logic around
invoke("execute_stream_command") and invoke("cancel_stream_command") to persist
and honor a pending-cancel state: when adding commandId to activeCommands (and
in the same area that imports invoke/Channel and calls
invoke("execute_stream_command") in execute_stream_command path), record
commandId in a transient pendingCancel map (or mark it) and, if
invoke("execute_stream_command") returns false (or the first cancel attempt
returns false), retry or enqueue a cancel to invoke("cancel_stream_command")
until it succeeds (or notify Rust to persist pending cancellation), and ensure
the cancel handler consults and clears that pendingCancel entry so a Stop press
before Rust registers PID reliably cancels the process; apply the same change to
the other invoke/cancel pair mentioned (lines 362–369) so both streaming and
non-streaming cancel flows are covered.
packages/desktop/src-tauri/src/lib.rs (1)

1027-1052: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel tracked stream commands during desktop shutdown.

StreamCommandState is only used for explicit stop requests right now. The existing RunEvent::Exit path still tears down PTYs only, so with the new process-group isolation a stream command that is running when the app closes can outlive the desktop process entirely. Please drain this state and kill each tracked command on shutdown as well.

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/ai/tools/utils/centrifugo-sandbox.ts`:
- Around line 244-249: The abort path in publishCancel can race with the publish
attempt because it checks publishedCommand too early and may resolveCanceled
without sending command_cancel; update publish logic so publishCancel knows when
a publish is in-flight and, if publish is pending, wait for the publish promise
to settle (or register a continuation) and then send command_cancel if
publishedCommand becomes true and subscription exists; specifically, introduce
or use an in-flight publish promise/flag (related to publishedCommand in the
publish path around where publishedCommand = true is set) and change
publishCancel to await/attach to that promise before resolving canceled so
remote cancellation is always attempted when publish was started but not yet
flipped to true.

In `@packages/desktop/src-tauri/src/platform.rs`:
- Around line 151-159: build_command() currently calls libc::setsid() which
creates a new session for the child; this leaks detached process groups when
non-streaming callers (execute_command and the /execute path) return on timeout
without killing children. Either remove the unsafe setsid() block from
build_command() and instead call setsid() only in the streaming callers that
invoke platform::graceful_kill(), or keep setsid() but add explicit group-kill
logic to the non-streaming timeout branches (the timeout handling in
execute_command and the /execute handler) so they call platform::graceful_kill()
or send SIGTERM to the child process group; pick one approach and implement it
consistently so every caller that can timeout either does not create a new
session or reliably kills the created session on timeout.

In `@packages/local/src/index.ts`:
- Around line 804-807: activeStreamCommands entries are left running because
cleanup() never kills the detached procs; update cleanup() to iterate
this.activeStreamCommands, call terminateProcessTree(proc) for each tracked
ChildProcess, await/handle results as needed, then clear the map before
proceeding to disconnect Centrifugo or other shutdown steps so spawned pipelines
are terminated during client shutdown.

---

Outside diff comments:
In `@app/services/desktop-sandbox-bridge.ts`:
- Around line 333-349: The activeCommands flag is set before the Tauri/Rust side
has registered the spawned PID, so a cancel arriving in that race can be lost;
update the logic around invoke("execute_stream_command") and
invoke("cancel_stream_command") to persist and honor a pending-cancel state:
when adding commandId to activeCommands (and in the same area that imports
invoke/Channel and calls invoke("execute_stream_command") in
execute_stream_command path), record commandId in a transient pendingCancel map
(or mark it) and, if invoke("execute_stream_command") returns false (or the
first cancel attempt returns false), retry or enqueue a cancel to
invoke("cancel_stream_command") until it succeeds (or notify Rust to persist
pending cancellation), and ensure the cancel handler consults and clears that
pendingCancel entry so a Stop press before Rust registers PID reliably cancels
the process; apply the same change to the other invoke/cancel pair mentioned
(lines 362–369) so both streaming and non-streaming cancel flows are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e4849cc-49f3-4dd5-a97e-7dfec4b62093

📥 Commits

Reviewing files that changed from the base of the PR and between 112e8cb and da5934b.

📒 Files selected for processing (9)
  • app/services/desktop-sandbox-bridge.ts
  • lib/ai/tools/run-terminal-cmd.ts
  • lib/ai/tools/utils/__tests__/centrifugo-sandbox.test.ts
  • lib/ai/tools/utils/centrifugo-sandbox.ts
  • lib/ai/tools/utils/sandbox-types.ts
  • lib/centrifugo/types.ts
  • packages/desktop/src-tauri/src/lib.rs
  • packages/desktop/src-tauri/src/platform.rs
  • packages/local/src/index.ts

Comment thread lib/ai/tools/utils/centrifugo-sandbox.ts
Comment thread packages/desktop/src-tauri/src/platform.rs
Comment thread packages/local/src/index.ts
@ross0x01
Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit cancellation lifecycle findings in follow-up commit 11db8e6f:\n\n- deferred local abort resolution while command publish is in flight, then publishes command_cancel once the command publish succeeds\n- non-streaming desktop timeout paths now kill/reap the spawned process group before returning timeout errors\n- local sandbox cleanup now terminates active detached streaming commands\n\nValidation: focused Centrifugo test, run-terminal-cmd test, pnpm exec tsc --noEmit --pretty false, cargo check, and the commit hook full typecheck/Jest suite passed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/desktop/src-tauri/src/lib.rs`:
- Around line 68-120: The read tasks spawned in
wait_with_output_or_kill_on_timeout (stdout_task and stderr_task) can block
indefinitely after child.wait() returns; wrap the collection of those tasks in a
secondary tokio::time::timeout (e.g., reuse the provided timeout or a short
fixed duration) so that awaiting stdout_task.await and stderr_task.await is
bounded; on timeout abort the remaining tasks (stdout_task.abort(),
stderr_task.abort()), call platform::graceful_kill(&mut child).await if needed,
and return an Err describing the read timeout instead of blocking forever.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 098e69fe-a9a2-446d-91d1-7fcd8a9266e6

📥 Commits

Reviewing files that changed from the base of the PR and between da5934b and 11db8e6.

📒 Files selected for processing (4)
  • lib/ai/tools/utils/__tests__/centrifugo-sandbox.test.ts
  • lib/ai/tools/utils/centrifugo-sandbox.ts
  • packages/desktop/src-tauri/src/lib.rs
  • packages/local/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/local/src/index.ts
  • lib/ai/tools/utils/centrifugo-sandbox.ts

Comment thread packages/desktop/src-tauri/src/lib.rs
@ross0x01
Copy link
Copy Markdown
Contributor Author

Addressed the pipe-drain hang review note in follow-up commit fa203b03: desktop non-streaming command execution now treats stdout/stderr drain as part of the overall timeout. If the shell exits but descendants keep pipes open, it kills the original process group and returns the timeout error instead of awaiting the read tasks indefinitely.\n\nValidation: cargo check passed; commit hook typecheck and full Jest suite passed.

@ross0x01 ross0x01 merged commit 6654c83 into main May 16, 2026
5 checks passed
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