Skip to content

[Bug][macOS][Dashboard] Stop button can unload launchd service before native Codex teardown completes #4023

Closed

Description

@tommy1616

Client or integration

OpenCodex dashboard with Codex Desktop.

Area

Dashboard / service lifecycle / Codex config restore.

Summary

On macOS with the launchd background service installed, pressing the dashboard Stop proxy button stopped the proxy and made the dashboard unreachable, but left Codex routed to http://127.0.0.1:10100/v1. Codex then could not send requests until the proxy was manually started again.

The current POST /api/stop control flow appears vulnerable to terminating its own process before shared teardown completes:

  1. stopServiceIfInstalledDetailed() is called.
  2. On macOS it synchronously invokes launchctl unload for the LaunchAgent.
  3. Only after that returns does the handler await performStopTeardown(), which calls the native Codex restore path.

Because the API handler runs inside the launchd-managed proxy process, unloading its own job can terminate it before step 3 finishes. This matches the observed state: the listener and dashboard were gone, while openai_base_url, experimental_realtime_ws_base_url, and model_catalog_json remained injected in the Codex config.

This ordering is still present in tag v2.48.0 at src/server/management-api.ts around lines 275-348 and src/service.ts around lines 2351 and 3927-3932.

Expected: dashboard Stop must restore native Codex and verify the restore before allowing launchd to terminate the proxy, or delegate the complete stop to an external process that survives unloading the service. If safe completion cannot be guaranteed, the dashboard should refuse and instruct the user to run ocx stop, similar to the existing Task Scheduler protection.

Reproduction

  1. On macOS, install and run OpenCodex as a launchd background service.
  2. Confirm Codex routing is injected and the dashboard is available on port 10100.
  3. Press Stop proxy in the dashboard and confirm.
  4. Observe that the proxy and dashboard stop.
  5. Inspect the effective Codex config.toml.
  6. Observe that the loopback routing keys may remain, leaving Codex pointed to the stopped proxy.
  7. Start the proxy again; Codex requests work again because the stale route becomes reachable.

Version

Observed on 2.42.0. Source ordering verified unchanged in tag 2.48.0.

Operating system

macOS, Apple Silicon, launchd user service.

Provider and model

Not provider-specific.

Logs or error output

After manually starting the proxy again, the service log reported that the previous session did not shut down cleanly and restored Codex state from the journal. There was no successful native-restore message associated with the dashboard stop.

Screenshots and supporting files

Relevant source:

Related but not duplicate:

Redacted configuration

openai_base_url = "http://127.0.0.1:10100/v1"
experimental_realtime_ws_base_url = "http://127.0.0.1:10100/v1"
model_catalog_json = "/redacted/.codex/opencodex-catalog.json"

Checks

  • I searched existing open and closed issues.
  • I removed secrets, tokens, account details, credentials, and personal paths.

Activity

  1. added
    bugSomething isn't working
    guiDashboard, tray, settings UI
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Sep 8, 2026
  2. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 70 / 80

    이 이슈는 macOS에서 대시보드 Stop proxy를 누르면 launchd 서비스는 내려가는데, Codex 쪽 openai_base_url / experimental_realtime_ws_base_url / model_catalog_json 주입은 그대로 남아 Codex가 죽은 http://127.0.0.1:10100/v1을 계속 바라본다는 버그다. 작성자 가설은 단순하고 지금 dev 코드와 맞다. POST /api/stop가 먼저 stopServiceIfInstalledDetailed()로 launchd를 unload하고, 그다음에야 performStopTeardown()로 네이티브 Codex 복원을 한다. 핸들러가 launchd가 관리하는 프록시 프로세스 안에서 돌면, unload 순간에 프로세스가 죽어 teardown이 끝까지 못 갈 수 있다. 재현 후 서비스 로그에 “이전 세션이 깨끗이 안 내려가서 journal로 복원”이 보인다는 관찰과도 맞는다.

    지금 dev HEAD 29bb221c3(2.49.0)에서 경로를 다시 읽었다. src/server/management-api.ts 275–361줄: Windows Task Scheduler에는 installedServiceRespawnRisk()로 respawnable이면 409로 거절하고 ocx stop을 시키지만, 그 가드는 win32 전용이다. src/service.ts의 installedServiceRespawnRisk는 platform !== "win32"이면 바로 "none"을 반환한다(3860–3866줄 근처). 이어서 darwin 분기는 plist가 있으면 stopLaunchd() → launchctl unload(2351줄, 3927–3932줄)를 하고 "stopped"를 돌려준다. 그 다음에야 performStopTeardown(src/server/stop-teardown.ts)가 restoreNativeCodexAsync를 await한다. 즉 Windows #3008이 고친 “매니저를 너무 일찍 건드리면 안 된다” 교훈이 macOS에는 반대로 “매니저를 먼저 끄고 teardown”으로 남아 있다. #1798(주입 후 Codex가 config를 다시 씀)·#3008(Windows 대시보드 stop/update)은 관련이나 중복은 아니다. 둘 다 이미 CLOSED.

    왜 점수가 높은가. macOS + launchd 설치 사용자에게 Stop은 “안전한 종료”로 보이지만, 결과는 대시보드 사망 + Codex 루프백 고아 설정이다. 프록시를 다시 켜면 우연히 살아 보여 원인 추적이 어렵다. 플랫폼/서비스 수명주기 버그이고, 수정 방향도 이슈에 이미 세 갈래로 적혀 있다: (1) launchd unload 전에 네이티브 복원을 끝내고 검증한다, (2) unload에도 살아남는 외부 헬퍼에 전체 stop을 맡긴다, (3) 안전하게 끝낼 수 없으면 대시보드 Stop을 거절하고 ocx stop만 안내한다(Windows Task Scheduler 보호와 대칭).

    라인 / 경로로 보면 문제 지점은 이렇다.

    라인 315 (src/server/management-api.ts) - stopServiceIfInstalledDetailed()를 teardown보다 먼저 호출한다. darwin에서는 이 호출이 곧 자기 프로세스 unload다
    라인 348 (src/server/management-api.ts) - performStopTeardown는 그 뒤에야 await된다. unload가 프로세스를 끊으면 여기까지 도달하지 못하거나 중간에 죽는다
    라인 2351 (src/service.ts) - stopLaunchd가 launchctl unload를 동기로 실행한다. 자기 job unload와 in-process teardown이 양립하기 어렵다
    라인 3927–3932 (src/service.ts) - darwin 설치 시 unload 성공만 보면 "stopped"를 반환하고, “teardown 완료”와는 무관하다
    라인 3866 (src/service.ts) - installedServiceRespawnRisk가 non-Windows를 항상 "none"으로 본다. macOS self-unload 위험은 이 가드 밖에 있다

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

    • 대시보드 Stop을 macOS launchd에서도 지원할지, Windows처럼 ocx stop만 허용할지
    • 지원한다면 복원 순서(teardown → 검증 → unload)를 in-process로 할지, 짧은 helper/ocx stop 위임으로 갈지
    • unload 실패·부분 복원 시 사용자에게 어떤 409/메시지와 복구 명령(ocx restore / ocx stop)을 보여줄지
    • 같은 패턴이 Linux systemd user unit Stop에도 있는지 한 번에 점검할지

    너의 추천
    bug/platform/service/gui 라벨은 유지하고, 2.49 개발선에서 우선 수정 이슈로 받아 주세요. 최소 수정은 /api/stop에서 darwin(필요하면 linux)일 때 서비스 unload를 teardown+검증 뒤로 옮기거나, self-unload 위험이면 Windows와 같이 409로 거절하고 ocx stop을 안내하는 것이다. 회귀 테스트는 “launchd installed + Stop → config.toml 루프백 키 없음”을 프로세스 밖으로 검증하는 쪽이 안전하다. #3008 주석/가드 옆에 macOS 계약을 문서화하면 다음 기여자가 다시 순서를 뒤집지 않는다.

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

  3. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    Fixed on dev in ff524ae (via #4058).

    The dashboard Stop now refuses with 409 self_unload_service when the proxy is running as its own launchd job, instead of unloading the service and risking the handler dying before teardown restores your native Codex config. Linux systemd is covered by the same change. ocx stop is unaffected — it stops the proxy from the outside and owns the teardown through its receipt — and the message points there.

    Reordering teardown ahead of the manager stop was considered and rejected: the #3008 contract requires the manager to be proven stopped first, so refusing is the available remedy, which is what Windows already did.

    Review caught that the first version keyed on OCX_SERVICE=1, which ocx claude and ocx opencode also set on the proxies they spawn. That would have refused Stop for a proxy no manager supervises, so the plist and unit now write a dedicated OCX_SERVICE_MANAGED=1 marker and the refusal discriminates on that.

    Closing manually because pull requests here target dev, and GitHub only auto-closes on merge into main.

  4. added 2 commits that reference this issue on Sep 17, 2026
    a34ec9d
    9941bf5
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 workingguiDashboard, tray, settings UIplatformOS/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