Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,33 @@ Routed catalog entries also get their GPT-5 identity rewritten to the real upstr
Reasoning controls come from provider/model metadata across Codex's `low | medium | high | xhigh |
max | ultra` ladder; unsupported values are mapped or clamped before the upstream request.

### Coordinator diagnosis and recovery

Native config/history writes use a per-user SQLite coordinator keyed by the canonical `CODEX_HOME`.
If a process terminates in SQLite's initial creation window, a zero-byte coordinator can remain even
though it contains no authoritative transition row. `ocx doctor` reports the exact coordinator path
and distinguishes zero-byte, unversioned, rowless, valid, unsafe, and unreadable states without
creating SQLite sidecars. Automatic sync tolerates only an identity-stable zero-byte file that has
settled for at least one second and whose immutable SQLite snapshot has version zero with no tables;
a newly created zero-byte file remains on the locked coordinator path.

For a state that doctor proves is a zero-byte creation remnant, stop the OpenCodex proxy/service
and run:

```bash
ocx doctor --recover-zero-byte-coordinator --yes
ocx sync
```

Recovery moves the still-identical zero-byte file to a same-directory `.zero-byte-backup-*` path;
it does not delete the evidence or adopt legacy routed state. It refuses a running proxy, lock
contention, symlinks/reparse points, foreign ownership, changed files, every non-empty database,
and any coordinator that already has an authoritative row. If the file is less than one second
old, sync deliberately still treats it as coordinated; stop writers and use the explicit recovery,
or wait one second before retrying `ocx sync`. Desktop renderer filtering is a
separate layer: a correct catalog and coordinator do not by themselves bypass the Codex App model
allowlist.

### Routed local tools

Non-native routed catalog rows use `tool_mode: "code_mode_only"`. This lets Codex expose its official
Expand Down
18 changes: 18 additions & 0 deletions docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ unreachable; and 64 for invalid arguments.

### `ocx doctor`

The default report includes the native-write coordinator state and exact path using immutable
read-only SQLite inspection. Zero-byte, empty-unversioned, and rowless states are shown separately
from catalog/app-server health, so a successful catalog refresh is not mistaken for successful
Codex config injection.

After stopping the OpenCodex proxy/service, explicitly preserve and move a proven non-authoritative
coordinator, then retry sync:

```bash
ocx doctor --recover-zero-byte-coordinator --yes
ocx sync
```

The recovery accepts only a proven zero-byte remnant. It refuses every non-empty, valid, unknown,
changed, unsafe, or busy database and creates a same-directory `.zero-byte-backup-*` file instead
of deleting anything. A file younger than one second intentionally remains coordinated; after
stopping writers, use the explicit recovery above or wait one second before retrying `ocx sync`.

Comment thread
Ingwannu marked this conversation as resolved.
Resolved
Comment thread
Ingwannu marked this conversation as resolved.
Resolved
Run read-only environment and connectivity diagnostics: state paths and filesystem type, WSL dual
installs, proxy environment/config, ChatGPT reachability, Codex plugin and project-config warnings,
and pending history migration. The Codex app-home targeting section also detects the narrow Windows
Expand Down
4 changes: 2 additions & 2 deletions src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ const commandRunners: Record<string, CommandRunner> = {
},
doctor: async deps => {
const doctorArgs = deps.args.slice(1);
const { runDoctor } = await import("./doctor");
const { RECOVER_ZERO_BYTE_COORDINATOR_FLAG, runDoctor } = await import("./doctor");
await runDoctor(doctorArgs);
if (!doctorArgs.includes("--fix-codex-runtime")) {
if (!doctorArgs.includes("--fix-codex-runtime") && !doctorArgs.includes(RECOVER_ZERO_BYTE_COORDINATOR_FLAG)) {
Comment thread
Ingwannu marked this conversation as resolved.
Resolved
console.log("");
const { printCodexLogGuardDoctor } = await import("./codex-log-guard-doctor");
printCodexLogGuardDoctor();
Expand Down
90 changes: 90 additions & 0 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHome
import { scanCodexAgentRolesWithTomlModelFallback } from "../codex/subagent-model-fallback";
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
import { countPendingOpencodexHistory } from "../codex/history-provider";
import {
inspectCodexCoordinator,
recoverZeroByteCodexCoordinator,
type CodexCoordinatorDiagnostic,
} from "../codex/coordinator-doctor";
import {
inspectAbandonedResponseStateTemps,
reclaimAbandonedResponseStateTemps,
Expand Down Expand Up @@ -684,6 +689,7 @@ export async function fetchServiceMemory(
const mb = (bytes: number): string => `${Math.round(bytes / (1024 * 1024))}MB`;

export const RECLAIM_RESPONSE_TEMPS_FLAG = "--reclaim-response-temps";
export const RECOVER_ZERO_BYTE_COORDINATOR_FLAG = "--recover-zero-byte-coordinator";
/** Matches the dry run's entry bound so report and reclaim agree on a large backlog. */
const RESPONSE_TEMP_RECLAIM_MAX_CLEANUPS = 4_096;
/** Names the subsystem: other components mint temps with the same shape and are not covered. */
Expand Down Expand Up @@ -734,6 +740,61 @@ export function formatResponseTempLines(
return lines;
}

export function formatCoordinatorDoctorLines(diagnostic: CodexCoordinatorDiagnostic): string[] {
const pathLine = diagnostic.path ? [` path: ${diagnostic.path}`] : [];
const evidenceLines = "evidence" in diagnostic && diagnostic.evidence
? [
` size: ${diagnostic.evidence.sizeBytes} bytes; user_version: ${diagnostic.evidence.schemaVersion}`,
` tables: ${diagnostic.evidence.tables.length === 0 ? "none" : diagnostic.evidence.tables.join(", ")}`,
` transition rows: ${diagnostic.evidence.transitionRows ?? "not inspected"}; singleton=1 rows: ${diagnostic.evidence.singletonRows ?? "not inspected"}`,
]
: [];
switch (diagnostic.kind) {
case "absent":
return [" ok native-write coordinator not created yet", ...pathLine];
case "ready":
return [" ok native-write coordinator has an authoritative transition row", ...pathLine, ...evidenceLines];
case "zero-byte":
return [
" !! native-write coordinator is a zero-byte remnant and has no authority",
...pathLine,
...evidenceLines,
` Action: stop the OpenCodex proxy/service, then run ocx doctor ${RECOVER_ZERO_BYTE_COORDINATOR_FLAG} --yes`,
" Note: a file younger than 1 second stays coordinated; use explicit recovery after stopping writers, or wait 1 second and retry ocx sync.",
];
case "unversioned-empty":
return [
" !! native-write coordinator is a non-empty unversioned database; automatic recovery is refused",
...pathLine,
...evidenceLines,
];
case "rowless":
return [
" !! native-write coordinator has schema version 1 but no authoritative row; automatic recovery is refused",
...pathLine,
...evidenceLines,
];
case "unversioned-nonempty":
return [
" !! native-write coordinator is unversioned and contains unknown tables; automatic recovery is refused",
...pathLine,
...evidenceLines,
];
case "unsupported":
return [
` !! native-write coordinator schema version ${diagnostic.version} is unsupported; automatic recovery is refused`,
...pathLine,
...evidenceLines,
];
case "changed":
return [" -- native-write coordinator changed during diagnosis; re-run ocx doctor", ...pathLine];
case "unsafe":
return [` !! native-write coordinator path is unsafe: ${diagnostic.reason}`, ...pathLine];
case "unreadable":
return [` !! native-write coordinator is unreadable: ${diagnostic.reason}`, ...pathLine, ...evidenceLines];
}
}

/** Render the doctor "Memory / runtime" section lines (testable without console capture). */
export function formatServiceMemoryLines(report: ServiceMemoryReport): string[] {
const lines: string[] = [];
Expand Down Expand Up @@ -846,6 +907,33 @@ export async function runDoctor(args: string[] = []): Promise<void> {
return;
}

if (args.includes(RECOVER_ZERO_BYTE_COORDINATOR_FLAG)) {
if (!args.includes("--yes")) {
console.log(`Recovery is explicit and creates a same-directory backup. Re-run: ocx doctor ${RECOVER_ZERO_BYTE_COORDINATOR_FLAG} --yes`);
process.exitCode = 1;
return;
}
const diagnostics = readConfigDiagnostics().config;
const live = await findLiveProxy({
configFn: () => ({ port: diagnostics.port, hostname: diagnostics.hostname }),
});
if (live) {
console.log(`Recovery refused: OpenCodex proxy pid ${live.pid} is still running. Stop the proxy/service and retry.`);
process.exitCode = 1;
return;
}
const recovered = recoverZeroByteCodexCoordinator();
if (!recovered.ok) {
console.log(`Recovery refused: ${recovered.reason}.`);
process.exitCode = 1;
return;
}
console.log(`Moved the non-authoritative coordinator to ${recovered.backupPath}`);
console.log("Run `ocx sync` to retry Codex config injection. The backup was preserved and no Codex config/catalog file was changed by recovery.");
process.exitCode = 0;
return;
}

console.log("opencodex doctor\n");

// Ordering note: the memory/runtime section renders after "Running proxy
Expand Down Expand Up @@ -1005,6 +1093,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
const reason = cause instanceof CodexUserIdentityRefusal ? cause.message : String(cause);
console.log(` -- history coordinator namespace refused: ${reason}`);
}
console.log("\nCodex native-write coordinator");
for (const line of formatCoordinatorDoctorLines(inspectCodexCoordinator())) console.log(line);
const pending = countPendingOpencodexHistory();
if (pending.failed) {
console.log(" -- state DB locked or unreadable (Codex app open?) — migration state unknown");
Expand Down
2 changes: 2 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Usage:
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
ocx doctor --reclaim-response-temps
Reclaim abandoned response-state temp files (works without a running proxy)
ocx doctor --recover-zero-byte-coordinator --yes
Back up a proven zero-byte Codex coordinator after stopping the proxy
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
ocx login <provider> OAuth or API-key provider login
ocx logout <provider> Remove a stored OAuth login
Expand Down
4 changes: 4 additions & 0 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
name: "doctor",
usage: "ocx doctor",
summary: "Diagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability).",
details: [
"Default mode is observe-only and reports the native-write coordinator state and exact path.",
"After stopping the proxy/service, `--recover-zero-byte-coordinator --yes` moves only a proven zero-byte coordinator to a same-directory backup.",
],
},
{
name: "debug",
Expand Down
Loading
Loading