Skip to content

[Bug]: Service fails to auto-start after ocx update due to legacy launchctl unload and strict launchctlLoadFailed check #4141

Closed

Description

@jaychou0642-create

Client or integration

Codex CLI

Area

Service lifecycle

Summary

After updating OpenCodex (e.g. ocx update to v2.48.0 and v2.49.0) on macOS, the background service fails to automatically restart, leaving the proxy inactive. The operator has to manually run ocx start each time.

Root cause analysis:

  1. Legacy launchctl unload <plist> in src/service.ts:installLaunchd() fails to unregister the service from the modern macOS gui/<uid> domain.
  2. Subsequent launchctl load -w <plist> emits Load failed: 5: Input/output error to stderr because the job is still bootstrapped.
  3. The strict launchctlLoadFailed check (/\b(?:Load|Bootstrap) failed\b/i.test(stderr)) catches this stderr output and throws an uncaught error, aborting ocx service repair with exit code 1.
  4. The update fallback startProxyDirectly() fails to bind or exits due to the existing process/lock.
  5. In addition, statusLaunchd() uses un-scoped launchctl list | grep ${LABEL}, which returns empty in modern macOS subshells, leading ocx status to falsely report installed, not loaded (launchd).

Reproduction

  1. On macOS, install and register the background service: ocx service install.
  2. Run ocx update (or upgrade from v2.48.0 to v2.49.0).
  3. Notice:
    opencodex: service refresh failed — starting the proxy directly instead.
      Run 'ocx service repair' to see why the background service could not restart.
    
  4. Check proxy status or try to connect: no proxy is listening.
  5. Running ocx service repair manually fails with:
    launchctl could not load /Users/.../Library/LaunchAgents/com.opencodex.proxy.plist: Load failed: 5: Input/output error
    A previous job may still be bootstrapped. Try:
      launchctl bootout gui/501/com.opencodex.proxy
    then re-run 'ocx service repair'.
    

Version

2.49.0

Operating system

macOS 15.5

Provider and model

Not provider-specific

Logs or error output

launchctl could not load /Users/.../Library/LaunchAgents/com.opencodex.proxy.plist: Load failed: 5: Input/output error
A previous job may still be bootstrapped. Try:
  launchctl bootout gui/501/com.opencodex.proxy
then re-run 'ocx service repair'.

Checks

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

Activity

  1. lidge-jun commented on Sep 9, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 62 / 80

    이 이슈는 macOS에서 ocx update(예: 2.48→2.49) 뒤에 백그라운드 서비스가 자동으로 안 살아나서, 프록시가 꺼진 채로 남고 매번 ocx start를 손으로 해야 하는 문제를 말합니다. 운영자 메시지는 “service refresh failed — starting the proxy directly instead”인데, 직접 기동 fallback도 기존 프로세스/락 때문에 실패하면 완전 다운입니다. 2.49.0 안정 출시 직전·직후 사용자 체감이 큰 platform/service 버그입니다.

    현재 dev의 src/service.ts를 보면 이슈가 가리키는 축이 실제로 있습니다. launchctlLoadFailed(약 919–921행)는 stderr에 Load failed/Bootstrap failed만 있으면 실패로 보고, installLaunchd/repair 경로(약 2314·2344행 근처)가 이를 엄격히 봅니다. 레거시 launchctl unload(약 2363·2367·3550행 등)는 현대 macOS의 gui/<uid> 도메인에서 잡이 남은 채로 load -w가 Load failed: 5: Input/output error를 내며 exit 0인 경우가 있어, “이미 bootstrap된 잡”을 복구하지 못한 채 repair가 exit 1로 끝납니다. 이슈가 제안하는 launchctl bootout gui/.../com.opencodex.proxy 후 재시도가 그 구멍과 맞습니다. 추가로 statusLaunchd가 스코프 없는 launchctl list | grep에 의존하면 현대 서브셸에서 빈 결과가 나와 “installed, not loaded” 오진이 날 수 있다는 지적도, 같은 파일에 launchdJobMatchesPlist/launchdGuiDomain 같은 더 현대적인 헬퍼가 이미 있는 맥락과 겹칩니다.

    이 이슈 제목 앞에 붙은 형제 #4131은 템플릿 없이 열렸다가 “[Closed] Superseded by #4141”로 바뀌었지만 state는 아직 OPEN입니다. 정본은 이 이슈(#4141)로 두고 #4131은 닫는 편이 맞습니다. 라벨이 비어 있으니 bug/cli/platform/service 정도를 붙이면 트리아지에 도움이 됩니다. 수정 PR은 아직 안 보이므로, unload→bootout 전환·load 실패 시 bootout 후 재시도·status를 gui 도메인/print 쪽으로 맞추는 작은 PR이 이상적입니다. types.ts/config.ts 분할과 무관합니다.

    src/service.ts (launchctlLoadFailed) - Load failed stderr만으로 throw. I/O error=5 + 잔존 bootstrap과 맞물림
    src/service.ts (레거시 launchctl unload 경로) - gui 도메인 잔존 잡을 못 지움. bootout 필요
    src/service.ts (statusLaunchd / list|grep) - 오진 가능. 이미 있는 launchdGuiDomain·launchdJobMatchesPlist와 정렬 검토
    ocx update fallback startProxyDirectly - 기존 락/프로세스에 막히면 완전 다운. repair와 함께 봐야 함
    #4131 - 이 이슈로 대체. OPEN 잔존 시 close

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

    • 수정 범위를 uninstall/reinstall 전면 교체로 갈지, bootout+재bootstrap 최소 패치로 갈지
    • update 경로에서 repair 실패 시 사용자에게 bootout 한 줄을 자동으로 시도할지, 메시지만 강화할지
    • 2.49.0 안정 태그/노트에 “update 후 ocx service repair / bootout” 워크어라운드를 넣을지
    • CI/테스트: launchctl를 실제로 돌리기 어려우면 stderr 픽스처 단위 테스트로 launchctlLoadFailed·repair 분기만 고정할지

    너의 추천
    이 이슈를 정본으로 유지하고 #4131은 닫아라. help wanted로 두되, service.ts의 unload→bootout + load 실패 시 한 번 재시도 + status를 gui 도메인으로 맞추는 작은 PR을 받는 쪽이 좋다. 2.49 출시 노트에 워크어라운드 한 줄을 넣는 것도 검토하라. 중복·분할 캠페인 무관이라 close-don't-rebase 대상이 아니다.

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

  2. added
    bugSomething isn't working
    cliCLI, config inject, packaging flags
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Sep 9, 2026
  3. lidge-jun commented on Sep 10, 2026

    @lidge-jun
    Owner

    Fixed on dev by #4164, merged as 95a3f6a59.

    ocx service repair — which is what ocx update runs after replacing the binary — now recovers from the exact condition that was breaking it. Modern launchd writes Load failed: 5: Input/output error and exits 0 when a job is already bootstrapped in gui/<uid>, so the one case that most needed repairing was the one installLaunchd could not repair. It now boots the gui-domain job out before loading, and if load -w still reports a load failure it boots out once more and retries a single time.

    The decision in here deserves to be stated rather than discovered: bootout kills the live gui job. That is the repair this issue asks for, and it is also precisely why the previous code only printed the command in an error message instead of running it. It is scoped so a healthy job is never touched — it runs inside installLaunchd only, never in ocx service start, and only after load -w has already failed. If you disagree with that tradeoff, this is the line to push back on.

    launchctlLoadFailed itself is unchanged. That regex is the silent-success guard added in August, and the right fix was to recover from the condition it detects, not to stop detecting it. stopLaunchd, uninstallLaunchd, startLaunchd and statusLaunchd are all untouched.

    Sequenced after #4152, which rewrote the same runLaunchctl runner; this adopts that seam rather than introducing a second one.

    Worth naming for a change of this kind: no launchctl, ocx service, or ocx start/stop/restart was executed anywhere while producing it. A live proxy was running on the development machine and a separate task owned it. The behaviour is proven by stderr fixtures against the injection seam and by remote CI.

    Evidence: exact-head CI at ae057c4211c39673eb40183318deb0fa31bdd649 — gh run view 34418986812 --exit-status returns 0 for Cross-platform CI, with Service lifecycle, React Doctor and Enforce PR target branch also 0. Twenty-six checks succeeded, none failed or cancelled. git merge-base --is-ancestor ae057c421 origin/dev passes.

    Still open and deliberately out of scope, in case you hit either: statusLaunchd remains launchctl list | grep rather than a gui domain print, and the ocx update fallback still spawns the proxy without waiting for a bind. Both change behaviour independently of this bug and deserve their own change.

    Closing by hand because pull requests here target dev, so GitHub does not auto-close a linked issue.

    Plan and root-cause record: devlog/_plan/260910_post249_round2/040_4141_launchctl_bootout.md.

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 flagsplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)serviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions