Skip to content

bug(windows): tray icon exits after 3s and fails to launch detached host (ocx tray install) #2804

Closed

Description

@benedictusrey

Client or integration

Codex CLI

Area

CLI

Summary

When running ocx tray install on Windows:

  1. The command reports Windows tray: installed and running.
  2. Within ~3 seconds, the tray process silently terminates, and subsequent ocx tray status calls report Windows tray: installed, not currently running.
  3. In the Windows taskbar, the icon remains as a "ghost" that instantly disappears upon right-click or hover.

Expected Behavior

ocx tray install should launch a persistent background tray process that remains running and displays the context menu on right-click.


Root Causes

  1. Environment Dropped in Detached Host (src/tray/windows.ts):
    DETACHED_TRAY_HOST_LAUNCHER uses $startInfo.UseShellExecute = $true. In .NET, UseShellExecute = $true invokes ShellExecuteEx, discarding process environment variables (OCX_TRAY_ENTRY_B64). When bun src/cli/index.ts __tray-host launches without it, parseTrayHostEntry() throws Missing tray host entry and exits immediately.

  2. Leftover Stop Signal Consumed on First Timer Tick (src/tray/windows-tray.ps1):
    signalTrayStop() sets the named event Local\OpenCodexTrayStop-<hash>. When the new tray starts in -Mode Run, it never resets this event. On the first 3000ms timer tick, if ($stopEvent.WaitOne(0)) detects the leftover signal and calls [System.Windows.Forms.Application]::Exit().

  3. WinForms Context Menu Styling & Non-Guarded I/O (src/tray/windows-tray.ps1):
    [System.Windows.Forms.Application]::EnableVisualStyles() is missing, causing ContextMenuStrip rendering issues. Additionally, $ErrorActionPreference = "Stop" causes minor I/O locks on tray-heartbeat.json to abort the UI thread.


Proposed Fix

1. Direct Launch via Native VBS Host in src/tray/windows.ts

Launch opencodex-tray.vbs via wscript.exe directly in spawnTray, matching the exact detached execution model used by Windows boot:

function spawnTray(state: WindowsTrayEntry): void {
  const launcher = installedTrayLauncherPath();
  if (existsSync(launcher)) {
    const wscript = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
    execFileSync(wscript, ["//B", "//NoLogo", launcher], {
      stdio: "ignore",
      windowsHide: true,
      timeout: 15_000,
    });
    return;
  }
  launchWindowsTrayHost(state);
}

If retaining DETACHED_TRAY_HOST_LAUNCHER as a fallback, ensure environment inheritance is preserved:

const DETACHED_TRAY_HOST_LAUNCHER = [
  "$startInfo = New-Object System.Diagnostics.ProcessStartInfo",
  "$startInfo.FileName = $env:OCX_TRAY_HOST_BUN",
  "$startInfo.Arguments = $env:OCX_TRAY_HOST_ARGS",
  "$startInfo.UseShellExecute = $false",
  "$startInfo.CreateNoWindow = $true",
  "$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden",
  "$startInfo.EnvironmentVariables['OCX_TRAY_ENTRY_B64'] = $env:OCX_TRAY_ENTRY_B64",
  "$child = [System.Diagnostics.Process]::Start($startInfo)",
  "if ($null -eq $child) { throw 'Windows tray host did not start.' }",
  "$child.Dispose()",
].join("; ");

2. Reset Stop Signal & Guard Timer in src/tray/windows-tray.ps1

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
try { [System.Windows.Forms.Application]::EnableVisualStyles() } catch { }

# Reset stop event on startup in Run mode
$stopEvent = New-Object System.Threading.EventWaitHandle($false, [System.Threading.EventResetMode]::AutoReset, "Local\OpenCodexTrayStop-$stableHash", [ref]$stopEventCreated)
if ($Mode -eq "Stop") {
  [void]$stopEvent.Set()
  $stopEvent.Dispose()
  exit 0
} else {
  [void]$stopEvent.Reset()
}

# Guard the timer tick
$timer.add_Tick({
  try {
    if ($stopEvent.WaitOne(0)) {
      [System.Windows.Forms.Application]::Exit()
      return
    }
    Update-TrayState
  } catch {
    Write-ActionLog "timer tick failed: $($_.Exception.Message)"
  }
})

Reproduction

  1. On a Windows machine, run: ocx tray install
  2. Observe immediate output: Windows tray: installed and running
  3. Wait 5-10 seconds and run: ocx tray status
  4. Notice status reports: Windows tray: installed, not currently running
  5. Observe taskbar tray icon: moving the cursor over or right-clicking the icon causes it to disappear instantly.

Version

2.34.0

Operating system

Windows 11 (x64)

Provider and model

N/A (OS-level CLI/tray packaging issue)

Logs or error output

PS C:\Users\Alpha> ocx tray install
Windows tray: installed and running

PS C:\Users\Alpha> ocx tray status
Windows tray: installed and running

