Repository navigation
scripts: add Windows Codex desktop full-restart helper #2293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # 000 Plan: Windows model-picker full-restart path | ||
|
|
||
| ## Problem | ||
|
|
||
| ocx sync --restart-codex rewrites the Codex catalog JSON and restarts the Codex | ||
| app-server (codex.exe app-server). Observed behavior: | ||
|
|
||
| - macOS: the desktop app model picker reflects the new catalog right away. | ||
| - Windows (stable/beta, MSIX package OpenAI.Codex_26.818.3698.0): the picker | ||
| keeps the stale list until the whole desktop app is quit and relaunched. | ||
|
|
||
| Local evidence (2026-08-21): | ||
|
|
||
| - Desktop UI processes are ChatGPT.exe (Electron shell), installed as MSIX | ||
| package family OpenAI.Codex_2p2nqsd0c76g0, start app id (AUMID) | ||
| OpenAI.Codex_2p2nqsd0c76g0!App. | ||
| - ocx sync --restart-codex matches only codex.exe app-server and | ||
| codex-code-mode-host.exe command lines | ||
| (src/codex/app-server-processes.ts, isCodexAppServerCommandLine). The | ||
| Electron UI is never signalled, so its cached picker survives. | ||
| - After the 20:57 sync + restart, codex.exe (PID 8592) started fresh at 20:59 | ||
| while all ChatGPT.exe UI processes kept their earlier start time, and the | ||
| picker still showed only OpenAI models. | ||
|
|
||
| Research findings (subagent, bundle inspection of app.asar): | ||
|
|
||
| - The renderer fetches model/list and config/read over stdio JSON-RPC into a | ||
| TanStack Query cache; there is no filesystem watcher on the catalog file. | ||
| - The UI invalidates those queries only on a codex-app-server-initialized | ||
| event. On Windows, externally killing the codex.exe child may not produce | ||
| that event reliably (hypothesis, untested from inside this session), which | ||
| would explain why ocx restart alone does not refresh the picker here while | ||
| macOS recovers. | ||
| - Official docs say to restart the desktop app after changing model_catalog_json; | ||
| no supported refresh hook exists. Known upstream cluster: openai/codex | ||
| issues 19694, 26308, 32349, 34487 (desktop picker vs CLI catalog divergence). | ||
| - Relaunch must go through MSIX activation (shell:AppsFolder AUMID), not the | ||
| exe path under WindowsApps (ACL-restricted, no package identity). | ||
|
|
||
| ## Scope | ||
|
|
||
| IN (audit amendments folded in): | ||
|
|
||
| - A supported, documented way to fully restart the Windows Codex desktop app | ||
| after a catalog sync: graceful WM_CLOSE first, bounded taskkill /T /F | ||
| fallback, relaunch via AUMID. Targets resolve InstallLocation at runtime | ||
| via Get-AppxPackage -PackageFamilyName (the family string is NOT a | ||
| substring of the install path); only the root ChatGPT.exe whose parent lies | ||
| outside the package is selected so taskkill /T cascades to codex.exe and | ||
| codex-code-mode-host.exe; the script refuses to kill its own ancestry. | ||
| - A GitHub issue on lidge-jun/opencodex recording the platform gap, the beta | ||
| caveat, upstream issue links, and the requested UX (sync should offer a full | ||
| app restart on Windows). The issue MUST include Version (installed | ||
| @bitkyc08/opencodex version) and Operating system fields, which | ||
| enforce-issue-quality hard-requires once Client or integration is present; | ||
| Reproduction carries the PID/start-time evidence; upstream issues are cited | ||
| as related-but-unverified. | ||
|
|
||
| OUT: | ||
|
|
||
| - Changing ocx sync runtime behavior in this unit (the issue proposes it; | ||
| implementation is a later unit). | ||
| - Killing processes outside the OpenAI.Codex_2p2nqsd0c76g0 package family. | ||
| - Testing the unverified stdio-respawn hypothesis by killing codex.exe from | ||
| inside this session (would kill our own host); recorded as an open question | ||
| for an external terminal test. | ||
|
|
||
| ## Work phases | ||
|
|
||
| - wp1 (010): add scripts/restart-codex-desktop-app.ps1 with -DryRun/-Force, | ||
| graceful-close then bounded forced fallback, relaunch via AUMID; file the | ||
| templated GitHub issue; record evidence. | ||
|
|
||
| ## Accept criteria | ||
|
|
||
| - Script -DryRun exits 0 AND lists the specific live root PID(s) it would | ||
| stop and the relaunch command, without stopping anything (an exit-0 no-op | ||
| does not pass). Focused probe evidence per scripts/AGENTS.md is the real | ||
| gate (tsconfig includes only src/); bun x tsc --noEmit still runs as a | ||
| no-regression check. | ||
| - Issue exists on origin with bug_report template headings. | ||
|
|
||
| ## Safety notes | ||
|
|
||
| - Running the restart from inside a Codex conversation kills that conversation | ||
| host app; the script warns and docs say to run it from an external terminal. | ||
| - Forced kill is limited to processes whose Path is under the runtime-resolved | ||
| InstallLocation. Close-to-tray behavior is explicitly checked: if | ||
| CloseMainWindow() only hides the window, the wait expires and the forced | ||
| path runs; record observed behavior. Record the PowerShell edition the | ||
| probe ran under (Get-AppxPackage differs between 5.1 and 7). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # 010 wp1: Restart script + issue (diff level) | ||
|
|
||
| ## NEW: scripts/restart-codex-desktop-app.ps1 (amended per audit) | ||
|
|
||
| PowerShell 5.1-compatible script: | ||
|
|
||
| - param([switch]$DryRun, [switch]$Force). | ||
| - Constants: package family OpenAI.Codex_2p2nqsd0c76g0, AUMID | ||
| OpenAI.Codex_2p2nqsd0c76g0!App, process names ChatGPT, codex, | ||
| codex-code-mode-host. | ||
| - Resolve $installLoc = (Get-AppxPackage -PackageFamilyName | ||
| OpenAI.Codex_2p2nqsd0c76g0).InstallLocation at runtime; fail with an | ||
| actionable message when empty. Wrap process Path access in try/catch | ||
| (Access denied for other users processes). | ||
| - Select ONLY the root ChatGPT.exe whose ParentProcessId lies outside | ||
| $installLoc (Win32_Process via Get-CimInstance). taskkill /PID <root> /T /F | ||
| cascades to codex.exe and its codex-code-mode-host.exe child. Never list | ||
| code-mode-host as an independent target. | ||
| - Self-kill guard: walk $PID ancestry; abort with a clear message when any | ||
| selected target is in it. | ||
| - Warn: active Codex turns are interrupted; run from an external terminal. | ||
| - Graceful pass: CloseMainWindow() on the process with a MainWindowHandle, | ||
| wait up to 15 s in 1 s polls for all targets to exit. If the process | ||
| survives past the timeout, print that close-to-tray behavior is suspected | ||
| before escalating. | ||
| - Forced pass (remaining targets, or immediately with -Force): | ||
| taskkill /PID <id> /T /F per remaining PID (/T covers child tree so | ||
| codex.exe is not orphaned). | ||
| - Relaunch: Start-Process "shell:AppsFolder\<AUMID>" unless -DryRun. | ||
| - -DryRun: print planned actions (targets, method, relaunch command), touch | ||
| nothing, exit 0. | ||
|
|
||
| ## MODIFY: none (runtime untouched in this unit) | ||
|
|
||
| ## Verification | ||
|
|
||
| ## Cycle 2 addendum (2026-08-21, provider verification + push) | ||
|
|
||
| - command-code stealth/ox-alpha re-probed after credit purchase: /v1/chat/completions | ||
| and /v1/responses both return 200 with valid completions. No code change needed. | ||
| - opencode-go upstream (https://opencode.ai/zen/go/v1) returns 500 Internal server | ||
| error for every model probed directly (kimi-k2.7-code, ox-alpha-free); the proxy | ||
| 502 "upstream stream ended" is an upstream outage, not an adapter defect. | ||
| ox-alpha-free is also absent from models.dev opencode-go roster and from | ||
| scripts/model-metadata.source.json, so the opencode-go/ox-alpha-free slug was | ||
| never a registered catalog model; opencode-free/x-preview-f-free is the working | ||
| free-tier route (verified 200 on both endpoints). | ||
| - Direct push to origin/dev rejected by ruleset 20763889 (pull_request rule, admin | ||
| bypass = pull_requests_only). Fallback per user intent: branch | ||
| codex/windows-restart-helper pushed, PR #2293 opened targeting dev (MERGEABLE). | ||
|
|
||
| - powershell -File scripts/restart-codex-desktop-app.ps1 -DryRun -> exit 0 | ||
| AND output names the live root PID (e.g. 9928) and its child codex.exe; | ||
| nothing stopped. Record $PSVersionTable.PSVersion. | ||
| - bun x tsc --noEmit -> exit 0. | ||
| - gh issue create with bug_report.yml headings: Client or integration = Codex | ||
| App; Area = Platform (Windows / macOS / Linux); Version = installed | ||
| @bitkyc08/opencodex version (package.json); Operating system = Windows 11 | ||
| (build from systeminfo); Reproduction includes the 20:57 sync / 20:59 fresh | ||
| codex.exe vs stale UI start-time evidence; upstream issues | ||
| 19694/26308/32349/34487 cited as related-unverified; beta-channel caveat | ||
| stated. After creation, re-read state with gh issue view until the | ||
| enforce-issue-quality workflow settles (creation alone can auto-close). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| #Requires -Version 5.1 | ||
| <# | ||
| .SYNOPSIS | ||
| Fully restarts the Windows Codex desktop app (MSIX package) so the model | ||
| picker re-reads the on-disk catalog after ocx sync. | ||
| .NOTES | ||
| Run this from an external terminal. Running it from inside a Codex | ||
| conversation kills the app hosting that conversation. | ||
| #> | ||
| [CmdletBinding()] | ||
| param( | ||
| [switch]$DryRun, | ||
| [switch]$Force | ||
| ) | ||
|
|
||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| $PackageFamily = "OpenAI.Codex_2p2nqsd0c76g0" | ||
| $Aumid = "OpenAI.Codex_2p2nqsd0c76g0!App" | ||
|
|
||
| Import-Module Appx -ErrorAction SilentlyContinue | ||
| $pkg = Get-AppxPackage -Name OpenAI.Codex | Where-Object { $_.PackageFamilyName -eq $PackageFamily } | ||
| if (-not $pkg -or -not $pkg.InstallLocation) { | ||
| Write-Error "MSIX package $PackageFamily was not found; nothing to restart." | ||
| exit 1 | ||
| } | ||
| $InstallLoc = $pkg.InstallLocation | ||
|
|
||
| $nameFilter = "Name='ChatGPT.exe' OR Name='codex.exe' OR Name='codex-code-mode-host.exe'" | ||
| $all = @(Get-CimInstance -ClassName Win32_Process -Filter $nameFilter) | ||
| $targets = @($all | Where-Object { | ||
| $_.ExecutablePath -and $_.ExecutablePath.StartsWith($InstallLoc, [System.StringComparison]::OrdinalIgnoreCase) | ||
| }) | ||
|
|
||
| if ($targets.Count -eq 0) { | ||
| Write-Host "Codex desktop app is not running." | ||
| exit 0 | ||
| } | ||
|
|
||
| $targetIds = @{} | ||
| foreach ($t in $targets) { $targetIds[[uint32]$t.ProcessId] = $t } | ||
|
|
||
| # Roots are targets whose parent is outside the package tree; killing each | ||
| # root with taskkill /T cascades to codex.exe and its code-mode-host child. | ||
| $roots = @($targets | Where-Object { -not $targetIds.ContainsKey([uint32]$_.ParentProcessId) }) | ||
|
Comment on lines
+43
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Select only a verified Lines 43-45 classify any listed process as a root when its parent is not in A package-owned parent with another executable name makes its child appear to be a root. An independently started Filter roots to 🤖 Prompt for AI Agents |
||
|
|
||
| # Self-kill guard: never target our own ancestry. Skipped under -DryRun so the | ||
| # report stays useful when Codex itself launched this script. | ||
| $ancestry = @{} | ||
| if (-not $DryRun) { | ||
| $cursor = $PID | ||
| while ($cursor) { | ||
| $ancestry[[uint32]$cursor] = $true | ||
| $parent = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$cursor").ParentProcessId | ||
| if ($parent -and -not $ancestry.ContainsKey([uint32]$parent)) { $cursor = $parent } else { break } | ||
| } | ||
| foreach ($r in $roots) { | ||
| if ($ancestry.ContainsKey([uint32]$r.ProcessId)) { | ||
| Write-Error "Refusing to restart: selected root PID $($r.ProcessId) is an ancestor of this script." | ||
| exit 1 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Write-Host ("Targets ({0}):" -f $targets.Count) | ||
| foreach ($t in $targets) { | ||
| Write-Host (" PID {0} {1} parent={2}" -f $t.ProcessId, $t.Name, $t.ParentProcessId) | ||
| } | ||
| Write-Host ("Root(s) to stop: {0}" -f (($roots | ForEach-Object { $_.ProcessId }) -join ", ")) | ||
| Write-Host ('Relaunch command: Start-Process "shell:AppsFolder\{0}"' -f $Aumid) | ||
|
|
||
| if ($DryRun) { | ||
| Write-Host "Dry run: nothing was stopped or launched." | ||
| exit 0 | ||
| } | ||
|
|
||
| foreach ($r in $roots) { | ||
| $rootPid = [uint32]$r.ProcessId | ||
| $stopped = $false | ||
| if (-not $Force) { | ||
| $proc = Get-Process -Id $rootPid -ErrorAction SilentlyContinue | ||
| if ($proc -and $proc.MainWindowHandle -ne 0) { | ||
| Write-Host "Sending graceful close to PID $rootPid..." | ||
| [void]$proc.CloseMainWindow() | ||
| for ($i = 0; $i -lt 15; $i++) { | ||
| Start-Sleep -Seconds 1 | ||
| if (-not (Get-Process -Id $rootPid -ErrorAction SilentlyContinue)) { $stopped = $true; break } | ||
| } | ||
| if (-not $stopped) { | ||
| Write-Host "PID $rootPid survived graceful close (close-to-tray suspected); forcing." | ||
| } | ||
| } | ||
| } | ||
| if (-not $stopped) { | ||
| Write-Host "Force-stopping process tree at PID $rootPid..." | ||
| & "$env:SystemRoot\System32\taskkill.exe" /PID $rootPid /T /F | Out-Null | ||
| } | ||
| } | ||
|
|
||
| Start-Sleep -Seconds 1 | ||
| Start-Process "shell:AppsFolder\$Aumid" | ||
|
Comment on lines
+85
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Confirm that every package target exited before relaunch. Line 87 sets Line 96 also discards After the graceful pass, re-check every original target PID. Force-stop surviving package targets, check 🤖 Prompt for AI Agents |
||
| Write-Host "Codex desktop app restarted." | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Record the actual PowerShell version.
Line 54 asks to record
$PSVersionTable.PSVersion, but the verification entry contains no recorded value or host name. Add the exact version and executable used, such aspowershell.exeversuspwsh, to prove the PowerShell 5.1 compatibility check.This assessment uses the PowerShell compatibility requirement in the supplied implementation and the verification text in this plan.
🤖 Prompt for AI Agents