Skip to content

ocx restore / stop / uninstall refuse to restore config.toml on paginated Codex history, leaving the CLI pointed at a dead proxy port #4812

Description

@practical-tools-lab

Client or integration

Codex CLI

Area

Service lifecycle

Summary

When any Codex thread in ~/.codex/state_*.sqlite is tagged model_provider='opencodex' on a paginated-history store — the schema every current Codex build uses — ocx restore, ocx stop, and ocx uninstall all refuse to touch ~/.codex/config.toml. The proxy can then be stopped and uninstalled while config.toml still contains model_provider = "opencodex" + [model_providers.opencodex] base_url = "http://127.0.0.1:10100/v1" (or the root openai_base_url override). Every subsequent codex invocation fails with a connection error on the dead port until the user hand-edits TOML.

There is no --force, no fallback strip, and no escape hatch in the CLI. The refusal message ("Native writer coordination is required") does not tell the user how to recover.

Expected instead:

One of:

  • a documented --force / "strip routing anyway" mode for restore/uninstall, accepting that opencodex-tagged threads will not resume;
  • or, on refusal, a degraded-but-working end state: e.g. remove the root model_provider/openai_base_url while keeping the [model_providers.opencodex] table on disk (so tagged threads still resolve by id and native requests fall back to the built-in openai provider);
  • or at minimum, an actionable error telling the user the exact config.toml lines to delete.

Notably, the apply path already implements the middle option: when the relabel unit stands down, injection deliberately keeps an existing opencodex table so tagged threads keep resolving (src/codex/inject.ts:497-502). The remove path has no equivalent.

Reproduction

  1. Loopback install, codexDesktopAuthless: true, run ocx syncconfig.toml gets model_provider = "opencodex" + the provider table.
  2. Start one Codex Desktop/CLI conversation → the thread row is written with model_provider='opencodex' and history_mode='paginated' (current Codex schema).
  3. ocx uninstall (or ocx stop, or ocx restore).
  4. Observe: the Codex-config step reports Codex configuration preserved: history_paginated_requires_native_writer; uninstall finishes with a failed step; config.toml still routes to 127.0.0.1:10100.
  5. codex / the desktop app → connection refused until manual TOML edit.

Version

2.56.0

Operating system

macOS 26 (arm64)

Checks

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

Why it happens

preflightCodexHistoryInjection is a read-only guard that returns "history_paginated_requires_native_writer" if the threads table has a history_mode column AND either the restore manifest is non-empty or any queried thread is paginated:

src/codex/history-provider.ts:404-421

    if (!existsSync(resolvedPath)) {
      return restoreEntries.length > 0 ? "history_state_database_missing" : null;
    }
    db = new Database(resolvedPath, { readonly: true });
    const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
    const paginatedColumn = columns.some(column => column.name === "history_mode");
    if (paginatedColumn && restoreEntries.length > 0) return "history_paginated_requires_native_writer";
    ...
      WHERE ${providerTableMode ? resumeHistory ? "model_provider IN ('openai', 'opencodex')" : "0"
        : "model_provider = 'opencodex'"}
    `).all();
    for (const row of rows) {
      if (paginatedColumn || row.history_mode === "paginated") return "history_paginated_requires_native_writer";

Both restore entry points refuse on that result, in order:

src/codex/inject/restore.ts:233-234 — config restore refuses before touching the journal:

    const historyError = preflightCodexHistoryInjection(false, false);
    if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };

src/codex/inject/remove.ts:145-146 — the strip path refuses identically:

  const historyError = preflightCodexHistoryInjection(false, false);
  if (historyError) return { success: false, message: `Codex configuration preserved: ${historyError}. Native writer coordination is required.` };

Every user-facing command funnels into restoreNativeCodexAsyncrestoreCodexConfigInlineImpl → that preflight: ocx restore (src/cli/dispatch.ts:196), ocx stop (src/server/stop-teardown.ts:60, src/cli/index.ts:793), ocx uninstall (src/cli/index.ts:1315), and the service paths (src/service/cli.ts:293,337).

ocx uninstall then completes the trap: the failed restore is recorded in failures (src/cli/index.ts:1364-1367 exits 1 and keeps config/backups "so the failed restore step can be retried"), but the proxy service/shim is still removed — so the state it preserves is precisely a config.toml pointing at a now-unroutable port.

Impact

  • Self-inflicted deadlock: the guard exists to keep opencodex-tagged threads resolvable, but after uninstall the endpoint is gone, so the protected threads fail at request time anyway — while all native use is also broken. The protection protects nothing once the proxy is uninstalled.
  • ocx status already knows this state is fatal ("Not running — Codex/Claude requests will fail with connection errors", src/cli/index.ts:1422-1423), yet restore/stop/uninstall can all produce it with no supported recovery path.
  • Any authless / client-compaction / admission-token user on a current Codex build who has started ≥1 conversation is in the refusal class.

Suggested fix

Add a degraded mode to the restore preflight: when refusal is due only to history_paginated_requires_native_writer, allow stripping the root routing keys (model_provider, openai_base_url, experimental_realtime_ws_base_url) while leaving the marker-owned [model_providers.opencodex] table on disk, and print exactly what was kept and why. Offer ocx restore --force-remove-provider (or similar) for full removal including the table.

Related issues

This report is filed separately because it is not about how the refusal is classified or surfaced. It is about the end state after ocx uninstall has already completed: the guard preserves a config.toml pointing at a port that no longer exists, so the opencodex-tagged threads it protects fail at request time anyway, while all native Codex use breaks too. That recovery gap remains regardless of whether the warning is counted as fatal or advisory.

Investigated by reading the installed @bitkyc08/opencodex@2.56.0 package (npm -g; 2.56.0 is the current published version at time of filing). Every quoted snippet is byte-exact from that tree. Corroborating host state: loopback bind, codexDesktopAuthless: true, codexClientCompaction unset, no codexAccountNamespaces, 15-model injected catalog with no reserve row.

Activity

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 workingcliCLI, config inject, packaging flagsserviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions