Repository navigation
fix(switch): add explicit daemon restart and stale-auth recovery - #164
Conversation
Probe a responding daemon through the supported version command and restart only on an explicit request. Bound output collection and process exit, including helpers that close their pipes before exiting; terminate and reap only our child on timeout. Use the selected CODEX_HOME and prefer its managed executable. Keep absent and indeterminate states distinct, accepting the supported lifecycle detection/restart race. Motivated by codejunkienick’s PR Loongphy#163 (Loongphy#163); implements its daemon coordination idea with an explicit contract and independent code. No persistent configuration or registry schema changes.
Add --restart-daemon to one-shot query, picker, previous-account and JSON paths at their shared activation boundary. Persist account and registry state before daemon coordination; explicit requests include unchanged-file recovery and retry. Preserve the existing JSON stdout document and put lifecycle diagnostics and quoted home-specific recovery commands on stderr. Requested restart failures return the existing operational exit code 1. Reject the option with --live and retain other activation behavior. Cover selected-home propagation, persistence ordering, unchanged selection, previous and multi-match routes, failed selection, absent and indeterminate daemons, JSON failures and retry. Native suite: 446 passed, 5 skipped; required isolated list smoke and Windows/macOS cross-builds passed. Related: Loongphy#162 and codejunkienick’s Loongphy#163. This opts into disruptive restarts without adding persistent config or changing the registry schema.
Use the cwd-relative canonicalization API for PATH candidates, which can be relative. The absolute-only API asserts on a valid relative PATH entry when managed Codex layouts are unavailable. Add an isolated CLI regression that removes the managed executable, finds the lifecycle helper through a relative PATH directory, and verifies restart and selected-home propagation. Linux suite: 447 passed, 5 skipped.
Merge fix/daemon-account-switch without rewriting published history. Update delivery notes to the final 5baaa35 candidate, 447 passing tests, and upstream PR Loongphy#164. Keep local delivery documentation and publishing guards out of the upstream-facing branch.
@loongphy/codex-auth-darwin-arm64
@loongphy/codex-auth-darwin-x64
@loongphy/codex-auth-linux-arm64
@loongphy/codex-auth-linux-x64
@loongphy/codex-auth-win32-arm64
@loongphy/codex-auth-win32-x64
@loongphy/codex-auth
commit: |
Check executable access for Unix managed and PATH candidates so a non-executable regular file cannot mask a later usable Codex binary. Retain the regular native executable check on Windows. Extend the relative-PATH regression to ./fallback and empty PATH entries, and add an isolated CLI test with a non-executable decoy before the valid helper. No new resolver abstraction. Linux suite: 448 passed, 5 skipped; required isolated list smoke passed.
There was a problem hiding this comment.
Thanks — this is a real problem and I like the contract here: explicit opt-in, no schema changes, exit 1 with JSON preserved on stdout and diagnostics on stderr. I verified the mechanism against upstream OpenAI codex (app-server daemon {version,restart} subcommands, the $CODEX_HOME/app-server-control/app-server-control.sock path, and the LifecycleOutput JSON shape all match), and the full test suite passes on this branch (zig 0.16).
One requested change: make the mechanism two-tier, because our Codex fork already ships a purpose-built, non-disruptive command for exactly this flow.
codex app-server daemon reload-auth exists in our fork (tagged [codext]; it landed there on 2026-10-11, after this PR was opened, so it's not in your base). Its doc comment states the intended use verbatim:
External account switchers (codex-auth) run this after replacing auth.json so daemon-hosted sessions pick up the new account without restarting the daemon.
reload-auth makes the running daemon re-read auth.json in place: sessions stay alive, no client reconnection, and the daemon skips it while a turn is running (SkippedBusy). It returns Unsupported when pointed at a non-codext daemon, which gives you a clean capability probe for the fallback.
Suggested behavior — keep the --restart-daemon flag name and everything around it, change what happens underneath:
- Keep your probe as-is (socket stat →
daemon version;.not_runningshort-circuits before any spawn). - If the daemon is running, first attempt
codex app-server daemon reload-authwith its own short timeout (suggest ~10s, not the 120s restart budget), so a hung daemon fails fast.- Reload succeeds → done: report success without restarting (add an
Outcomevariant, e.g..reloaded, and a matching diagnostic line). This is the common case on codex. Unsupported(official daemon), spawn failure, or timeout → fall back tocodex app-server daemon restartexactly as implemented today.
- Reload succeeds → done: report success without restarting (add an
SkippedBusy(daemon deferred because a turn is running): treat as deferred, exit 0, and do not restart — restarting would kill the very turn that made the daemon busy. The diagnostic should tell the user the switch will be picked up once the turn finishes (retryswitch --restart-daemonlater).- Exit 1 only when both reload and restart failed/were impossible; stdout JSON semantics unchanged.
Why this shape:
- Official codex is unaffected: on an upstream daemon the first tier returns
Unsupportedand you fall through to your existing restart path — so this PR remains the correct (and only) mechanism there. - On codex it turns a disruptive operation into a seamless one: no killed turns, no client reconnection, for the single most common case (switch account, keep working).
Latency: no added cost on a plain switch (the two-tier only applies when --restart-daemon is passed). With the flag, the added cost is one bounded UDS subprocess (~100 ms typical for CLI startup + roundtrip) — negligible next to a restart, which takes seconds plus client reconnection on the codex side, and which the reload tier avoids entirely when supported.
Happy to review the follow-up push. The maintainer-side implementation is also prototyped in #172 (draft, stacked on this branch: two-tier ensure/tryReload + reload-auth fake scenarios) — either land the same shape here or merge #172 after this PR; both paths work.
Two-tier --restart-daemon coordination, per the maintainer review: - ensure() probes the daemon, then attempts 'app-server daemon reload-auth' (codex daemons) with a 10s budget before falling back to the full restart (upstream daemons without reload support) - reload outcomes: .reloaded (credentials applied in place, no restart) and .deferred (daemon busy with a running turn — deferred, exit 0, rerun later); reload unavailability or failure falls through to the existing restart path - diagnostics for both new outcomes; docs updated - fake lifecycle script gains reload-auth variants; client + integration expectations updated for the version → reload-auth → restart sequence [skip ci]
Loongphy
left a comment
There was a problem hiding this comment.
Verification passed on the updated head (afded66) — switching this review to approved.
Autoreview (codex, independent pass): overall: patch is correct (0.98) — no accepted/actionable findings in scope; "adds a daemon auth reload attempt with restart fallback and a busy-turn deferral. No P0 defect ... is evident."
Local validation:
zig build -Doptimize=ReleaseFastclean; fullzig build testexit 0 (including the three new reload scenarios and the updated integration sequences).- E2E with a fake codex daemon:
switch beta --restart-daemonnow runsversion → reload-auth, reports "the running daemon reloaded the new credentials without a restart", and does not restart (no session interruption); the reload-unsupported path still falls back torestart, and the busy path defers with exit 0.
Good work on the quick turnaround — merging when you're ready.
With Codex 0.160.0,
switchcan successfully select account A inauth.jsonand the registry while the shared app-server daemon continues using account B. Newly opened clients can attach to that same daemon. This was confirmed through the supportedaccount/readrequest withrefreshToken: false.This adds
switch <query> --restart-daemonto persist the selection and then restart a responsive daemon through Codex's supported lifecycle command. Explicit requests also work when the selected files are unchanged, so users can repair stale daemon authentication or retry a failed restart.Why another PR alongside #163?
codejunkienick's #163 correctly identifies the daemon issue and proposes the same supported restart mechanism. This offers a different user contract and hardens the lifecycle operation:
--restart-daemon; no registry/schema changedaemon versionresponseThese differences warrant review as an alternative implementation. They can also inform improvements to #163; this PR does not assume how maintainers will reconcile the two.
Behavior and scope
--live --restart-daemonis rejected; live switching retains its existing behavior.Validation
zig build run -- list, formatting, and whitespace checks passed.0836af1are green.CODEX_HOME, responsive/absent/indeterminate probes, unchanged selection, failed restart and retry, JSON stdout plus stderr/exit 1, picker/previous routes, relative/emptyPATHentries and skipped non-executable Unix candidates, and timeout cleanup with both open and closed output pipes.Real Linux acceptance with Codex 0.160.0 passed: disk already selected A while the daemon reported B; explicit unchanged-selection restart restored A. Selecting A without the option kept the same daemon PID. Subsequent A→B→A switches changed daemon PIDs and returned the expected identities through supported
account/readrequests withrefreshToken: false. Configuration/history checks passed, and the candidate was selected for local use. The existing active chat briefly displayed “Reconnecting to server…”, reconnected using the newly selected account, and continued in the same session without manual exit/resume.Related: #162, #163.