PS C:\Users\Alpha> ocx tray status
Windows tray: installed, not currently running

Screenshots and supporting files

No response

Redacted configuration

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    cliCLI, config inject, packaging flags
    guiDashboard, tray, settings UI
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    on Aug 28, 2026
  2. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 70 / 80

    이슈는 Windows에서 ocx tray install이 “installed and running”이라고 말한 뒤 약 3초 만에 트레이가 죽고, ocx tray status는 not running, 작업표시줄에는 ghost 아이콘만 남는다는 보고다. 작성자가 적은 원인 두 개는 지금 dev(fe063d1) 코드와 맞춰 보면 설득력 있다.

    첫째, src/tray/windows.ts의 DETACHED_TRAY_HOST_LAUNCHER(대략 505–514행)는 $startInfo.UseShellExecute = $true로 Bun __tray-host를 띄운다. .NET에서 UseShellExecute=true면 부모 env가 child에 안 넘어가는 경우가 있어, 같은 함수가 env에 넣는 OCX_TRAY_ENTRY_B64가 host에 도달하지 않으면 parseTrayHostEntry()(544행 근처)가 Missing tray host entry로 바로 죽는다. 같은 폴더의 windows-tray.ps1 Start-OcxCommand는 이미 UseShellExecute = $false로 env를 복사한다. 둘째, windows-tray.ps1은 named event Local\OpenCodexTrayStop-<hash>를 만들고 Timer 간격이 3000ms다. install/reinstall 경로의 signalTrayStop()이 남긴 signaled 상태를 첫 Tick이 소비하면, 방금 띄운 트레이가 3초 만에 Exit한다. ghost 아이콘은 WinForms NotifyIcon이 비정상 종료될 때 흔히 보인다.

    플랫폼/GUI/CLI 라벨이 붙어 있고, 최근 Windows multi-home admit(#2800)과도 같은 Windows 운영 축이다. 수정 PR이 아직 없고, RCA가 파일·라인까지 짚혀 있어서 help-wanted로 열기 좋다. Preview 배포나 types/config 스플릿과는 무관하다.

    라인 src/tray/windows.ts ~509 (UseShellExecute = $true) - detached host 런처가 entry env를 버리도록 설계된 자리. $false + EnvironmentVariables 복사, 또는 entry를 파일/인자로 넘기는 쪽으로 바꿔야 한다.
    라인 src/tray/windows.ts ~544-548 (parseTrayHostEntry) - env 없으면 즉시 throw. host가 조용히 죽어서 install이 성공처럼 보이는 원인.
    라인 src/tray/windows-tray.ps1 ~57 / ~332-337 (stop event + 3s timer) - 이전 stop signal이 AutoReset으로 남아 있으면 첫 Tick에 Exit. install 직전 WaitOne으로 drain하거나 ManualReset+명시 Clear가 필요하다.
    경로 ocx tray install 성공 메시지 - host/ps1이 살아 있는지 heartbeat를 확인하기 전에 “running”을 말하는 흐름이면 거짓 성공이다.
    경로 재현 - Windows 실기기에서 install → 3초 대기 → status/ghost 아이콘을 고정 재현 절차로 남겨야 한다.

    메인테이너의 판단이 필요한 지점

    • env 전달 수정과 stop-event drain을 한 PR에 묶을지, 두 커밋/두 PR로 나눌지.
    • install이 heartbeat 확인 전까지 실패로 보고하게 할지(행동 변경).
    • help wanted / good first issue를 붙일지(RCA가 이미 상세함).

    너의 추천
    RCA가 HEAD와 일치하니 버그로 유지하고, UseShellExecute=$false(또는 entry 파일 전달) + install 경로에서 stop event drain + (가능하면) heartbeat 확인 후 running 표시를 한 PR로 받아라. 우선순위 높은 Windows 안정성 이슈다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    Fixed on dev by commit 42b88dc (PR #2856).

    The 3-second exit had a concrete cause: the detached fallback launch path went through UseShellExecute = true, which dropped the OCX_TRAY_ENTRY_B64 environment variable. The tray host then started, found no entry, threw Missing tray host entry, and died — which is exactly the "exits after 3s and fails to launch detached host" you saw.

    Two changes: the launch path now passes the entry through correctly instead of losing it to the shell, and the PowerShell side only resets stale stop state after it has singleton ownership, with the 3-second startup timer guarded so a slow start is not mistaken for a failed one.

    Verified with tests/windows-tray.test.ts — 21 pass, 138 assertions — driven RED against the missing launch contract, plus a mutation check (shortening the timeout to 1s makes it fail, so the assertion is real). tsc and privacy:scan clean.

    An honest caveat: no real Windows 11 machine was available here, so this is verified by asserting the spawn contract and options rather than by an end-to-end tray launch. If you can run ocx tray install from a dev build and report back, that would close the loop properly.

    This PR targeted dev, so GitHub did not auto-close the issue — closing manually.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI, config inject, packaging flagsguiDashboard, tray, settings UIplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions