Skip to content

[Bug]: Duplicate launches can leave headless Electron process trees running on Windows #4395

Description

@lmmontoya-ai

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

This is the exact observed sequence that produced two stale process trees. It has not yet been reduced to a deterministic minimal reproduction.

The preceding update and PowerToys Run are observed context; neither is confirmed as a requirement.

  1. Run T3 Code Nightly on Windows.
  2. Install an available in-app update and allow the application to close/relaunch.
  3. While no application window is visible, launch T3 Code Nightly through PowerToys Run.
  4. Launch it again approximately 30 seconds later.
  5. Allow a subsequent launch to become the normal visible instance.
  6. Inspect all T3 Code (Nightly).exe processes using Task Manager, Process Explorer, or Win32_Process.
  7. Observe whether the earlier launch trees exit.

Observed timeline:

  • Update installation started at 2026-07-22 14:14:09.631 -05:00.
  • First later launch started at 14:18:58.027.
  • Second later launch started at 14:19:28.614.
  • A successful startup trace began at 14:20:40.533.
  • Both earlier process trees were still alive at 2026-07-23 14:56:07 -05:00, more than 24 hours later.

Expected behavior

A secondary process that does not become the single-instance lock owner should notify/reveal the primary instance and terminate promptly.

The rejected process should not leave behind an Electron main process, GPU process, network-service process, window, backend, renderer, or listening socket.

Normal graceful-shutdown handling for the primary application should not prevent a secondary instance from exiting.

Actual behavior

Two earlier launch attempts remained alive as separate headless Electron process trees for more than 24 hours.

Each tree contained:

  • One Electron main process
  • One GPU process
  • One network-service utility process

Neither tree contained:

  • A renderer process
  • An embedded T3 backend process
  • A visible window or window handle
  • A TCP listener owned by any process in the tree

At the latest snapshot, they consumed approximately 325.4 MiB combined.

The process shape and source ordering are consistent with secondary instances reaching the single-instance rejection path and failing to terminate. However, current telemetry does not record the lock result or PID, so this remains the leading hypothesis rather than a confirmed fact.

Suspected lifecycle race

At the affected commit:

  1. DesktopApp.startup performs shell-environment discovery and registers normal lifecycle handling before requesting the single-instance lock.
  2. DesktopClerk.configure requests the lock. If rejected, it calls app.quit() and immediately interrupts the Effect startup fiber.
  3. The already-registered before-quit handler prevents that first quit, requests graceful shutdown, waits for DesktopShutdown.awaitComplete, and schedules a second app.quit().
  4. Shutdown completion depends on the scoped application finalizer, while the rejected startup path is interrupting and unwinding that scope.

The suspected race is between startup interruption/scope teardown, shutdown completion, and the asynchronous second quit.

The persistent processes suggest either that shutdown completion is not observed in this partial-startup state or that the second quit does not execute successfully. Instrumentation is needed to distinguish those paths.

Suggested investigation

  • Log PID, parent PID, lock accepted/rejected, and time spent before requesting the lock.
  • Log entry and completion of before-quit, shutdown completion, the second quit, will-quit, and final process exit.
  • Consider acquiring the lock immediately after resolving and setting the user-data path, before shell discovery and normal lifecycle registration.
  • Give rejected secondary instances a minimal termination path that is not intercepted by primary-instance graceful shutdown.
  • Add a Windows regression test that starts one lock owner and multiple concurrent secondaries, then verifies that every losing process tree exits within a bounded timeout.

Related but not duplicate

  • #1460: Windows NSIS updates could not proceed while application windows remained alive.
  • #4328: Windows shows no progress during a long silent update.
  • #3553: a live Windows backend could incorrectly enter reconnecting state.
  • #2359: analogous leaked-process cleanup for temporary OpenCode servers.
  • #2424: open PR modifying second-instance handling for deep links, but not rejected-instance cleanup.

This report is intentionally limited to the process leak. It does not claim that these stale trees cause the separate reconnect delay.

Impact

Minor bug or occasional failure

This was observed once, but two launches produced two process trees that survived for more than 24 hours and retained approximately 325 MiB. The severity should be reconsidered if controlled testing shows it happens consistently during updates or concurrent launches.

Version or commit

T3 Code Nightly 0.0.29-nightly.20260722.878 @ 9a0a07167f0623c3a7db0ffeff2e3939760309df

Release tag: v0.0.29-nightly.20260722.878

