Skip to content

macOS launchd: ocx service crash-loops with OpenAiTierBackupCollisionError when current config differs from .pre-openai-tiers-v2.bak rollback #1599

Description

@lidge-jun

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

  1. Install OpenCodex 2.13.0 and register the macOS service (ocx service / com.opencodex.proxy, KeepAlive + RunAtLoad).
  2. Leave an existing ~/.opencodex/config.json.pre-openai-tiers-v2.bak that parses as pre-migration / rollback (openaiProviderTierVersion: 1, defaultProvider: openai-multi).
  3. Rewrite current ~/.opencodex/config.json to a smaller current-tree config that still triggers startup migration (observed: kimi-only, no openaiProviderTierVersion).
  4. Let launchd start/restart the service, or run ocx start.
  5. 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):

  1. Copy the blocking backup to ~/.opencodex/config.json.pre-openai-tiers-v1-rollback.<ts>.bak.
  2. Remove only the blocking .pre-openai-tiers-v2.bak path.
  3. Run /opt/homebrew/bin/ocx service repair.
  4. 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

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

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