Client or integration
Other
Installed macOS launchd service com.opencodex.proxy (ocx service). The same throw also aborts ocx start.
Area
Service lifecycle
Summary
On OpenCodex 2.13.0, an installed launchd service can report "registered / loaded" while never serving port 10100. Startup dies with OpenAiTierBackupCollisionError: Existing OpenAI tier backup differs from the current config when current ~/.opencodex/config.json differs from ~/.opencodex/config.json.pre-openai-tiers-v2.bak and that backup is still classified as a rollback snapshot (openaiProviderTierVersion !== 2).
KeepAlive then turns a single throw into a crash loop. ocx service status says the job is installed, but ocx health --json is {"ok":false,"pid":null,"port":null} and GET /healthz is connection-refused.
This is not a reopen of closed #257. #257 was the ocx init path writing a fresh config without openaiProviderTierVersion, which made startup treat a current 3-tier config as legacy and collide with an existing v2 backup. That init-path gap was fixed in 2aa4430c by stamping the current tier version on fresh/re-init configs, and the close-out explicitly kept rollback-backup immutability. The remaining hole is the start / launchd path: if a later rewritten config still needs startup migration and a rollback-classified .pre-openai-tiers-v2.bak is present, backupConfigBeforeOpenAiTierMigration() throws instead of preserving-then-continuing. ocx init already has cleanupOpenAiTierBackupAfterInit() (src/cli/init.ts) that copies the snapshot to config.json.pre-openai-tiers-v1-rollback.<ts>.bak and unlinks the blocking v2 path. ocx start / the LaunchAgent do not.
Expected: keep the rollback snapshot (do not silently overwrite it), but do not crash-loop the installed service. ocx start / ocx service repair should preserve-then-continue, or fail with an actionable service-status message instead of throwing inside keepalive.
Reproduction
- Install OpenCodex 2.13.0 and register the macOS service (
ocx service / com.opencodex.proxy, KeepAlive + RunAtLoad).
- Leave an existing
~/.opencodex/config.json.pre-openai-tiers-v2.bak that parses as pre-migration / rollback (openaiProviderTierVersion: 1, defaultProvider: openai-multi).
- Rewrite current
~/.opencodex/config.json to a smaller current-tree config that still triggers startup migration (observed: kimi-only, no openaiProviderTierVersion).
- Let launchd start/restart the service, or run
ocx start.
- Observe
OpenAiTierBackupCollisionError in ~/.opencodex/service.log, launchd last exit code 1, and port 10100 never coming up.
Operator workaround that recovered the live host (do not delete the rollback):
- Copy the blocking backup to
~/.opencodex/config.json.pre-openai-tiers-v1-rollback.<ts>.bak.
- Remove only the blocking
.pre-openai-tiers-v2.bak path.
- Run
/opt/homebrew/bin/ocx service repair.
- Service returned to
GET http://127.0.0.1:10100/healthz → 200.
Version
2.13.0
CLI binary: /opt/homebrew/bin/ocx → @bitkyc08/opencodex 2.13.0.
LaunchAgent exec: source checkout bun.exe + src/cli/index.ts (OCX_SERVICE=1, start --port 10100).
Operating system
macOS Darwin 27.0.0 arm64 (persistent LaunchAgent install; host had been up 8 days when the service was found dead).
Provider and model
Not request-path specific. Crash happens before the proxy binds. Live config at failure time was kimi-only; colliding backup was defaultProvider=openai-multi.
Logs or error output
ocx status
❌ Proxy: not running
Health: http://127.0.0.1:10100/healthz unreachable
Service: installed and loaded (launchd) — registered but NOT serving
↳ re-run 'ocx service repair'
ocx health --json
{"ok":false,"pid":null,"port":null}
curl http://127.0.0.1:10100/healthz
Failed to connect to 127.0.0.1 port 10100: Couldn't connect to server
launchctl print gui/$UID/com.opencodex.proxy
state = spawn scheduled
last exit code = 1
runs = 19
properties = keepalive | runatload
Repeated ~/.opencodex/service.log tail:
OpenAiTierBackupCollisionError: Existing OpenAI tier backup differs from the current config
at backupConfigBeforeOpenAiTierMigration (src/config.ts:464:15)
at runOpenAiTierStartupMigration (src/providers/openai-tier-startup.ts:23:8)
at startServer (src/server/index.ts:493:51)
at handleStart (src/cli/index.ts:241:16)
After preserving the rollback and ocx service repair:
{"status":"ok","service":"opencodex","version":"2.13.0","pid":12831,"port":10100}
GET /healthz -> 200
launchd state = running
Screenshots and supporting files
None. No secrets, tokens, emails, or account identifiers attached.
Redacted configuration
Current ~/.opencodex/config.json at failure time (shape only):
{
"port": 10100,
"defaultProvider": "kimi",
"googleAntigravityStaticCatalogVersion": 2,
"providers": { "kimi": { "adapter": "openai-chat" } }
}
Size 3118 bytes. No openaiProviderTierVersion.
Colliding ~/.opencodex/config.json.pre-openai-tiers-v2.bak (shape only):
{
"defaultProvider": "openai-multi",
"openaiProviderTierVersion": 1,
"providers": ["openai", "openai-multi", "anthropic", "xai", "cursor", "google-antigravity", "opencode-go", "kimi", "opencode-free"]
}
Size 25439 bytes. classifyOpenAiTierBackup() treats this as "rollback" because openaiProviderTierVersion !== 2.
Related code:
src/config.ts classifyOpenAiTierBackup() / backupConfigBeforeOpenAiTierMigration() — rollback-classified divergence throws OpenAiTierBackupCollisionError
src/providers/openai-tier-startup.ts — startup migration calls backup then save
src/cli/init.ts cleanupOpenAiTierBackupAfterInit() — already preserve-then-unlink on the init path only
Checks
Client or integration
Other
Installed macOS launchd service
com.opencodex.proxy(ocx service). The same throw also abortsocx start.Area
Service lifecycle
Summary
On OpenCodex 2.13.0, an installed launchd service can report "registered / loaded" while never serving port 10100. Startup dies with
OpenAiTierBackupCollisionError: Existing OpenAI tier backup differs from the current configwhen current~/.opencodex/config.jsondiffers from~/.opencodex/config.json.pre-openai-tiers-v2.bakand that backup is still classified as a rollback snapshot (openaiProviderTierVersion !== 2).KeepAlive then turns a single throw into a crash loop.
ocx service statussays the job is installed, butocx health --jsonis{"ok":false,"pid":null,"port":null}andGET /healthzis connection-refused.This is not a reopen of closed #257. #257 was the
ocx initpath writing a fresh config withoutopenaiProviderTierVersion, which made startup treat a current 3-tier config as legacy and collide with an existing v2 backup. That init-path gap was fixed in2aa4430cby stamping the current tier version on fresh/re-init configs, and the close-out explicitly kept rollback-backup immutability. The remaining hole is the start / launchd path: if a later rewritten config still needs startup migration and a rollback-classified.pre-openai-tiers-v2.bakis present,backupConfigBeforeOpenAiTierMigration()throws instead of preserving-then-continuing.ocx initalready hascleanupOpenAiTierBackupAfterInit()(src/cli/init.ts) that copies the snapshot toconfig.json.pre-openai-tiers-v1-rollback.<ts>.bakand unlinks the blocking v2 path.ocx start/ the LaunchAgent do not.Expected: keep the rollback snapshot (do not silently overwrite it), but do not crash-loop the installed service.
ocx start/ocx service repairshould preserve-then-continue, or fail with an actionable service-status message instead of throwing inside keepalive.Reproduction
ocx service/com.opencodex.proxy, KeepAlive + RunAtLoad).~/.opencodex/config.json.pre-openai-tiers-v2.bakthat parses as pre-migration / rollback (openaiProviderTierVersion: 1,defaultProvider: openai-multi).~/.opencodex/config.jsonto a smaller current-tree config that still triggers startup migration (observed: kimi-only, noopenaiProviderTierVersion).ocx start.OpenAiTierBackupCollisionErrorin~/.opencodex/service.log, launchd last exit code 1, and port 10100 never coming up.Operator workaround that recovered the live host (do not delete the rollback):
~/.opencodex/config.json.pre-openai-tiers-v1-rollback.<ts>.bak..pre-openai-tiers-v2.bakpath./opt/homebrew/bin/ocx service repair.GET http://127.0.0.1:10100/healthz→ 200.Version
2.13.0
CLI binary:
/opt/homebrew/bin/ocx→@bitkyc08/opencodex2.13.0.LaunchAgent exec: source checkout
bun.exe+src/cli/index.ts(OCX_SERVICE=1,start --port 10100).Operating system
macOS Darwin 27.0.0 arm64 (persistent LaunchAgent install; host had been up 8 days when the service was found dead).
Provider and model
Not request-path specific. Crash happens before the proxy binds. Live config at failure time was kimi-only; colliding backup was
defaultProvider=openai-multi.Logs or error output
Repeated
~/.opencodex/service.logtail:OpenAiTierBackupCollisionError: Existing OpenAI tier backup differs from the current config at backupConfigBeforeOpenAiTierMigration (src/config.ts:464:15) at runOpenAiTierStartupMigration (src/providers/openai-tier-startup.ts:23:8) at startServer (src/server/index.ts:493:51) at handleStart (src/cli/index.ts:241:16)After preserving the rollback and
ocx service repair:{"status":"ok","service":"opencodex","version":"2.13.0","pid":12831,"port":10100} GET /healthz -> 200 launchd state = runningScreenshots and supporting files
None. No secrets, tokens, emails, or account identifiers attached.
Redacted configuration
Current
~/.opencodex/config.jsonat failure time (shape only):{ "port": 10100, "defaultProvider": "kimi", "googleAntigravityStaticCatalogVersion": 2, "providers": { "kimi": { "adapter": "openai-chat" } } }Size 3118 bytes. No
openaiProviderTierVersion.Colliding
~/.opencodex/config.json.pre-openai-tiers-v2.bak(shape only):{ "defaultProvider": "openai-multi", "openaiProviderTierVersion": 1, "providers": ["openai", "openai-multi", "anthropic", "xai", "cursor", "google-antigravity", "opencode-go", "kimi", "opencode-free"] }Size 25439 bytes.
classifyOpenAiTierBackup()treats this as"rollback"becauseopenaiProviderTierVersion !== 2.Related code:
src/config.tsclassifyOpenAiTierBackup()/backupConfigBeforeOpenAiTierMigration()— rollback-classified divergence throwsOpenAiTierBackupCollisionErrorsrc/providers/openai-tier-startup.ts— startup migration calls backup then savesrc/cli/init.tscleanupOpenAiTierBackupAfterInit()— already preserve-then-unlink on the init path onlyChecks