Skip to content

fix(catalog): skip byte-identical catalog writes so a resync cannot mark Codex stale - #1460

Closed
tlsdnwn55 wants to merge 1 commit into
lidge-jun:devfrom
tlsdnwn55:fix/no-op-catalog-resync
Closed

fix(catalog): skip byte-identical catalog writes so a resync cannot mark Codex stale#1460
tlsdnwn55 wants to merge 1 commit into
lidge-jun:devfrom
tlsdnwn55:fix/no-op-catalog-resync

Conversation

@tlsdnwn55

@tlsdnwn55 tlsdnwn55 commented Aug 11, 2026

Copy link
Copy Markdown

Summary

writeRetainedCatalogSync wrote the Codex catalog unconditionally — replaceActiveCodexCatalog
atomicWriteFile, with no comparison against the bytes already on disk — so every sync moved the
catalog file's mtime even when the produced content was identical and the advertised model set did
not change.

collectCodexAppServerCatalogState() compares that mtime against each running Codex's start time.
An ordinary ocx start, or any dashboard/CLI action that re-syncs an unchanged model set, therefore
classified every already-running Codex as holding an outdated in-memory catalog. Since #1407 that
verdict withholds all opencodex-authored v2 model guidance, so a configured injectionModel and
subagentModels roster silently stop reaching the session for the rest of that Codex's lifetime,
even though nothing about the catalog changed. A long-lived Codex App app-server outlives every
proxy restart, which makes the state effectively latched: restarting the Codex CLI does not clear it.

This PR skips the write when the prepared bytes equal what is on disk, and reports
catalogWritten: false for that case.

  • The comparison is byte-exact, not a decoded-string comparison. readFileSync(path, "utf8")
    replaces every invalid sequence with U+FFFD, so a malformed byte on disk would compare equal to
    a legitimately encoded replacement character in the prepared content; the skip would then preserve
    the corruption and report catalogWritten: false for a file that is not what we prepared. An
    unreadable or absent file still falls back to a real write.
  • added still reports the routed rows the catalog carries, because they are on disk either way.
  • cacheSynced is unaffected: refreshCodexModelCatalog invalidates the models cache whenever the
    catalog exists, independently of whether it was rewritten. The stale-app-server hint and the
    startup cache-invalidation branch both key on cacheSynced as well, so their behaviour is
    unchanged.
  • #1046 covered warning about stale app-servers after a catalog rewrite; it did not cover
    avoiding the rewrite when there is nothing to write.

Closes #1459

Verification

  • bun x tsc --noEmit — clean.
  • bun test tests/codex-catalog-sync-hardening.test.ts tests/codex-convergence-account-selectors.test.ts tests/codex-refresh.test.ts tests/codex-sync-api.test.ts tests/codex-catalog-restore.test.ts tests/codex-retained-root-serialization.test.ts
    — 72 pass, 0 fail.
  • bun run test (full suite) — 10871 pass, 7 skip, 1 fail. The single failure,
    server combo failover 030 activation matrix > connect cancellation wins with 499, no backup, warning, or cooldown (30s hook timeout), reproduces identically with this branch's changes
    stashed on unmodified dev, and touches neither catalog sync nor guidance. That run predates the
    rebase onto current dev and the byte-exactness change.
  • bun run privacy:scan — passed.
  • Both regressions were driven red against the behaviour they describe:
    • removing the equal-content guard fails secondWritten (Expected: false, Received: true), and
      the same test mutates the catalog on disk and asserts the next sync does rewrite it, so the
      guard is not vacuous;
    • restoring the decoded-string comparison fails
      a malformed on-disk byte that decodes to the same string is still repaired on written
      (Expected: true, Received: false). That test corrupts one byte inside a preserved JSON string
      value to 0x80, then asserts the repaired file differs in bytes while decoding to the same
      string as the corrupted file — exactly the pair a string comparison equates — and that the
      result is valid UTF-8 containing EF BF BD.
  • Reproduced on macOS 15.3.1 with @bitkyc08/opencodex 2.12.0, Codex CLI 0.147.0
    (originator: codex-tui), Codex App running: collectCodexAppServerCatalogState() reported
    stale solely because of the Codex App app-server (started 02:43:11Z) against a catalog whose
    mtime had been moved to 02:52:27Z by a resync, while both live CLI-owned processes were newer.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed — the sub-agent surface guide's catalog-state
    section now states that a byte-identical resync leaves the file untouched.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. No auth,
    credential, workflow, or release-automation surface is touched. The write path keeps its
    existing permit check, write serialization, and atomic-write behaviour; the only change is an
    equal-content short-circuit evaluated inside the same serialized region.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes

    • Catalog synchronization now avoids rewriting unchanged catalog files, preserving their timestamps.
    • Invalid file contents are repaired reliably, including malformed UTF-8 data.
    • Catalog updates continue to report routed and account-bound entries even when no rewrite is needed.
  • Documentation

    • Clarified when catalog timestamps change and how unchanged synchronization affects running app-server freshness.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a27e48bb-f1df-4d23-ac9a-d9063f8dd346

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0fee6 and 642805c.

📒 Files selected for processing (1)
  • src/codex/catalog/sync.ts

📝 Walkthrough

Walkthrough

Catalog synchronization now compares generated catalog bytes with the existing file before writing. Unchanged syncs preserve the file modification time and report catalogWritten: false. Tests cover real changes, invalid UTF-8 corruption, convergence, and retained-sync behavior. Documentation reflects the timestamp behavior.

Changes

Catalog synchronization

Layer / File(s) Summary
Byte-level catalog comparison
src/codex/catalog/sync.ts
The sync reads exact catalog bytes and compares them with the serialized catalog. Matching bytes skip replacement and return catalogWritten: false.
Synchronization and convergence validation
tests/codex-catalog-sync-hardening.test.ts, tests/codex-convergence-account-selectors.test.ts
Tests verify modification-time preservation for unchanged syncs, rewrites for real changes and invalid UTF-8 corruption, and no-write convergence behavior.
Catalog timestamp documentation
docs-site/src/content/docs/guides/sub-agent-surface.md
The guide states that byte-identical syncs leave the catalog file unchanged.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • lidge-jun/opencodex#1395: The byte-exact comparison prevents unchanged catalog rewrites, but it does not address the retrieved issue's machine-wide stale-state guidance bug.

Possibly related PRs

Suggested labels: documentation

Suggested reviewers: lidge-jun, chrisae9, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 summarizes the primary fix: skipping byte-identical catalog writes to prevent false Codex staleness.
Linked Issues check ✅ Passed The implementation and tests satisfy issue #1459 by preserving unchanged catalog bytes and metadata while repairing missing or corrupted files.
Out of Scope Changes check ✅ Passed The code, tests, and documentation changes directly support the catalog write optimization and stale app-server fix described in issue #1459.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 added the bug Something isn't working label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

4/4 boxes ticked.

This pull request is already Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently. If no CodeRabbit review appears, comment @coderabbitai review to request one.
Maintainers: @lidge-jun @Ingwannu @Wibias

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The reported issue is real, and this is the right minimal direction: avoiding a true no-op write keeps the catalog mtime meaningful without weakening the existing permit, revalidation, or atomic-write path. I reproduced the focused suite on exact PR head 7bf8b84a after installing the locked dependencies: 100 pass / 0 fail; typecheck and privacy scan also pass.

I am requesting one correctness change before this becomes review-ready:

currentCatalogFileContent() in src/codex/catalog/sync.ts claims to compare exact bytes, but readFileSync(path, "utf8") === content compares decoded JavaScript strings. Invalid UTF-8 bytes are replaced with U+FFFD, so different on-disk bytes can compare equal to a legitimately encoded replacement character. I reproduced the boundary directly: a buffer containing raw byte 0x80 and a buffer containing UTF-8 EF BF BD have equal decoded strings but unequal bytes. In a catalog string field, this path can therefore preserve malformed bytes, skip the atomic repair write, and return catalogWritten: false even though the prepared bytes are not identical to the file.

Please read the file as bytes and compare it with the UTF-8 bytes of content (Buffer.equals or an equivalent exact Uint8Array comparison), while preserving the current unreadable/absent fallback to a real write. Add a regression where an invalid on-disk byte decodes to the same string as a real U+FFFD in the prepared content and assert that the sync still rewrites it. The existing mtime no-op regression should continue to prove that genuinely identical bytes are skipped.

The branch is currently four commits behind dev, still inside the repository's accepted freshness window. Once the exact-byte boundary is fixed, the focused checks are rerun, and the four readiness boxes are completed against the resulting head, this remains a strong merge candidate for #1459.

@tlsdnwn55

Copy link
Copy Markdown
Author

@Ingwannu thanks — the finding is correct and is fixed on new head 0b0fee65.

currentCatalogFileContent() is replaced by currentCatalogFileBytes(), which returns the raw Buffer; the skip now requires onDiskBytes.equals(Buffer.from(content, "utf8")). The unreadable/absent fallback to a real write is unchanged, and the check still runs inside the same serialized region under the existing permit.

Regression added: a malformed on-disk byte that decodes to the same string is still repaired. It settles the catalog, corrupts one byte inside a preserved JSON string value to 0x80, and asserts that the next sync rewrites the file (catalogWritten: true), that the repaired bytes differ from the corrupted file while decoding to the same string — the exact pair a decoded-string comparison equates — and that the result is valid UTF-8 containing EF BF BD. Restoring onDiskBytes.toString("utf8") === content fails it on written (Expected: true, Received: false), so it is not vacuous.

Re-verified on 0b0fee65: bun x tsc --noEmit clean; bun test on codex-catalog-sync-hardening, codex-convergence-account-selectors, codex-refresh, codex-sync-api, codex-catalog-restore, codex-retained-root-serialization — 72 pass / 0 fail; bun run privacy:scan passed.

The branch was force-pushed to rewrite the commit author, so the SHA you reviewed is superseded. Sorry for the extra round.

@tlsdnwn55

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 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.

…ark Codex stale

`writeRetainedCatalogSync` wrote the catalog unconditionally, with no content
comparison, so every sync moved the file's mtime even when the produced bytes
were identical to what was already on disk.

`collectCodexAppServerCatalogState()` compares that mtime against each running
Codex's start time, so an ordinary `ocx start` — or any dashboard action that
re-syncs an unchanged model set — classified every already-running Codex as
holding an outdated in-memory catalog. Since lidge-jun#1407 that verdict withholds all
opencodex-authored v2 model guidance, so a configured `injectionModel` and
roster silently stop reaching sessions for the rest of that Codex's lifetime,
even though the advertised model set never changed. A long-lived Codex App
app-server outlives every proxy restart, which makes the state effectively
latched: restarting the CLI does not clear it.

An identical write is now skipped and reported as `catalogWritten: false`.
`added` still reports the routed rows the catalog carries, because they are on
disk either way, and `cacheSynced` is unaffected (`refreshCodexModelCatalog`
invalidates the models cache whenever the catalog exists, independently of
whether it was rewritten).

The comparison is byte-exact, not a decoded-string comparison. Reading the file
as UTF-8 replaces every invalid sequence with U+FFFD, so a malformed byte on
disk would compare equal to a legitimately encoded replacement character in the
prepared content: the skip would then preserve the corruption and report
`catalogWritten: false` for a file that is not what we prepared. An unreadable
or absent file still falls back to a real write.

The convergence/retained-sync byte-agreement assertions asserted
`catalogWritten: true` for exactly the no-op case while also asserting the bytes
did not change; they now assert the no-op they describe (no policy skip, no
write, bytes unchanged). Assertions covering syncs that really change content
are untouched.
@tlsdnwn55
tlsdnwn55 force-pushed the fix/no-op-catalog-resync branch from 0b0fee6 to 642805c Compare August 11, 2026 08:55
@tlsdnwn55

Copy link
Copy Markdown
Author

Rebased onto the current dev head 38c9b355 (no conflicts; the diff is unchanged from 0b0fee65, which already carried the byte-exact comparison and its regression). New head: 642805c1.

Verified locally on 642805c1:

  • bun x tsc --noEmit — clean
  • bun run privacy:scan — passed
  • bun test on the affected subsystems — 292 pass / 1 skip / 0 fail:
    codex-catalog-sync-hardening, codex-convergence-account-selectors, codex-refresh, codex-sync-api, codex-catalog-restore, codex-retained-root-serialization, codex-app-server-processes, codex-desired-state, codex-models-cache-invalidate, codex-native-residue, injection-model-api, multi-agent-compat, combos

Scope note on the full-suite claim, so the attestation is precise: the last full bun run test I ran was on the pre-rebase branch (10871 pass / 7 skip / 1 fail — server combo failover 030 activation matrix > connect cancellation wins with 499, no backup, warning, or cooldown, a 30s hook timeout that reproduces identically on unmodified dev with my changes stashed). It predates the rebase onto 38c9b355. Since then I have run the focused set above rather than the whole suite; the diff itself is byte-identical to the reviewed 0b0fee65, and it touches only the catalog write path plus its tests and one docs paragraph.

@github-actions
github-actions Bot marked this pull request as ready for review August 11, 2026 08:58
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR head and changed files ==='
git rev-parse HEAD
git diff --name-status HEAD^1 HEAD

echo '=== Diff ==='
git diff --find-renames --unified=80 HEAD^1 HEAD -- \
  src/codex/catalog/sync.ts \
  tests/codex-catalog-sync-hardening.test.ts \
  tests/codex-convergence-account-selectors.test.ts \
  docs-site/src/content/docs/guides/sub-agent-surface.md

echo '=== sync.ts outline ==='
ast-grep outline src/codex/catalog/sync.ts --items all --view expanded

echo '=== Catalog write/read references ==='
rg -n -C 4 -P '\b(currentCatalogFileBytes|currentCatalogFileContent|catalogWritten|writeFile|readFile|withPermit|permit)\b' \
  src/codex/catalog/sync.ts tests/codex-catalog-sync-hardening.test.ts tests/codex-convergence-account-selectors.test.ts

Length of output: 78987


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== syncCatalogModels commit path ==='
sed -n '1450,1520p' src/codex/catalog/sync.ts

echo '=== Catalog replacement primitive definitions and callers ==='
rg -n -C 8 -P '\b(function|const)\s+(replaceActiveCodexCatalog|withCatalogWriteSerialization)\b|\breplaceActiveCodexCatalog\s*\(|\bwithCatalogWriteSerialization\s*\(' src

echo '=== catalogWritten consumers ==='
rg -n -C 8 -P '\bcatalogWritten\b' --glob '!tests/**' .

echo '=== Refresh and cache synchronization entry points ==='
rg -n -C 6 -P '\b(syncCatalogModels|cacheSynced|invalidateCodexModelsCache|refreshCodexModelCatalog)\s*\(' src

Length of output: 80627


@github-actions[bot] Review completed for PR #1460.

I found no blocking issues.

The update in src/codex/catalog/sync.ts compares raw on-disk bytes with the prepared UTF-8 bytes inside the existing serialized write section. It preserves the write fallback for absent or unreadable files. It also preserves the existing models-cache invalidation behavior.

The tests cover unchanged catalog bytes, real catalog changes, malformed UTF-8 with replacement-decoding equivalence, and convergence output.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@github-actions[bot] Acknowledged. PR #1460 is marked ready for review at 642805c11e1abbb019c5d3b03b5bc101117df205.

You are interacting with an AI system.

@lidge-jun

Copy link
Copy Markdown
Owner

Landed on dev — closing this pull request as superseded rather than merged, because the change is already there and the branch now conflicts with it.

Your fix is on dev as c7eec01ca with your authorship intact, plus a follow-up correcting the comparison. The reviewer's blocker was real: currentCatalogFileContent() used readFileSync(path, "utf8") === content, which compares decoded strings, so a catalog holding a bare 0x80 compares equal to prepared content holding a legitimately encoded U+FFFD — the guard would have preserved the corruption and reported catalogWritten: false for a file that is not what we prepared. Reproduced directly: {"decodedEqual":true,"bytesEqual":false}.

You independently reached the same Buffer.equals fix on your newer head, which is why this is closing as superseded rather than as something we had to replace. The landed version differs from yours only in the helper name (currentCatalogFileContent vs currentCatalogFileBytes) and comment wording.

Regression on dev converges the catalog on a retained description carrying a real U+FFFD, corrupts exactly that EF BF BD into a bare 0x80, and asserts the pair decodes equal while differing in bytes, that the sync rewrites and repairs it, and that a genuinely byte-identical resync is still skipped. Verified red against the string comparison and green after.

Thank you — this was a good catch and a clean fix.

@lidge-jun lidge-jun closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants