Skip to content

fix(codex): warn when a startup write leaves an app-server stale (#1046) - #1072

Open
lidge-jun wants to merge 1 commit into
devfrom
codex/1046-startup-stale-app-server
Open

fix(codex): warn when a startup write leaves an app-server stale (#1046)#1072
lidge-jun wants to merge 1 commit into
devfrom
codex/1046-startup-stale-app-server

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Service startup rewrites the Codex catalog and the models cache, but an app-server that booted earlier keeps its own in-memory model list — Codex builds a static models manager from the catalog once and never rereads the file. Every check a user runs reads the file; the picker renders memory. The reporter pinned it with a two-host comparison: their catalog was rewritten 4m27s after the app-server started, and killing the app-server made the routed models reappear.

afterCatalogWriteHandleAppServers() already exists, but only runs from explicit ocx sync / sync-cache. Startup never called it.

The obvious fix is unsafe, and that shaped this one. That handler's restart: true branch SIGTERMs matching app-servers and says in its own log line that active turns may be interrupted. Wiring it into an unattended boot would kill a user's in-flight turn on every login, repair, or update. A human typing --restart-codex is consenting to that; a boot is not.

So this adds a warning-only counterpart that cannot reach the signal path — asserted by an injected kill that fails the test if it is ever called. It also asks the existing mtime classifier whether an app-server is genuinely stale, rather than warning about any running one, so a boot with Codex open and a current catalog stays quiet.

Two startup sites write, and neither warns on its own:

  1. startServer() invalidates the models cache during boot.
  2. handleStart runs the catalog sync moments later.

handleStart ORs their results and warns once, after both. Warning after the first would read a catalog mtime the second is about to move, and two independent calls would warn twice. Making that observable required typing the startup sync seam, which had been discarding syncModelsToCodex()'s result and returning a bare boolean meaning "the integration was enabled" — so "a write happened" was not observable at the startup boundary at all.

The memoized catalog state is dropped before the check. It caches for 5s on the default path, so a fresh reading taken before the write would otherwise be replayed after it and this would stay silent about the staleness it just created.

Failure is swallowed throughout: startup synchronization is best-effort and must not stop the proxy from coming up.

Closes #1046.

Verification

$ bun run typecheck
(clean)

$ bun test tests/codex-app-server-processes.test.ts tests/codex-desired-state.test.ts tests/codex-catalog-sync-hardening.test.ts tests/codex-catalog-writer.test.ts tests/service-lifecycle.test.ts
 74 pass, 1 skip, 0 fail

$ bun run privacy:scan
Privacy scan passed

Red-green, per branch rather than per suite:

  • Warning on any state instead of only stale fails the quiet-path test.
  • Removing the catch fails the swallowed-failure test.
  • The injected kill assertion passes only because no startup path reaches restartCodexAppServers().

One honest limit recorded rather than papered over: the 5s memo is only consulted when every io field is defaulted, so an io-injected test bypasses it and cannot reproduce the masking. Rather than write a test that looks like proof and cannot fail, the memo/invalidation mechanism the fix depends on is asserted directly.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features

    • Added startup detection for stale Codex app servers after synchronization.
    • Displays a warning when stale processes are detected.
    • Startup synchronization now reports cache and catalog update status.
  • Bug Fixes

    • Prevented startup failures when process discovery or synchronization checks encounter errors.
    • Avoided warnings for fresh, inactive, or unknown processes.
  • Tests

    • Added coverage for stale-process warnings, cache invalidation, write tracking, and graceful failure handling.

Service startup rewrites the catalog and the models cache, but an app-server
that booted earlier keeps its own in-memory model list - Codex builds a static
manager from the catalog once and never rereads the file. Every check a user
runs reads the file; the picker renders memory. The reporter pinned it exactly:
their catalog was rewritten four minutes after the app-server started.

afterCatalogWriteHandleAppServers already exists but only runs from explicit
sync and sync-cache, and its restart branch SIGTERMs matching processes while
saying active turns may be interrupted. That is fine when a human typed
--restart-codex. It is not fine on a login. So this adds a warning-only
counterpart that can never reach the signal path, asserted by an injected kill
that fails the test if called.

It also asks the mtime classifier whether an app-server is actually stale
rather than warning about any running one, so a boot with Codex open and a
current catalog stays quiet.

Two startup sites write: the server's cache invalidation and the later catalog
sync. Neither warns on its own - handleStart ORs their results and warns once,
after both, because warning after the first would read an mtime the second is
about to move. Making that observable meant typing the startup sync seam, which
had been discarding its result and returning a bare boolean.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Startup synchronization now reports catalog and cache writes. Server startup records cache rewrites. The CLI invokes a failure-safe stale Codex app-server check after startup writes and logs one warning for stale processes.

Changes

Startup stale app-server handling

Layer / File(s) Summary
Track startup write outcomes
src/codex/desired-state.ts, src/server/index.ts, tests/codex-desired-state.test.ts
Startup synchronization returns ran, catalogWritten, and cacheSynced. Server cache invalidation records and consumes whether a rewrite completed successfully. Tests cover disabled, successful, and failed synchronization.
Detect stale app-server processes
src/codex/app-server-processes.ts, tests/codex-app-server-processes.test.ts
The warning formatter accepts PID-only records. The startup helper resets catalog state, checks current process state, warns only for stale processes, never signals processes, and swallows discovery failures.
Wire warning into startup
src/cli/index.ts
Startup consumes the cache-write marker and invokes one stale-process warning when cache invalidation or catalog synchronization writes occur.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Startup
  participant CodexSync
  participant CacheState
  participant StaleCheck
  participant AppServers
  Startup->>CodexSync: run startup synchronization
  CodexSync-->>Startup: return catalog/cache write outcomes
  Startup->>CacheState: consume cache rewrite status
  Startup->>StaleCheck: check after a startup write
  StaleCheck->>AppServers: collect process catalog state
  AppServers-->>StaleCheck: return stale or non-stale state
  StaleCheck-->>Startup: return warned status
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the startup stale-app-server warning added for Codex writes.
Linked Issues check ✅ Passed The changes address issue #1046 by warning after startup catalog or cache writes, checking staleness, preserving startup, and avoiding process termination.
Out of Scope Changes check ✅ Passed All production and test changes support startup write tracking and stale Codex app-server warnings described in issue #1046.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/1046-startup-stale-app-server

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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 5, 2026

@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: 1

🤖 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 `@tests/codex-desired-state.test.ts`:
- Around line 195-204: Add a focused test near the existing
syncCodexOnStartIfEnabled tests where the callback returns { catalogWritten:
true, cacheSynced: true }, then assert the function’s result preserves both
flags. Keep the existing undefined and thrown-callback coverage unchanged and
verify the positive outcome directly.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 537753c7-5e2e-4150-aeef-0f637f0a2bc8

📥 Commits

Reviewing files that changed from the base of the PR and between 0e92714 and 254db13.

📒 Files selected for processing (6)
  • src/cli/index.ts
  • src/codex/app-server-processes.ts
  • src/codex/desired-state.ts
  • src/server/index.ts
  • tests/codex-app-server-processes.test.ts
  • tests/codex-desired-state.test.ts

Comment on lines +195 to +204
async () => { calls += 1; return undefined; },
);
expect(ran).toBe(true);
expect(ran.ran).toBe(true);
expect(calls).toBe(1);
}
});

test("the port reaches the sync", async () => {
const ports: number[] = [];
await syncCodexOnStartIfEnabled(43210, {}, async port => { ports.push(port); });
await syncCodexOnStartIfEnabled(43210, {}, async port => { ports.push(port); return undefined; });

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add positive write-outcome coverage.

These tests only cover undefined and thrown synchronization callbacks. Add a callback that returns { catalogWritten: true, cacheSynced: true }. Assert that syncCodexOnStartIfEnabled preserves both flags.

Without this test, a false-valued mapping can disable the startup warning gate while the changed tests still pass.

As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.”

🤖 Prompt for 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.

In `@tests/codex-desired-state.test.ts` around lines 195 - 204, Add a focused test
near the existing syncCodexOnStartIfEnabled tests where the callback returns {
catalogWritten: true, cacheSynced: true }, then assert the function’s result
preserves both flags. Keep the existing undefined and thrown-callback coverage
unchanged and verify the positive outcome directly.

Source: Path instructions

@lidge-jun

Copy link
Copy Markdown
Owner Author

CI on this PR just reproduced #1061 — and it is not caused by this change.

The macOS leg failed on one test: native profile OpenCodex process-exit phases > hard OpenCodex process exit after each published transaction phase…, at tests/native-profile-crash-boundaries.test.ts:178. This branch touches src/codex/app-server-processes.ts, src/codex/desired-state.ts, src/server/index.ts, src/cli/index.ts and two of their tests. It does not touch that file.

That line is the exact defect #1061 reports and #1071 fixes: the test waits for the settled file to exist, then immediately parses it, so a partially written document satisfies the wait and throws. #1071 replaces that with a wait for parseable content and makes the child publish atomically.

Run: 9018 pass / 8 skip / 1 fail. The single failure is the flake, on the macOS leg, which is where #1061 says it shows up.

So this is load-dependent CI noise against an unrelated pre-existing defect. Two ways forward, whichever the maintainers prefer:

  • land #1071 first and re-run this leg, or
  • re-run the macOS job — it passes on a lucky scheduling.

I have not rebased this branch onto #1071, because chaining an app-server fix behind a test-harness fix would create a dependency that does not exist in the code.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 254db138cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +585 to +586
const first = collectCodexAppServerCatalogState();
const second = collectCodexAppServerCatalogState();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Control time in the memoization test

On slower Windows/macOS CI this new test can become flaky because it calls the real default collector twice and asserts object identity. The collector records the cache timestamp before the cold process scan and only reuses the memo for 5s; its own default path can spend up to ~8s+5s on Windows or ~5s+3s on macOS, so by the time second is collected the cache may already be expired and this recomputes a different object. Use injected time/fake IO or otherwise control the TTL boundary instead of depending on wall-clock duration.

AGENTS.md reference: AGENTS.md:L136-L137

Useful? React with 👍 / 👎.

Comment thread src/cli/index.ts
Comment on lines +326 to +328
if (consumeStartupCacheInvalidationWrite() || startupSync.catalogWritten || startupSync.cacheSynced) {
const { warnIfStaleCodexAppServersAfterStartupWrite } = await import("../codex/app-server-processes");
warnIfStaleCodexAppServersAfterStartupWrite({ log: console });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface the warning from ensure's parent sync

When startup is driven through ocx ensure, this warning only runs inside the detached ocx start child, which is spawned with stdio: "ignore"; the parent then returns as soon as /healthz responds and runs its own syncModelsToCodex(port) without any stale-app-server handler. In that shim/autostart path the warning is either discarded or can run before the parent writes the final catalog, so users can still be left with a stale Codex picker and no actionable message. Mirror this post-write warning around the parent-side ensure sync, or avoid the duplicate parent write.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant