Repository navigation
[Bug][macOS][Dashboard] Stop button can unload launchd service before native Codex teardown completes #4023
Description
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:
stopServiceIfInstalledDetailed()is called.- On macOS it synchronously invokes
launchctl unloadfor the LaunchAgent. - 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
- On macOS, install and run OpenCodex as a launchd background service.
- Confirm Codex routing is injected and the dashboard is available on port 10100.
- Press Stop proxy in the dashboard and confirm.
- Observe that the proxy and dashboard stop.
- Inspect the effective Codex
config.toml. - Observe that the loopback routing keys may remain, leaving Codex pointed to the stopped proxy.
- 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:
- https://github.com/lidge-jun/opencodex/blob/v2.48.0/src/server/management-api.ts#L275-L348
- https://github.com/lidge-jun/opencodex/blob/v2.48.0/src/service.ts#L2351
- https://github.com/lidge-jun/opencodex/blob/v2.48.0/src/service.ts#L3927-L3932
Related but not duplicate:
- ocx stop/restore leave unmarked openai_base_url + proxy models_cache behind when Codex app rewrote config after injection #1798 covers Codex rewriting the config after injection and marker/hash-based restore failure.
- [Bug][Windows]: dashboard update aborts after proxy stops when history restore exits non-zero #3008 covers Windows dashboard update/stop lifecycle behavior.
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
lidge-jun commentedon Sep 8, 2026
리뷰 · 우선순위 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로 복원”이 보인다는 관찰과도 맞는다.지금
devHEAD29bb221c3(2.49.0)에서 경로를 다시 읽었다.src/server/management-api.ts275–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 위험은 이 가드 밖에 있다메인테이너의 판단이 필요한 지점
ocx stop만 허용할지ocx stop위임으로 갈지ocx restore/ocx 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이 작성했습니다
fix(service): identify the managed job by a wrapper-only marker
wp4: fix four reported bugs with no owning pull request (#4058)
lidge-jun commentedon Sep 8, 2026
Fixed on
devin ff524ae (via #4058).The dashboard Stop now refuses with 409
self_unload_servicewhen 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 stopis 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, whichocx claudeandocx opencodealso 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 dedicatedOCX_SERVICE_MANAGED=1marker and the refusal discriminates on that.Closing manually because pull requests here target
dev, and GitHub only auto-closes on merge intomain.wp7: report the actual reason a proxy refused to stop (#4067)
wp4: fix four reported bugs with no owning pull request (lidge-jun#4058)
wp7: report the actual reason a proxy refused to stop (lidge-jun#4067)
test(stop): follow the sibling-aware respawn pre-check in the #4023 d…