Skip to content

fix: write Codex session cache atomically#295

Open
zerone0x wants to merge 1 commit intoexospherehost:mainfrom
zerone0x:fix/codex-cache-atomic-writes
Open

fix: write Codex session cache atomically#295
zerone0x wants to merge 1 commit intoexospherehost:mainfrom
zerone0x:fix/codex-cache-atomic-writes

Conversation

@zerone0x
Copy link
Copy Markdown

@zerone0x zerone0x commented May 5, 2026

Summary

  • Write Codex session cache updates to a temporary file before atomically renaming into place
  • Clean up the temporary cache file on best-effort write failures
  • Add regression coverage that a discovered Codex transcript is cached without leaving temp files behind

Fixes #276

Test Plan

  • bun run test:run __tests__/lib/codex-sessions.test.ts
  • bun run lint
  • bun run test:run

Summary by CodeRabbit

  • Bug Fixes

    • Improved cache file writing reliability through atomic file operations, preventing potential data loss during concurrent access.
  • Tests

    • Added tests verifying cache file creation, persistence, and cleanup of temporary files.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

writeCacheEntry now uses atomic file writes via a temporary file with PID suffix and renameSync to prevent concurrent writes from clobbering each other's cache entries. Test coverage validates the cache write behavior and temporary file cleanup.

Changes

Atomic Cache Writes

Layer / File(s) Summary
Core Implementation
lib/codex-sessions.ts
writeCacheEntry updated to write to a PID-suffixed temporary file, then atomically rename it into place, replacing the prior direct in-place write. node:fs imports reformatted to multi-line.
Test Infrastructure
__tests__/lib/codex-sessions.test.ts
Test beforeEach now destructures both findCodexTranscript and internal _getCacheFilePath from the module. Node fs imports expanded to include existsSync, readdirSync, readFileSync.
Test Coverage
__tests__/lib/codex-sessions.test.ts
New test verifies findCodexTranscript writes the discovered transcript path into the cache file and confirms no leftover .tmp files remain in the cache directory.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hop and cache, no more race!
Temp files guard each bunny's place,
With atomic renames swift and true,
No more clobbers, just the right view!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title 'fix: write Codex session cache atomically' directly and concisely describes the main change in the PR, which is implementing atomic writes for the Codex session cache.
Description check ✅ Passed The PR description covers the main changes, links the related issue (#276), provides a test plan, but omits the required checklist sections from the template (lint, tsc, test:run, build).
Linked Issues check ✅ Passed The PR implements the core acceptance criteria from #276: writeCacheEntry now writes to a temporary file and atomically renames it into place, eliminating the partial-write window during concurrent access.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: atomic cache writes in lib/codex-sessions.ts and regression test coverage in the test file, with no unrelated modifications.

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@lib/codex-sessions.ts`:
- Around line 63-66: The read-modify-write around readCache() -> modify
cache[sessionId] -> writeFileSync(tmpPath) -> renameSync(CACHE_PATH) is racy and
can clobber concurrent updates; wrap that whole sequence in a cross-process
critical section (e.g., obtain an advisory lock on CACHE_PATH or a dedicated
lockfile with retry/backoff) so only one process reads+updates+renames at a
time, or implement an optimistic merge-retry: after writing the tmp file but
before rename, re-read CACHE_PATH (or stat+contents), merge any new keys into
your in-memory cache, and retry the write/rename loop until no concurrent
changes remain. Use the existing symbols (readCache, CACHE_PATH, tmpPath,
sessionId, writeFileSync, renameSync) to locate and protect the critical region.
🪄 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: CHILL

Plan: Pro

Run ID: dcf9bced-ad41-4db2-a05c-3725406257cf

📥 Commits

Reviewing files that changed from the base of the PR and between e88fd44 and 49c8ff8.

📒 Files selected for processing (2)
  • __tests__/lib/codex-sessions.test.ts
  • lib/codex-sessions.ts

Comment thread lib/codex-sessions.ts
Comment on lines 63 to +66
const cache = readCache();
cache[sessionId] = path;
writeFileSync(CACHE_PATH, JSON.stringify(cache), "utf-8");
writeFileSync(tmpPath, JSON.stringify(cache), "utf-8");
renameSync(tmpPath, CACHE_PATH);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Atomic rename here still allows concurrent lost updates

This is still a read-modify-write race: two writers can both read the same old cache, each add a different sessionId, and the last renameSync wins, dropping the other entry. Atomic replacement prevents partial JSON, but not clobbering.

Use a cross-process critical section for read+write (e.g., lock file with retry/backoff) or a merge-on-conflict retry loop after failed/changed writes so concurrent writers preserve both entries.

🤖 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 `@lib/codex-sessions.ts` around lines 63 - 66, The read-modify-write around
readCache() -> modify cache[sessionId] -> writeFileSync(tmpPath) ->
renameSync(CACHE_PATH) is racy and can clobber concurrent updates; wrap that
whole sequence in a cross-process critical section (e.g., obtain an advisory
lock on CACHE_PATH or a dedicated lockfile with retry/backoff) so only one
process reads+updates+renames at a time, or implement an optimistic merge-retry:
after writing the tmp file but before rename, re-read CACHE_PATH (or
stat+contents), merge any new keys into your in-memory cache, and retry the
write/rename loop until no concurrent changes remain. Use the existing symbols
(readCache, CACHE_PATH, tmpPath, sessionId, writeFileSync, renameSync) to locate
and protect the critical region.

NiveditJain added a commit that referenced this pull request May 5, 2026
…atch + complete the PR #293 audit (#297)

PR #293 wired per-CLI tool-name canonicalization so builtin policies that match Claude PascalCase names (`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`) fire under non-Claude CLIs. The map for Copilot was incomplete: Copilot's `view` tool — used by the model both to read files and to list directory contents — was not mapped, so `block-read-outside-cwd` never fired on `view` calls. User-reported regression: with `block-read-outside-cwd` enabled under Copilot CLI, the model could still list `$HOME` via a `view` call (a single tool invocation with `tool_input: {path: "/home/user"}`) where PR #293 had only fixed the `bash ls -la` flow.

Empirical confirmation in this user's local session log at `~/.copilot/session-state/.../events.jsonl`: `{"type":"tool.execution_start","data":{"toolName":"view","arguments":{"path":"/home/nivedit"}}}` against Copilot CLI 1.0.39.

Auditing all seven supported CLIs against their public tool registries plus on-disk session evidence revealed three more gaps in the same class:

- Copilot was missing `view`, `create`, `apply_patch`, `web_fetch`, `powershell`, `*_bash`/`*_powershell` (the eight session-management tools), `rg`, `show_file` — directory/file reads, file creation, patches, PowerShell, web fetches all bypassed policies.
- Cursor (PR #293 left it as passthrough) — Cursor uses `Shell` for what Claude calls `Bash`, so every Bash builtin (`block-sudo`, `block-rm-rf`, `block-read-outside-cwd` Bash branch, …) silently no-op'd on Cursor sessions.
- Codex (PR #293 left it as passthrough) — Codex hooks report `tool_name: "apply_patch"` even when matchers say `Edit`/`Write`; live sessions also expose `write_stdin` which sends input to a running shell.
- OpenCode was missing `apply_patch` and `websearch`.

Fix:
- Extend `COPILOT_TOOL_MAP` in `src/hooks/types.ts` with the full Copilot CLI tool surface — `view`/`show_file` → `Read`, `create` → `Write`, `apply_patch` → `Edit`, `web_fetch` → `WebFetch`, `powershell` and the `*_bash`/`*_powershell` session-management tools → `Bash`, `rg` → `Grep`.
- Extend `OPENCODE_TOOL_MAP` with `apply_patch` → `Edit`, `websearch` → `WebSearch`. Mirror the same entries in the OpenCode shim template at `src/hooks/integrations.ts:734` (the shim must stay self-contained — it's loaded in-process by opencode).
- Add `CURSOR_TOOL_MAP` (`Shell` → `Bash`) and `CODEX_TOOL_MAP` (`apply_patch` → `Edit`, `write_stdin` → `Bash`) in `src/hooks/types.ts`, plus matching cursor/codex branches in `handler.ts:canonicalizeToolName`. `apply_patch` maps to `Edit` (not `Write`) for consistency with the existing `str_replace_editor` → `Edit` entry; the choice was confirmed via AskUserQuestion. The trade-off is documented: `Edit` preserves Claude semantics (Claude's own `Edit` tool doesn't trigger `block-write-outside-cwd` either), while `Write` would have been stricter but inconsistent with Claude.

Tests:
- `__tests__/hooks/handler.test.ts` — extend the existing per-Copilot canonicalization loop to cover every new entry (with `[view, Read]` listed first as the regression anchor); add new test blocks for Cursor (`Shell` → `Bash`, plus passthrough for `Read`/`Write`/`Grep`/`Delete`/`Task`/`MCP:*`) and Codex (`apply_patch` → `Edit`, `write_stdin` → `Bash`, plus passthrough for `Bash`/`mcp__*`).
- `__tests__/e2e/hooks/copilot-integration.e2e.test.ts` — pinned regression test "blocks `view` of a path outside cwd under Copilot (regression for #295)" mirroring the PR #293 `ls -la` test, plus a new `CopilotPayloads.preToolUse.view` factory in `__tests__/e2e/helpers/payloads.ts`.
- `__tests__/hooks/opencode-plugin-shim.test.ts` — extend the OPENCODE_TOOL_MAP coverage loop with `apply_patch` and `websearch`.

Verified: `bun run test:run` → 1461 passed, `bun run test:e2e` → 291 passed (Copilot e2e went 11 → 12), `bunx tsc --noEmit` → clean. Manual repro of all three: Copilot `view /etc` denies, Cursor `Shell sudo …` denies, Codex `write_stdin` denies (canonicalized to `Bash`).

Pre-existing item not in this PR: the dogfood `.opencode/plugins/failproofai.mjs` was never updated with `TOOL_NAME_MAP` after PR #293 (the template was updated but the dogfood file is hand-maintained). Production users get the correct map via the template; only contributors running OpenCode against this repo are affected. Reading or rewriting that file requires temporarily disabling the `block-read-outside-cwd` agent-settings guard — deferred to a follow-up PR.

Co-authored-by: Claude Opus 4.7 <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

1 participant