Environment

  • Windows x64, build 26200.8875
  • Timezone: America/Bogota, UTC-05:00
  • Electron 41.5.0
  • Executable: %LOCALAPPDATA%\Programs\t3-code-desktop\T3 Code (Nightly).exe
  • User-data directory in child arguments: %APPDATA%\t3code
  • Observed launcher: PowerToys Run
  • Launcher process: PowerToys.PowerLauncher.exe, PID 17728

Logs or stack traces

Process snapshot:
2026-07-23 14:56:07.636 -05:00

PowerToys.PowerLauncher.exe
PID 17728
Started 2026-07-21 17:01:32.039 -05:00
│
├─ T3 Code (Nightly).exe
│  PID 29556
│  Started 2026-07-22 14:18:58.027 -05:00
│  MainWindowHandle=0
│  WorkingSet=57.4 MiB
│
│  ├─ PID 5660
│  │  Started 2026-07-22 14:19:25.071 -05:00
│  │  --type=gpu-process
│  │  WorkingSet=51.5 MiB
│  │
│  └─ PID 35060
│     Started 2026-07-22 14:19:25.092 -05:00
│     --type=utility
│     --utility-sub-type=network.mojom.NetworkService
│     WorkingSet=43.4 MiB
│
│  Tree total: approximately 152.3 MiB
│
└─ T3 Code (Nightly).exe
   PID 23264
   Started 2026-07-22 14:19:28.614 -05:00
   MainWindowHandle=0
   WorkingSet=64.4 MiB

   ├─ PID 35752
   │  Started 2026-07-22 14:19:53.366 -05:00
   │  --type=gpu-process
   │  WorkingSet=59.2 MiB
   │
   └─ PID 25728
      Started 2026-07-22 14:19:53.384 -05:00
      --type=utility
      --utility-sub-type=network.mojom.NetworkService
      WorkingSet=49.5 MiB

   Tree total: approximately 173.1 MiB

Combined working set: approximately 325.4 MiB

No TCP rows were returned by `netstat -ano -p tcp` for:
29556, 5660, 35060, 23264, 35752, or 25728.

A healthy visible instance had renderer children and an embedded
`apps/server/dist/bin.mjs --bootstrap-fd 3` backend child.
Neither stale tree contained those process types.

Relevant update trace from %USERPROFILE%\.t3\userdata\logs\desktop.trace.ndjson.4:

traceId=3e87348ea1cf0f7487cd3ddbb9219503

2026-07-22 14:14:09.631 -05:00
desktop.updates.installDownloadedUpdate
duration=2918.812 ms
status=Success

2026-07-22 14:14:09.631 -05:00
desktop.updates.install
duration=2919.026 ms
status=Success

2026-07-22 14:14:10.279 -05:00
desktop.lifecycle.windowAllClosed
duration=0.235 ms
status=Success

A separate successful startup trace confirms the startup ordering. It cannot be correlated to either stale PID because current trace records do not include the process PID:

traceId=7f5ac4dcb7a82c8ef0d6a4089a78715c
runId=2d7ccd59aca8
startup=2026-07-22 14:20:40.533 -05:00

desktop.shellEnvironment.installIntoProcess     7179.908 ms
├─ first Windows environment read              2584.700 ms
└─ second Windows environment read             4593.264 ms

desktop.appIdentity.resolveUserDataPath            1.514 ms
desktop.appIdentity.configure                      1.957 ms
desktop.lifecycle.register                         5.057 ms
desktop.clerk.configure                             2.586 ms
desktop.electron.whenReady                          0.253 ms

Current telemetry does not explicitly record:

- Startup PID and parent PID
- Single-instance lock accepted/rejected
- Entry into the rejected-instance exit path
- Whether DesktopShutdown.awaitComplete resolved
- Whether the second app.quit() executed
- Final will-quit or process-exit completion

Screenshots, recordings, or supporting files

The relevant sanitized process and trace excerpts are included above.

The complete trace is not attached because it contains unrelated local paths and endpoint information. A redacted Process Explorer screenshot or process snapshot can be added if needed.

Workaround

Restarting Windows removes the stale process trees.

They can also be removed manually by ending only the verified headless root trees in Process Explorer or Task Manager. PIDs must be rechecked first to avoid terminating the active application instance.

No prevention is currently known other than avoiding repeated launches while an update or relaunch appears to still be in progress.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions