Skip to content

Harden OpenCode probe teardown to avoid leaked processes#2657

Open
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/investigate-opencode-leak
Open

Harden OpenCode probe teardown to avoid leaked processes#2657
juliusmarminge wants to merge 1 commit into
mainfrom
t3code/investigate-opencode-leak

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented May 12, 2026

Summary

  • Wrap the OpenCode provider probe in safe scoped teardown so cleanup failures do not mask a successful inventory refresh.
  • Strengthen OpenCode runtime shutdown by terminating direct child processes and matching serve processes, in addition to the existing process-group cleanup.
  • Add coverage for the case where probe teardown throws after a successful inventory load.

Testing

  • Not run (bun fmt)
  • Not run (bun lint)
  • Not run (bun typecheck)
  • Not run (bun run test)

Note

Medium Risk
Touches OpenCode probe lifecycle and subprocess termination logic; mistakes could cause lingering processes or over-aggressive kills on Unix platforms.

Overview
Wraps the OpenCode provider inventory probe with scopedSafeTeardown so finalizer/cleanup errors during scope close no longer mask a successful refresh.

Hardens OpenCode runtime shutdown by, on non-Windows platforms, additionally killing the direct spawned child and any matching opencode serve --hostname=... --port=... processes (via ps scan) alongside the existing process-group kill.

Adds a regression test ensuring checkOpenCodeProviderStatus still returns a ready snapshot even if probe teardown throws after inventory loads.

Reviewed by Cursor Bugbot for commit b538723. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Harden OpenCode probe teardown to avoid leaked processes on scope finalization

  • Wraps the OpenCode inventory probe in scopedSafeTeardown so that teardown/finalizer errors are suppressed and a successful provider refresh result is preserved even if scope cleanup throws.
  • Adds process-enumeration helpers (listProcessCommands, terminateMatchingOpenCodeServeProcesses) to opencodeRuntime.ts that identify and signal stray OpenCode serve processes by matching binary path, hostname, and port.
  • Extends the runOpenCodeCommand finalizer to concurrently send SIGTERM to the process group, the direct child, and any matching serve processes; waits 1 second; then sends SIGKILL to all three.
  • Adds a regression test confirming provider refresh results survive teardown errors.
  • Behavioral Change: teardown is now more aggressive and parallelized on non-Windows platforms; stray child processes that previously leaked will now receive explicit kill signals.

Macroscope summarized b538723.

- keep successful provider refreshes when probe scope teardown fails
- terminate stray matching `opencode serve` processes alongside the child
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2174f2b7-2fa9-455d-90d1-f82c0d3a56ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/investigate-opencode-leak

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels May 12, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Failable killDirectChild short-circuits SIGKILL cleanup phase
    • Changed Effect.asVoid to Effect.ignore in killDirectChild (and the Windows path of killOpenCodeProcessGroup) so that errors from child.kill on already-exited processes are suppressed, allowing the full SIGTERM-then-SIGKILL kill chain to complete.

Create PR

Or push these changes by commenting:

@cursor push b3ce952ec9
Preview (b3ce952ec9)
diff --git a/apps/server/src/provider/opencodeRuntime.ts b/apps/server/src/provider/opencodeRuntime.ts
--- a/apps/server/src/provider/opencodeRuntime.ts
+++ b/apps/server/src/provider/opencodeRuntime.ts
@@ -450,7 +450,7 @@
 
       const killOpenCodeProcessGroup = (signal: NodeJS.Signals) =>
         process.platform === "win32"
-          ? child.kill({ killSignal: signal, forceKillAfter: "1 second" }).pipe(Effect.asVoid)
+          ? child.kill({ killSignal: signal, forceKillAfter: "1 second" }).pipe(Effect.ignore)
           : Effect.sync(() => {
               try {
                 process.kill(-Number(child.pid), signal);
@@ -461,7 +461,7 @@
               }
             });
       const killDirectChild = (signal: NodeJS.Signals) =>
-        child.kill({ killSignal: signal, forceKillAfter: "1 second" }).pipe(Effect.asVoid);
+        child.kill({ killSignal: signal, forceKillAfter: "1 second" }).pipe(Effect.ignore);
       const killMatchingServeProcesses = (signal: NodeJS.Signals) =>
         terminateMatchingOpenCodeServeProcesses({
           binaryPath: input.binaryPath,

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b538723. Configure here.

});
const terminateChild = killOpenCodeProcessGroup("SIGTERM").pipe(
const killDirectChild = (signal: NodeJS.Signals) =>
child.kill({ killSignal: signal, forceKillAfter: "1 second" }).pipe(Effect.asVoid);
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.

Failable killDirectChild short-circuits SIGKILL cleanup phase

High Severity

killDirectChild uses Effect.asVoid (which only discards the success value) instead of Effect.ignore (which also suppresses errors). Since child.kill from @effect/platform is a failable effect, it can produce a PlatformError (e.g., when the child already exited). When it fails inside Effect.all, the entire SIGTERM phase fails, which short-circuits Effect.andThen so the SIGKILL phase never runs. The trailing Effect.ignore on the pipeline only suppresses the overall error — it doesn't undo the short-circuit. This means killOpenCodeProcessGroup("SIGKILL") and killMatchingServeProcesses("SIGKILL") are skipped, potentially leaving orphaned serve processes alive — the exact problem this PR aims to fix.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b538723. Configure here.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 12, 2026

Approvability

Verdict: Needs human review

This PR introduces significant changes to process termination logic with new system-level operations. An unresolved high-severity review comment identifies a bug where improper error handling in killDirectChild could short-circuit the SIGKILL phase, potentially leaving orphaned processes alive.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant