You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Service fails to auto-start after ocx update due to legacy launchctl unload and strict launchctlLoadFailed check #4141
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:
Legacy launchctl unload <plist> in src/service.ts:installLaunchd() fails to unregister the service from the modern macOS gui/<uid> domain.
Subsequent launchctl load -w <plist> emits Load failed: 5: Input/output error to stderr because the job is still bootstrapped.
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.
The update fallback startProxyDirectly() fails to bind or exits due to the existing process/lock.
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
On macOS, install and register the background service: ocx service install.
Run ocx update (or upgrade from v2.48.0 to v2.49.0).
Notice:
opencodex: service refresh failed — starting the proxy directly instead.
Run 'ocx service repair' to see why the background service could not restart.
Check proxy status or try to connect: no proxy is listening.
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.
이 이슈는 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 대상이 아니다.
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 errorand 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.
Activity
lidge-jun commentedon Sep 9, 2026
리뷰 · 우선순위 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 updatefallbackstartProxyDirectly- 기존 락/프로세스에 막히면 완전 다운. repair와 함께 봐야 함#4131 - 이 이슈로 대체. OPEN 잔존 시 close
메인테이너의 판단이 필요한 지점
launchctlLoadFailed·repair 분기만 고정할지너의 추천
이 이슈를 정본으로 유지하고 #4131은 닫아라. help wanted로 두되,
service.ts의 unload→bootout + load 실패 시 한 번 재시도 + status를 gui 도메인으로 맞추는 작은 PR을 받는 쪽이 좋다. 2.49 출시 노트에 워크어라운드 한 줄을 넣는 것도 검토하라. 중복·분할 캠페인 무관이라 close-don't-rebase 대상이 아니다.이 댓글은 grok-bot이 작성했습니다
docs(devlog): re-split the round-2 lanes after the roadmap audit
lidge-jun commentedon Sep 10, 2026
Fixed on
devby #4164, merged as95a3f6a59.ocx service repair— which is whatocx updateruns after replacing the binary — now recovers from the exact condition that was breaking it. Modern launchd writesLoad failed: 5: Input/output errorand exits 0 when a job is already bootstrapped ingui/<uid>, so the one case that most needed repairing was the oneinstallLaunchdcould not repair. It now boots the gui-domain job out before loading, and ifload -wstill 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:
bootoutkills 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 insideinstallLaunchdonly, never inocx service start, and only afterload -whas already failed. If you disagree with that tradeoff, this is the line to push back on.launchctlLoadFaileditself 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,startLaunchdandstatusLaunchdare all untouched.Sequenced after #4152, which rewrote the same
runLaunchctlrunner; this adopts that seam rather than introducing a second one.Worth naming for a change of this kind: no
launchctl,ocx service, orocx start/stop/restartwas 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-statusreturns 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/devpasses.Still open and deliberately out of scope, in case you hit either:
statusLaunchdremainslaunchctl list | greprather than aguidomainprint, and theocx updatefallback 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.docs(devlog): record lidge-jun#4147, lidge-jun#3859, and the lidge-ju…
fix(service): recover a stale launchd job with bootout before load
docs(devlog): close the round-2 record at the screenshot gate
docs(devlog): re-split the round-2 lanes after the roadmap audit
docs(devlog): record lidge-jun#4147, lidge-jun#3859, and the lidge-ju…
fix(service): recover a stale launchd job with bootout before load
docs(devlog): close the round-2 record at the screenshot gate