Skip to content

fix(app-bind): self-heal orphaned runtime-proxy bind with no backup (#614) - #615

Merged
ndycode merged 4 commits into
mainfrom
fix/614-orphaned-app-bind
Jun 16, 2026
Merged

fix(app-bind): self-heal orphaned runtime-proxy bind with no backup (#614)#615
ndycode merged 4 commits into
mainfrom
fix/614-orphaned-app-bind

Conversation

@ndycode

@ndycode ndycode commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Fixes #614.

Problem

When app-bind rewrites the real ~/.codex/config.toml to the runtime-proxy provider but its state/backup files are later lost (cleanup, partial unbind, a marker-less re-run of first-run setup, crash), the config stays bound while the tooling goes blind:

  • getAppBindStatus and rotation status infer "bound" only from app-bind/*.json, so they report "not configured" even though config.toml still has model_provider = "codex-multi-auth-runtime-proxy" + the proxy block.
  • unbind-app keys restoration off the backup file, so with no backup it's a no-op ("Codex app bind was not configured").

Net: the user's Codex CLI/Desktop is routed to a dead proxy port with no automated recovery — hand-editing config.toml was the only fix. (Reproduced live during 2.3.1 smoke testing; it bound the real config twice.)

Fix

lib/runtime/config-toml.ts

  • configHasRuntimeRotationProvider(rawConfig) — detects a bound config from either the top-level model_provider or the proxy [model_providers.<id>] block (top-level scan stops at the first table, so a stray mention elsewhere isn't a false positive).
  • restoreConfigTomlFromRuntimeRotationProviderWithoutBackup(currentConfig, defaultProvider = "openai") — strips the proxy block and, with no original backup line to restore, falls the top-level provider back to openai. Pins line endings to the input style (fixes a CRLF-collapse edge).

lib/runtime/app-bind.ts

  • unbindCodexAppRuntimeRotationLocked now self-heals: when there's no backup and no state but config.toml is still bound, it restores the config directly and reports Restored Codex app config … from an orphaned runtime-proxy bind (no backup was present).
  • getAppBindStatus derives bound from the config when no state file exists and exposes a new unmanagedBind flag.
  • formatAppBindStatus surfaces "bound but unmanaged" with the unbind-app remedy instead of "not configured".

Testing

  • 11 new regression tests across config-toml-restore.test.ts (detection true/false/empty/stray-mention; no-backup restore incl. custom provider + CRLF) and app-bind.test.ts (unmanaged-status detection, self-heal unbind end-to-end, clean-config no-op).
  • Verified end-to-end against a simulated orphaned bind in an isolated temp home: status → "bound but unmanaged"; unbind-app → restores model_provider = "openai", removes the proxy block, preserves other sections; after → bound=false, unmanaged=false.
  • npm run build, typecheck, eslint clean. Full suite: 4947 passed, 3 skipped (was 4936).
  • No behavior change to the normal backed-up bind/unbind path (existing app-bind/config-toml tests unchanged and green).

Release

Targets main; will ship in the next patch (2.3.2). Can't go into the already-published 2.3.1.

🤖 Generated with Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

fixes the orphaned app-bind scenario where config.toml stayed bound to the runtime proxy after state/backup files were lost, leaving the user stuck on a dead port with no automated recovery path.

  • configHasRuntimeRotationProvider lets getAppBindStatus and unbind-app consult the config directly when no state file is present, exposing a new unmanagedBind flag and "bound but unmanaged" status message.
  • restoreConfigTomlFromRuntimeRotationProviderWithoutBackup synthesizes a minimal original config (model_provider = "openai") and delegates to the shared restore path; correctly handles the half-orphan case (proxy block present, top-level provider already native) to avoid producing duplicate TOML keys, and pins CRLF line endings for Windows-authored configs.
  • 11 new regression tests cover detection, no-backup restore, half-orphan duplicate-key guard, CRLF preservation, disable_response_storage cleanup, and the full self-heal unbind flow.

Confidence Score: 5/5

safe to merge; recovery logic is correct, the half-orphan duplicate-key fix is test-covered, and CRLF handling is explicit for Windows configs

the core detection and restore functions are well-isolated and tested across all meaningful variants. the only gap is that startup-registration files (launchAgent/Windows startup) are not attempted during orphaned recovery, which could leave a stale startup entry after a crash-orphan — but this doesn't corrupt data or block normal operation

lib/runtime/app-bind.ts — the orphaned self-heal branch at line 809 skips startup file cleanup; worth a follow-up to attempt best-effort deletion of paths.startupPath and paths.launchAgentPath in the existing cleanup loop

Important Files Changed

Filename Overview
lib/runtime/app-bind.ts adds orphaned-bind detection in getAppBindStatus and self-heal recovery in unbindCodexAppRuntimeRotationLocked; startup artifact cleanup is missing in the orphaned path
lib/runtime/config-toml.ts adds configHasRuntimeRotationProvider (detects proxy bind from config directly) and restoreConfigTomlFromRuntimeRotationProviderWithoutBackup (no-backup recovery with CRLF normalization and half-orphan duplicate-key guard)
test/app-bind.test.ts 4 new integration tests covering unmanaged-status detection, self-heal end-to-end, half-orphan duplicate-key guard, and clean-config no-op; all use temp-dir isolation
test/config-toml-restore.test.ts 7 new unit tests covering detection (true/false/empty/stray-mention/half-orphan) and no-backup restore (default provider, custom provider, CRLF, half-orphan duplicate-key, disable_response_storage cleanup)

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[unbindCodexAppRuntimeRotation] --> B[withAppBindLock]
    B --> C[readAppBindState]
    C --> D{state exists?}
    D -- yes --> E[stopRouter / removeAppBindStartup]
    E --> F[readAppBindBackup]
    D -- no --> F
    F --> G{backup exists?}
    G -- yes --> H[restoreConfigTomlFromAppBind\nusing backup content]
    G -- no --> I{state exists?}
    I -- yes --> J[restoreConfigTomlFromAppBind\nusing empty backup]
    I -- no --> K[readConfigIfExists paths.configPath]
    K --> L{configHasRuntimeRotationProvider?}
    L -- yes --> M[restoreConfigTomlFromRuntimeRotationProviderWithoutBackup\nself-heal orphaned bind]
    L -- no --> N[no-op: not configured]
    M --> O[selfHealed = true]
    H --> P[cleanup statePath / backupPath / statusPath]
    J --> P
    O --> P
    N --> P
    P --> Q[getAppBindStatus]
    Q --> R{state === null?}
    R -- yes --> S[readConfigIfExists\nconfigHasRuntimeRotationProvider]
    S --> T[unmanagedBind = true/false]
    R -- no --> U[unmanagedBind = false]
    T --> V[return AppBindStatus]
    U --> V
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[unbindCodexAppRuntimeRotation] --> B[withAppBindLock]
    B --> C[readAppBindState]
    C --> D{state exists?}
    D -- yes --> E[stopRouter / removeAppBindStartup]
    E --> F[readAppBindBackup]
    D -- no --> F
    F --> G{backup exists?}
    G -- yes --> H[restoreConfigTomlFromAppBind\nusing backup content]
    G -- no --> I{state exists?}
    I -- yes --> J[restoreConfigTomlFromAppBind\nusing empty backup]
    I -- no --> K[readConfigIfExists paths.configPath]
    K --> L{configHasRuntimeRotationProvider?}
    L -- yes --> M[restoreConfigTomlFromRuntimeRotationProviderWithoutBackup\nself-heal orphaned bind]
    L -- no --> N[no-op: not configured]
    M --> O[selfHealed = true]
    H --> P[cleanup statePath / backupPath / statusPath]
    J --> P
    O --> P
    N --> P
    P --> Q[getAppBindStatus]
    Q --> R{state === null?}
    R -- yes --> S[readConfigIfExists\nconfigHasRuntimeRotationProvider]
    S --> T[unmanagedBind = true/false]
    R -- no --> U[unmanagedBind = false]
    T --> V[return AppBindStatus]
    U --> V
Loading

Reviews (3): Last reviewed commit: "test(app-bind): assert disable_response_..." | Re-trigger Greptile

…614)

When app-bind rewrote ~/.codex/config.toml to the runtime-proxy provider
but its state/backup files were later lost, the config stayed bound while
`getAppBindStatus` and `unbind-app` — which inferred "bound" purely from
the state files — reported "not configured" and refused to act. The user
was left with a config pointing at a dead proxy port, recoverable only by
hand-editing config.toml.

Fixes:
- config-toml: add `configHasRuntimeRotationProvider()` (detect a bound
  config from the top-level model_provider or the proxy provider block)
  and `restoreConfigTomlFromRuntimeRotationProviderWithoutBackup()` (strip
  the proxy block and fall back the top-level provider to "openai" when no
  original backup exists; pins line endings to the input style).
- app-bind: `unbindCodexAppRuntimeRotationLocked` now self-heals — when
  there is no backup and no state but config.toml is still bound, it
  restores the config and reports the recovery.
- app-bind: `getAppBindStatus` derives `bound` from the config when no
  state file is present and exposes a new `unmanagedBind` flag;
  `formatAppBindStatus` surfaces "bound but unmanaged" with the unbind
  remedy instead of "not configured".

Adds 11 regression tests (detection, no-backup restore incl. CRLF,
unmanaged-status detection, self-heal unbind, clean-config no-op).
Full suite: 4947 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ndycode, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 14 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d86dca8e-3c0d-4d21-aab0-12a9f2871657

📥 Commits

Reviewing files that changed from the base of the PR and between fb3ec8a and a490274.

📒 Files selected for processing (3)
  • lib/runtime/config-toml.ts
  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
📝 Walkthrough

Walkthrough

adds orphaned app-bind recovery for issue #614. two new helpers (configHasRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProviderWithoutBackup) are added to lib/runtime/config-toml.ts. AppBindStatus gains unmanagedBind; getAppBindStatus and unbindCodexAppRuntimeRotation use these helpers to detect and self-heal orphaned config.toml binds that have no state or backup files.

Changes

Orphaned bind detection and self-healing unbind

Layer / File(s) Summary
config.toml orphan-detection and no-backup restore helpers
lib/runtime/config-toml.ts, test/config-toml-restore.test.ts
configHasRuntimeRotationProvider scans config.toml lines for proxy model_provider or provider block. restoreConfigTomlFromRuntimeRotationProviderWithoutBackup synthesizes a minimal original config and delegates to the existing restore routine, normalizing CRLF/LF. tests cover positive/negative detection and restore correctness including CRLF preservation and custom default provider.
AppBindStatus.unmanagedBind, getAppBindStatus detection, self-healing unbind
lib/runtime/app-bind.ts, test/app-bind.test.ts
AppBindStatus adds unmanagedBind: boolean (line 86-92). getAppBindStatus calls configHasRuntimeRotationProvider when state file is absent to set unmanagedBind and bound (line 624-636). formatAppBindStatus emits an early message for unmanaged binds. unbind adds a selfHealed flag (line 787) and a recovery branch that calls the no-backup restore when no backup/state exists but the config is still proxy-bound (line 809-824), updating the result message to reflect the three outcomes (line 840-864). tests seed an orphaned config scenario, assert status/formatting, verify self-healing restores model_provider = "openai" and strips the proxy block, and confirm unbind is a no-op on a clean config.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

bug


review flags

windows/crlf robustness at lib/runtime/config-toml.ts:305-335: synthetic original config is built with bare \n. if restoreConfigTomlFromRuntimeRotationProvider internally joins with \n, the crlf rewrite at the end is your only safety net. the test at test/config-toml-restore.test.ts:150-192 covers the happy path but doesn't exercise a case where the synthetic original itself creates mixed line endings before the restore call — consider adding that edge case to catch a future regression where the synthetic path accidentally introduces crlf/lf inconsistency.

concurrency gap at lib/runtime/app-bind.ts:809-824: the orphaned-bind recovery branch reads config.toml, synthesizes a restore, and writes it back. this path is not inside the existing file lock that guards the normal backup-based restore flow — confirm the lock wraps the full orphaned path too, otherwise two concurrent unbind-app calls could double-write the config.

selfHealed flag error path at lib/runtime/app-bind.ts:787 + 840-864: selfHealed is declared but if the orphaned recovery branch throws (fs write failure, etc.), the message fallthrough will still compute as if the operation succeeded. consider explicit error surfacing instead of silent fallback to "not configured" — a partial self-heal followed by a misleading status message could be worse than a clear error.

missing regression test: no test covers the case where configHasRuntimeRotationProvider returns true but the restoreConfigTomlFromRuntimeRotationProviderWithoutBackup write fails mid-unbind. the status and message after a partial self-heal (e.g., orphan detected, restore called, write throws) is untested — add a scenario where the restore throws and confirm the result message and status are defensible.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning title uses correct conventional-commits format (fix type, scope, lowercase imperative) but exceeds 72-character limit at 74 characters, violating the requirement. shorten to 72 chars or fewer; consider 'fix(app-bind): self-heal orphaned runtime-proxy bind (#614)' (65 chars).
Linked Issues check ⚠️ Warning pr implements core #614 fixes (self-healing unbind, config-scanned status, unmanaged flag) and adds 11 regression tests; however greptile flagged 3 code defects that compromise the fix: duplicate model_provider lines on partial-orphan recovery, missing partial-orphan recovery test, and missing disable_response_storage cleanup test. resolve greptile issues 1–3 before merge: fix duplicate-line bug in restoreConfigTomlFromRuntimeRotationProviderWithoutBackup, add partial-orphan recovery test, add disable_response_storage cleanup test.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed all changes directly address #614 orphaned-bind recovery (detection, self-heal unbind, unmanaged status, regression tests); no out-of-scope additions detected.
Description check ✅ Passed the pull request description is comprehensive and well-structured, covering the problem, fix, testing approach, and release plan with clear references to file changes and specific function additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/614-orphaned-app-bind
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/614-orphaned-app-bind

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ndycode

ndycode commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread lib/runtime/config-toml.ts
Comment thread test/config-toml-restore.test.ts
Comment thread test/config-toml-restore.test.ts
…ery (#614)

Greptile review (P1) on #615: when the proxy provider block was present but
the top-level model_provider already pointed at a non-proxy value (e.g. a
half-orphaned config, or one where unbind partially ran), the no-backup
recovery path duplicated the model_provider key — invalid TOML that codex-cli
refuses to parse, leaving the user worse off.

Root cause was the shared restoreTopLevelModelProvider: its fallback splice
fired whenever no *proxy* model_provider line was found, even when a valid
non-proxy top-level line already existed. Guard the splice so it only inserts
the original line when there is no top-level model_provider at all; an existing
line is left untouched (removing the proxy block is sufficient).

Adds the two P2 tests Greptile requested: half-orphan recovery (asserts a
single model_provider line) and disable_response_storage cleanup in the
no-backup path.

Full suite: 4949 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/runtime/config-toml.ts`:
- Around line 313-334: The root bug is in the
`restoreConfigTomlFromRuntimeRotationProviderWithoutBackup` function at
lib/runtime/config-toml.ts#L313-L334: it unconditionally synthesizes a
`model_provider` line without checking if the current config already has a
non-proxy provider, causing duplicate keys in the output. Fix this by using
`extractTopLevelModelProviderLine` to check if `currentConfig` already contains
a non-proxy top-level `model_provider` before synthesizing the
`syntheticOriginal`; if a non-proxy provider already exists, set
`syntheticOriginal` to an empty string (or just the line ending) to preserve the
existing line instead of duplicating it. At lib/runtime/app-bind.ts#L809-L824,
the same function is called for self-healing; no direct code change is needed
there since the fix propagates from the root cause fix. Add a regression test at
test/config-toml-restore.test.ts#L150-L192 that restores a partial-orphan config
(one with both a non-proxy `model_provider` and a proxy block) and asserts the
output has exactly one `model_provider` line with no proxy block. Add another
regression test at test/app-bind.test.ts#L858-L875 that seeds a partial-orphan
config, calls `unbindCodexAppRuntimeRotation`, and asserts the restored config
has exactly one `model_provider` line (the existing non-proxy one) with no
duplicates and no proxy block.

In `@test/app-bind.test.ts`:
- Around line 858-875: Add a new test case in the test file (in addition to the
existing "self-heals a bound config with no backup/state on unbind" test) that
covers the partial-orphan recovery scenario. Create a test helper function
(similar to seedOrphanedBind) that seeds a config containing both a non-proxy
model_provider line set to "openai" AND a
[model_providers.codex-multi-auth-runtime-proxy] block, then call
unbindCodexAppRuntimeRotation with the same parameters as the existing test.
Assert that the restored config contains exactly one model_provider line with
the value "openai", does not contain any duplicate model_provider entries, and
does not contain the proxy block, to verify that
restoreConfigTomlFromRuntimeRotationProviderWithoutBackup handles this scenario
correctly without creating duplicates.
- Around line 858-875: The test for orphaned-bind recovery is missing a
verification that the injected disable_response_storage = false line is properly
removed during restoration. Add an expect statement to the test between line 858
and 875 that verifies the restored config does not contain the
disable_response_storage = false line, similar to how the test already checks
that codex-multi-auth-runtime-proxy is removed. This ensures that the
restoreTopLevelResponseStorage function properly cleans up the synthetic config
injection when recovering from an orphaned bind state.

In `@test/config-toml-restore.test.ts`:
- Around line 150-192: Add a new test case within the
restoreConfigTomlFromRuntimeRotationProviderWithoutBackup describe block to
verify recovery of a partial-orphan config (one that has a non-proxy
model_provider like "openai" AND a proxy block like
[model_providers.codex-multi-auth-runtime-proxy]). The test should pass a config
with model_provider = "openai" and the proxy block through the
restoreConfigTomlFromRuntimeRotationProviderWithoutBackup function, then assert
that the restored output contains exactly one model_provider line (the existing
"openai" one), does not contain the proxy block, and that
configHasRuntimeRotationProvider returns false for the result.
- Around line 150-192: Add a new test case within the
"restoreConfigTomlFromRuntimeRotationProviderWithoutBackup" describe block that
verifies cleanup of the disable_response_storage setting during orphaned
recovery. Create a test that defines a bound config string containing both the
runtime proxy model_provider and disable_response_storage set to false, calls
restoreConfigTomlFromRuntimeRotationProviderWithoutBackup with this bound
config, and then asserts that the restored result does not contain the
disable_response_storage line. This test should ensure that when the synthetic
original config doesn't include disable_response_storage, the
restoreTopLevelResponseStorage function properly removes this injected setting
during the no-backup restore path.
- Around line 150-192: The test suite is missing coverage for cleanup of the
`disable_response_storage = false` line that gets injected by
`enableTopLevelResponseStorage` during bind. This line should be removed during
restoration via `restoreConfigTomlFromRuntimeRotationProviderWithoutBackup`. In
test/config-toml-restore.test.ts at lines 150-192, add a test case to the
restoreConfigTomlFromRuntimeRotationProviderWithoutBackup describe block that
verifies a bound config containing `disable_response_storage = false` is
properly cleaned up after calling the restore function. In test/app-bind.test.ts
at lines 858-875, add a corresponding test case that verifies the same cleanup
occurs during orphaned recovery when calling `unbindCodexAppRuntimeRotation`,
ensuring that the injected line doesn't leave residue in either recovery
scenario.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ebb996a0-942f-474a-971d-b3c0f929761a

📥 Commits

Reviewing files that changed from the base of the PR and between c66731e and fb3ec8a.

📒 Files selected for processing (4)
  • lib/runtime/app-bind.ts
  • lib/runtime/config-toml.ts
  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (14)
test/**/*.test.ts

📄 CodeRabbit inference engine (test/AGENTS.md)

test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
!{dist/**,**/.*,**/node_modules/**}

📄 CodeRabbit inference engine (AGENTS.md)

Store source code in root index.ts, lib/, and scripts/ directories; never edit dist/ or local temp/cache directories as they are generated output

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
**/*.{ts,tsx,js,mjs,cjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only with "type": "module" configuration and require Node >= 18.17

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use TypeScript type assertions (as any, @ts-ignore, or @ts-expect-error)

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
**/*.{js,ts,tsx,jsx,json}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts,tsx,jsx,json}: Use CODEX_MULTI_AUTH_DIR environment variable to override the default settings and accounts storage root from ~/.codex/multi-auth/
Use CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0/1 to disable or enable the default-on live Responses proxy rotation for forwarded Codex CLI/app sessions
Keep CODEX_MODE=0/1 environment variable for disabling/enabling Codex mode in runtime

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (README.md)

**/*.{js,ts,tsx,jsx}: Implement stateful Responses background: true compatibility only when opt-in via backgroundResponses setting or CODEX_AUTH_BACKGROUND_RESPONSES=1 environment variable
Set request timeout behavior using CODEX_AUTH_FETCH_TIMEOUT_MS and stream stall timeout using CODEX_AUTH_STREAM_STALL_TIMEOUT_MS environment variables
Use CODEX_TUI_V2=0/1, CODEX_TUI_COLOR_PROFILE, and CODEX_TUI_GLYPHS environment variables to control terminal UI appearance and capabilities
Store accounts data in openai-codex-accounts.json file within the configured storage root, with per-project account files under ~/.codex/multi-auth/projects/<project-key>/
Store flagged accounts separately in openai-codex-flagged-accounts.json to support account recovery and repair workflows
Implement quota caching by writing to and reading from ~/.codex/multi-auth/quota-cache.json
Track runtime observability metrics in ~/.codex/multi-auth/runtime-observability.json with request counters, budget state, and multi-auth probe visibility
Record local usage in ~/.codex/multi-auth/usage/usage-ledger.jsonl using JSONL (newline-delimited JSON) format for each usage event
Store account policies in ~/.codex/multi-auth/account-policies.json to enable policy controls and account capability views
Store routing profiles in ~/.codex/multi-auth/routing-profiles.json for multi-auth routing configuration
Store budget guards in ~/.codex/multi-auth/budget-guards.json to implement runtime budget constraints and prevent over-spending
Store local client bridge tokens in ~/.codex/multi-auth/local-client-tokens.json with hashed values for /health, /v1/models, and /v1/responses endpoint protection
Implement all dashboard navigation using the specified hotkeys: Up/Down for movement, Enter for selection, 1-9 for quick switch, / for search, ? for help, Q for back/cancel
Implement account details dashboard with the specified hotkeys: S for set account, R for refresh/re-login, E fo...

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
**

⚙️ CodeRabbit configuration file

**: # PROJECT KNOWLEDGE BASE

Generated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.1

OVERVIEW

codex-multi-auth is a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installed codex-multi-auth entrypoint handles account-management commands locally, codex-multi-auth-codex forwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.

STRUCTURE

./
├── scripts/
│   ├── codex.js              # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup
│   ├── codex-multi-auth.js   # standalone package CLI entrypoint
│   ├── codex-routing.js      # auth command and compatibility alias routing
│   ├── codex-bin-resolver.js # official Codex binary discovery
│   ├── codex-app-router.js   # persistent localhost router for packaged Codex app bind
│   └── codex-app-launcher.js # reversible user-level app launcher routing helper
├── index.ts                  # optional plugin-host runtime entry
├── lib/                      # core runtime logic (see lib/AGENTS.md)
│   ├── auth/                 # OAuth flow, PKCE, callback server
│   ├── runtime/              # Codex CLI/app integration helpers, app bind, live sync, runtime observability
│   ├── request/              # request transform, SSE, failover, backoff
│   ├── storage/              # path resolution, migrations, backups, restore, import/export
│   ├── codex-cli/            # Codex CLI state sync and writer helpers
│   ├── codex-manager/        # command modules and settin...

Files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
lib/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/*.ts: Runtime rotation code must preserve pass-through semantics except for auth/provider headers that intentionally change
Never import from dist/ in source tests or library code

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
lib/**/runtime/**/*.ts

📄 CodeRabbit inference engine (lib/AGENTS.md)

lib/**/runtime/**/*.ts: Runtime proxy client-facing headers must not expose account emails or tokens
Runtime rotation should fail open to normal official Codex forwarding when startup helpers are unavailable
Never add account emails/tokens to runtime proxy client responses

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (lib/AGENTS.md)

Never suppress type errors

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
**/runtime/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not expose account emails or tokens in runtime proxy client response headers or logs

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
{lib/runtime/**/*.ts,lib/codex-manager/commands/status.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Runtime rotation provider ID is codex-multi-auth-runtime-proxy and status command is codex-multi-auth rotation status

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/runtime/config-toml.ts
  • lib/runtime/app-bind.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.

Applied to files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.

Applied to files:

  • test/app-bind.test.ts
  • test/config-toml-restore.test.ts
🔇 Additional comments (9)
lib/runtime/config-toml.ts (1)

275-303: LGTM!

test/config-toml-restore.test.ts (1)

119-148: LGTM!

lib/runtime/app-bind.ts (4)

12-14: LGTM!


86-92: LGTM!


624-640: LGTM!


855-864: LGTM!

test/app-bind.test.ts (3)

12-12: LGTM!


818-856: LGTM!


877-902: LGTM!

Comment thread lib/runtime/config-toml.ts
Comment thread test/app-bind.test.ts
Comment thread test/config-toml-restore.test.ts
Greptile/CodeRabbit follow-up: add an integration-level regression that
drives the partial-orphan case (proxy block present, top-level
model_provider already native) through unbindCodexAppRuntimeRotation and
asserts exactly one model_provider line survives — complementing the
unit-level coverage of restoreConfigTomlFromRuntimeRotationProviderWithoutBackup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ndycode

ndycode commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai the duplicate-model_provider P1 you flagged was reviewed against the original commit fb3ec8a. It's already fixed as of 46c2bdc and further covered by cd4c5d7:

  • Root fix (46c2bdc): restoreTopLevelModelProvider now only splices a top-level model_provider when none exists; a half-orphan (proxy block present, top-level already openai) keeps its single line and just drops the block. No duplicate key.
  • Tests: unit-level half-orphan + disable_response_storage cleanup (46c2bdc), and an integration-level half-orphan through unbindCodexAppRuntimeRotation (cd4c5d7) asserting exactly one model_provider line.

Full suite green (4949+). Please re-review at cd4c5d7.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current state of restoreTopLevelModelProvider in config-toml.ts
rg -n "restoreTopLevelModelProvider\|configHasRuntimeRotationProvider\|restoreConfigTomlFromRuntimeRotationProviderWithoutBackup\|model_provider" lib/runtime/config-toml.ts

echo "---"
# Check new tests for half-orphan and disable_response_storage in config-toml-restore test
rg -n "half.orphan\|disable_response_storage\|exactly one\|model_provider" test/config-toml-restore.test.ts

echo "---"
# Check integration-level half-orphan in app-bind test
rg -n "half.orphan\|exactly one\|model_provider\|unbindCodexApp" test/app-bind.test.ts

Length of output: 872


🏁 Script executed:

#!/bin/bash
# Find relevant source and test files
fd -e ts "config-toml|app-bind" --full-path
echo "==="
fd -e ts "config.toml|app.bind" --full-path
echo "==="
# Also check git log to confirm commits
git log --oneline -10
echo "==="
# Find any ts files in lib/runtime
fd -e ts . lib/ 2>/dev/null | head -30
echo "==="
fd -e ts . test/ 2>/dev/null | head -30

Length of output: 3208


🏁 Script executed:

#!/bin/bash
echo "=== config-toml.ts: restoreTopLevelModelProvider and related ==="
grep -n "restoreTopLevelModelProvider\|model_provider\|configHasRuntimeRotation\|restoreConfigToml\|disable_response_storage\|half.orphan\|RUNTIME_ROTATION_PROXY" lib/runtime/config-toml.ts | head -60

echo "=== full restoreTopLevelModelProvider function ==="
grep -n "" lib/runtime/config-toml.ts | sed -n '/restoreTopLevelModelProvider/,/^[0-9]*:}/p' | head -40

echo "=== config-toml-restore.test.ts: new tests ==="
grep -n "half.orphan\|disable_response_storage\|exactly one\|model_provider\|without.backup\|CRLF\|it(\|describe(" test/config-toml-restore.test.ts | head -50

echo "=== app-bind.test.ts: new integration tests ==="
grep -n "half.orphan\|exactly one\|model_provider\|it(\|describe(" test/app-bind.test.ts | head -50

Length of output: 12362


🏁 Script executed:

#!/bin/bash
echo "=== app-bind.test.ts half-orphan test full body ==="
sed -n '877,930p' test/app-bind.test.ts

echo "=== config-toml-restore.test.ts no-backup tests ==="
sed -n '150,230p' test/config-toml-restore.test.ts

Length of output: 5333



@ndycode verified at cd4c5d7. all three concerns are resolved:

  1. duplicate model_provider (P1)lib/runtime/config-toml.ts:146-153: restoreTopLevelModelProvider now walks output lines up to the first section header checking for any existing model_provider =. if one is found (the half-orphan case), the splice is skipped entirely. the guard comment at :140-145 documents the invariant clearly. fix is correct.

  2. missing half-orphan recovery test (P2) — covered at two levels:

    • unit: test/config-toml-restore.test.ts "does not duplicate model_provider when the top-level line is already non-proxy (half-orphan)" — asserts providerLines === 1 directly against restoreConfigTomlFromRuntimeRotationProviderWithoutBackup.
    • integration: test/app-bind.test.ts:877 "self-heals a half-orphan … without duplicating keys" — exercises the full unbindCodexAppRuntimeRotation path, asserts regex count of model_provider = lines is exactly 1, no proxy strings remain, bound === false.
  3. disable_response_storage cleanup (P3)test/config-toml-restore.test.ts "drops bind-injected disable_response_storage during no-backup recovery" covers the case where a bound config carries the injected line and recovery strips it.

no outstanding gaps. happy to approve.

[approve]

…ind (#614)

CodeRabbit follow-up: the orphaned-bind fixture now also carries the
bind-injected `disable_response_storage = false` line, and the self-heal
unbind test asserts it is removed during recovery (integration-level
coverage to match the unit-level test in config-toml-restore).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ndycode
ndycode merged commit cd1a700 into main Jun 16, 2026
1 check passed
ndycode added a commit that referenced this pull request Jun 16, 2026
Patch release with self-healing recovery for an orphaned runtime-proxy
app-bind (#614, #615). No runtime-rotation, storage, or auth behavior
changed.

- Bump version to 2.3.2 across package.json, package-lock.json,
  .codex-plugin/plugin.json, and AGENTS.md
- Add docs/releases/v2.3.2.md
- Promote v2.3.2 to current stable in README and docs portal; demote
  v2.3.1 to prior stable
- Add CHANGELOG entry

Full suite green (4950 passed, 3 skipped); typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] first-run auto app-bind can orphan ~/.codex/config.toml with no restorable backup

1 participant