Skip to content

fix(jetbrains): stop CLI on app close - #12105

Merged
kirillk merged 9 commits into
mainfrom
plan-address-issue-12048
Jul 10, 2026
Merged

fix(jetbrains): stop CLI on app close#12105
kirillk merged 9 commits into
mainfrom
plan-address-issue-12048

Conversation

@kirillk

@kirillk kirillk commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes #12048

Context

Closing a JetBrains IDE (originally reported on Windows) can leave a stale kilo serve process behind. The orphan keeps locks on the extracted CLI binary and can block clean IDE shutdown or the next CLI launch. While hardening this, we also addressed adjacent shutdown-robustness gaps found across the CLI lifecycle (download → spawn → connect → shutdown).

Implementation

Process-tree kill is now confirmed, not fire-and-forget. The non-Windows kill path confirms the whole tree has exited after SIGKILL instead of returning immediately, and the JVM shutdown-hook (no-wait) path now escalates SIGTERM→SIGKILL so a binary that ignores SIGTERM (or a stuck sandbox child) is never orphaned on JVM exit. This also removes flakiness in the process-tree kill test, which previously asserted an exit the production code never waited for.

App close no longer blocks the IDE. appWillBeClosed runs on the EDT; it now uses a fast, non-blocking teardown (send SIGTERM and return) with the JVM shutdown hook as the guaranteed backstop, instead of synchronously waiting up to several seconds for the kill + reader-thread joins.

Plugin unload no longer stalls behind an in-flight download. The unload teardown no longer waits on the lifecycle mutex; the CLI manager's own disposal guards handle any concurrent spawn.

kilo serve self-exits when its spawner dies (defense in depth). A hard kill of the IDE (SIGKILL/crash) delivers no signal and runs no shutdown hook. The extension/plugin now pass their PID via KILO_PARENT_PID, and a new parent-death watchdog in kilo serve exits the server when that process disappears.

Rollout note

The parent-death watchdog lives in the CLI core (packages/opencode/). JetBrains runs the pinned CLI release, so the watchdog is dormant for JetBrains until a CLI release is published and the pin is bumped (packages/kilo-jetbrains/package.json). It is a no-op until then — the extra KILO_PARENT_PID env var is simply ignored by an older CLI, so there is no failure or startup impact. The VS Code extension bundles its own CLI binary and picks it up on the next build. All JetBrains plugin-side fixes (kill confirmation, non-blocking app close, mutex-free unload) take effect immediately.

Tests

  • Deflaked the real process-tree kill test (capture both children deterministically) and added a test for the no-wait SIGKILL escalation path.
  • Added parent-watchdog unit tests (spawn → kill → assert the watchdog fires; no-op when unset/invalid).
  • Updated app-service coverage to assert app-close uses the non-blocking fast path (not the confirming dispose path).
  • Added a deterministic regression test that dispose ignores late SSE callbacks without reconnecting.

Verification

  • ./gradlew typecheck and the cli.* / app.* backend suites pass from packages/kilo-jetbrains/; the process-tree kill test is green across repeated reruns.
  • bun run typecheck passes for packages/opencode/; the parent-watchdog tests pass.
  • bun run typecheck passes for packages/kilo-vscode/.

Reviewer test steps

  1. Open a chat in a JetBrains IDE so kilo serve starts.
  2. Close or restart the IDE; confirm no stale kilo serve / kilo.exe remains and reopening starts Kilo cleanly. Repeat a few times (the original orphaning was intermittent).
  3. Quit the IDE mid-download or mid-request and confirm shutdown does not visibly hang.

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A (backend lifecycle fix, no visual changes)
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

@kilo-code-bot

kilo-code-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 5
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 206 App close can publish a stale Ready result
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 238 Fast close discards descendants before hook escalation
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 364 The rescan still misses reparented late children
packages/opencode/src/cli/cmd/serve.ts 37 Self-signaling bypasses graceful cleanup on Windows
packages/opencode/src/kilocode/parent-watchdog.ts 20 An inherited PID activates the watchdog for nested servers

SUGGESTION

File Line Issue
packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt 54 The test can signal before the TERM trap is installed
packages/opencode/test/kilocode/parent-watchdog.test.ts 44 The successful path leaves a five-second timer running

Fix these issues in Kilo Cloud

Files Reviewed (14 files)
  • .changeset/jetbrains-orphan-cli-shutdown.md - 0 issues
  • .changeset/serve-parent-watchdog.md - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/CliServer.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 3 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/plugin/KiloBackendDynamicPluginListener.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt - 1 issue
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/FakeCliServer.kt - 0 issues
  • packages/kilo-vscode/src/services/cli-backend/server-manager.ts - 0 issues
  • packages/opencode/src/cli/cmd/serve.ts - 1 issue
  • packages/opencode/src/kilocode/parent-watchdog.ts - 1 issue
  • packages/opencode/test/kilocode/parent-watchdog.test.ts - 1 issue
Previous Review Summaries (3 snapshots, latest commit 8afc161)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 8afc161)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 206 App-close can still return Ready after disposing the startup process
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 342 Rescan misses late children reparented before the parent exits

Fix these issues in Kilo Cloud

Files Reviewed (4 files)
  • bun.lock - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 2 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerReadyTest.kt - 0 issues

Previous review (commit a4a66b0)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt 198 App-close shutdown can race an in-flight CLI startup
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 280 A successful Windows helper return does not confirm process exit
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 297 Escalation misses descendants created during the grace period

SUGGESTION

File Line Issue
packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt 27 The parent trap masks regressions in descendant-kill behavior

Fix these issues in Kilo Cloud

Files Reviewed (7 files)
  • .changeset/jetbrains-orphan-cli-shutdown.md - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt - 1 issue
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 2 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/plugin/KiloBackendAppLifecycleListener.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/main/resources/kilo.jetbrains.backend.xml - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt - 1 issue

Previous review (commit 12f510f)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt 198 App-close shutdown can race an in-flight CLI startup
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 277 A successful Windows helper return does not confirm process exit
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 288 Escalation misses descendants created during the grace period

SUGGESTION

File Line Issue
packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt 27 The parent trap masks regressions in descendant-kill behavior

Fix these issues in Kilo Cloud

Files Reviewed (7 files)
  • .changeset/jetbrains-orphan-cli-shutdown.md - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt - 1 issue
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 2 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/plugin/KiloBackendAppLifecycleListener.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/main/resources/kilo.jetbrains.backend.xml - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt - 0 issues
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliKillTest.kt - 1 issue

Reviewed by gpt-5.6-sol-20260709 · Input: 174.6K · Output: 47.2K · Cached: 2.1M

Review guidance: REVIEW.md from base branch main

kirillk added 5 commits July 10, 2026 12:30
Address PR review: honor wait on the Windows tree-kill path so a
reported success is verified against actual process exit; re-enumerate
descendants before SIGKILL so children forked during the grace period
are escalated too. Rework the process-tree test to use a parent that
does not clean up its children so the assertions prove the tree kill.
Add a test for the disposal terminal-state guard.
Confirm process-tree exit after SIGKILL on the non-Windows kill path, and
escalate SIGTERM to SIGKILL on the shutdown-hook (no-wait) path so a
SIGTERM-ignoring tree is not orphaned on JVM exit. Deflake the process-tree
kill test by capturing both children deterministically and add coverage for
the no-wait escalation.

Make JetBrains IDE app-close teardown non-blocking: send SIGTERM and return,
letting the JVM shutdown hook confirm exit, so quitting the IDE no longer
stalls the EDT for up to several seconds. Stop plugin-unload teardown from
waiting on the lifecycle mutex behind an in-flight download.

Add a parent-death watchdog to `kilo serve`: the VS Code extension and
JetBrains plugin pass their PID via KILO_PARENT_PID and the server exits when
that process disappears, covering hard client kills where no signal or
shutdown hook runs.
Locks in the disposal terminal-state guarantee (R5): after dispose(), a late SSE onClosed/onFailure with a stale source must not resurrect the connection or schedule a reconnect. Deterministic — drives the listener directly, no sleeps.
The process-tree kill test flaked on Linux CI ("child still alive"): a
SIGKILLed orphan reparents to init and lingers as an unreaped zombie that
ProcessHandle still reports alive (onExit never fires for a non-child), so the
kill worked but was unobservable. The test now treats a zombie ('Z' in /proc)
or already-reaped process as exited, with an isAlive fallback off Linux.

Also simplify confirmKilled to wait only on the tracked parent (our real
child, reliably reaped) and report descendants best-effort, so wait=true
shutdown no longer blocks on an orphan exit it cannot observe.
@kirillk
kirillk merged commit 12fd290 into main Jul 10, 2026
32 checks passed
@kirillk
kirillk deleted the plan-address-issue-12048 branch July 10, 2026 18:31
)
if (state is CliServer.State.Error && process == proc) {
val current = synchronized(lock) {
if (state !is CliServer.State.Error || process != proc) return@synchronized null

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.

[WARNING]: App close can publish a stale Ready result

This branch only cleans up an Error. If awaitReady completes with Ready just before closeForShutdown() marks the manager closed and terminates the process, spawn() resumes here afterward and returns that stale Ready state. The connection layer can then recreate clients, SSE, and background jobs after disposal has completed. Please re-check closed/process for every result and reject a late Ready result.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

} ?: return
closing = proc
close(proc)
descendants(proc).forEach { it.destroy() }

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.

[WARNING]: Fast close discards descendants before hook escalation

The descendant handles receiving SIGTERM here are immediately discarded. If the CLI parent exits but a child ignores SIGTERM, that child is reparented before the JVM shutdown hook calls descendants(proc) again, so the hook can no longer find it to send SIGKILL. This can still leave an orphan on normal app close. Please retain this snapshot for the hook or otherwise escalate the same process group/tree.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// Re-enumerate before SIGKILL: a tool/shell can fork new descendants during the grace
// period, and killing the known processes can reparent them. Union the fresh scan with
// the original handles so late children are escalated too.
val kids = (original + descendants(proc)).distinctBy { it.pid() }

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.

[WARNING]: The rescan still misses reparented late children

A process can fork from a TERM handler after original is captured and then exit before this second descendants(proc) call. Its new child is already reparented at that point, so it appears in neither snapshot and survives the following SIGKILL pass. The current test creates all children before invoking the kill path, so it does not cover this race. Please use process-group/job semantics or another strategy that cannot lose descendants when the parent exits.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

() =>
new Promise<void>((resolve) => {
// Exit if the editor client that spawned us is hard-killed (no signal reaches us).
const stopWatchdog = startParentWatchdog(() => process.kill(process.pid, "SIGTERM"))

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.

[WARNING]: Self-signaling bypasses graceful cleanup on Windows

Windows emulates process.kill(..., "SIGTERM") as forceful process termination rather than delivering a POSIX signal to this JavaScript listener. The watchdog can therefore terminate the server before disposeAllInstances() and server.stop(true) run, leaving MCP/LSP descendants behind. Please invoke an idempotent shutdown routine directly from the watchdog and retain a bounded force-exit fallback.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

* Returns a function that stops the watchdog.
*/
export function startParentWatchdog(onOrphan: () => void, intervalMs = 1000): () => void {
const configured = Number(process.env["KILO_PARENT_PID"])

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.

[WARNING]: An inherited PID activates the watchdog for nested servers

The managed CLI passes KILO_PARENT_PID into process.env, and its shell, PTY, MCP, and LSP subprocesses inherit that environment. A kilo serve launched from one of those subprocesses therefore watches the editor PID even though the editor is not its direct parent; it can shut down when the launching shell reparents it or when the editor exits, contrary to the documented no-op behavior for manually launched servers. Please validate the launcher identity/direct-parent relationship or scrub this variable from descendant environments.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

val log = TestLog()
// The parent ignores SIGTERM, so only SIGKILL can stop it. This is the shutdown-hook path
// (wait=false); it must still escalate rather than orphan a tree that survives SIGTERM.
val proc = process("sh", "-c", "trap '' TERM; sleep 30")

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.

[SUGGESTION]: The test can signal before the TERM trap is installed

ProcessBuilder.start() only confirms that sh was created; the test immediately invokes the kill path without proving that trap '' TERM has executed. A regression that sends only SIGTERM could therefore pass by terminating the shell before the ignore disposition is active. Consider adding a deterministic readiness handshake after trap installation before calling killCliProcessTree.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

try {
await Promise.race([
orphaned,
new Promise((_, reject) => setTimeout(() => reject(new Error("watchdog did not fire")), 5000)),

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.

[SUGGESTION]: The successful path leaves a five-second timer running

Promise.race does not cancel its losing promise. When orphaned resolves, this referenced timeout remains active for five seconds, leaking asynchronous work across tests and potentially delaying a targeted test process. Please retain the timeout handle and clear it in finally alongside stop().


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
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.

Stale kilo.exe process blocks PhpStorm shutdown and restart

2 participants