Skip to content
Productivity
Agent

codex-pre-edit-tracking-investigation

Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc captures everything learned so the next session can pick up cleanly without re-deriving.

From plugin
nimbalyst
1.4k3 skills3 agents35 commands
Install
$ npx -y skills add nimbalyst/nimbalyst --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc captures everything learned so the next session can pick up cleanly without re-deriving.

Agent definition

codex-pre-edit-tracking-investigation.md

Codex Pre-Edit Tracking — Investigation State

Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc captures everything learned so the next session can pick up cleanly without re-deriving.

Tracker: **NIM-586** ("Codex sessions miss post-edit snapshot; sidebar diff peek uses git instead of session history") — the part about post-edit snapshots is solved. The pre-edit race for updates is still open.

The user-visible problem

In the **FilesEditedSidebar peek popover**, AI-edited files render as all-green (entire file looks added) instead of a real red-green diff. Reproduces 100% for any file the AI touches that is:

  • gitignored (e.g. anything under `/tests/` in this repo)
  • untracked in git
  • brand-new in a worktree

The peek calls `git:file-diff` with `group: 'working'` (see `FilesEditedSidebar.tsx:162-170`). For files not in HEAD, the handler falls through to `git diff --no-index -- /dev/null <file>` which produces an all-added unified diff. See `GitHandlers.ts:879-905`.

What we shipped this session (works, do not regress)

1. Codex SDK upgrade 0.128.0 → 0.130.0

  • Bumped in `packages/runtime/package.json:206` and `packages/electron/package.json:61`
  • npm lockfile had to be manually patched: 16 codex-related entries under `packages/electron/node_modules/@openai/` and `packages/runtime/node_modules/@openai/` were stale. Deleted those entries via a Node script, ran `npm install`, codex-sdk hoisted to root `node_modules/@openai/codex-sdk@0.130.0`.
  • Verified native binary present at `node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin`
  • `peer: true` count unchanged (17 → 17)

2. Post-edit snapshot pipeline (NIM-586 part 1)

Codex's `file_change` `item.completed` now triggers a `post_edit_snapshot` chunk that writes an `ai-edit` history row tagged with `sessionId` in metadata. Mirrors Claude's `AgentToolHooks.createTurnEndSnapshots`.

  • `StreamChunk.type` extended with `'post_edit_snapshot'` in `packages/runtime/src/ai/server/types.ts:388, 460-477`
  • `OpenAICodexProvider.maybeBuildFileChangePostEditSnapshot` at `OpenAICodexProvider.ts:~2110` — fires on `item.completed` for `file_change`, reads each affected path from disk, reuses the synthetic edit-group ID via `lookupCodexEditGroupId`, skips `delete` kinds.
  • Yielded in main loop at `OpenAICodexProvider.ts:~1010` right after pre-edit yield, wrapped in try/catch.
  • Handler in `MessageStreamingHandler.ts:~1329` writes via `historyManager.createSnapshot(absPath, content, 'ai-edit', desc, { sessionId, toolUseId })`.
  • Verified: 3 pre-edit + 3 ai-edit rows landed for test session, all stamped with same `toolUseId = nimtc|item_15|...`

3. Session-aware diff IPC (NIM-586 part 2)

`session:file-diff(workspacePath, sessionId, filePath)` IPC handler synthesizes a unified diff from pre-edit baseline (red) vs ai-edit snapshot (green), falling back to current disk if no `ai-edit` snapshot exists. Returns `{ unifiedDiff, isBinary, source }`.

  • Handler at `packages/electron/src/main/ipc/SessionFileHandlers.ts:~221`
  • New helper `historyManager.getLatestSnapshotContent(filePath, sessionId, snapshotType)` at `HistoryManager.ts:~1163`
  • `FilesEditedSidebar.handleGetDiff` (`FilesEditedSidebar.tsx:162-191`) tries the session-aware IPC first when `activeSessionId` is set; falls back to `git:file-diff` if no session baseline exists.
  • Verified via `renderer_eval`: returns 398-byte unified diff for a `change-tracking-codex-test.md` (new file) with `source: 'session-history'`.

4. Codex PreToolUse hook plumbing (LOADED BUT NOT WORKING)

All the wiring is in place but **codex is not honoring the inline `--config hooks.PreToolUse=[{...}]` override**. Evidence: `[CODEX] PreToolUse hook configured` log fires at session start (confirming resolver returns valid path and config-builder runs), but the sidecar dir is never created (confirming the hook subprocess never runs).

Files involved:

  • `packages/electron/resources/codex-pre-edit-hook.mjs` — Node script. Reads stdin payload, extracts paths from apply_patch DSL (`*** Add/Update/Delete File:`), snapshots each path's content to `<NIMBALYST_PRE_EDIT_DIR>/<sha1(path)>.json`. Always emits `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}`. **Verified working when invoked directly via `ELECTRON_RUN_AS_NODE=1 <electron> <hook.mjs>` with stdin payload.**
  • `packages/electron/src/main/services/ai/codexPreEditHookPath.ts` — resolves the script path in dev (via `getPackageRoot() + resources/`) and packaged (via `process.resourcesPath`).
  • `packages/electron/src/main/index.ts:~1429` — registers `OpenAICodexProvider.setPreEditHookScriptPathResolver(resolveCodexPreEditHookScriptPath)` and `setPreEditSidecarDirResolver(sessionId => userData/codex-pre-edit-snapshots/<safeId>)`.
  • `OpenAICodexProvider.ts`:
  • New static fields `preEditHookScriptPathResolver` and `preEditSidecarDirResolver` plus setters
  • `buildCodexConfigOverrides` injects `configOverrides.hooks = { PreToolUse: [{ matcher: '^apply_patch$', hooks: [{ type: 'command', command: '"<execPath>" "<hookPath>"' }] }] }`
  • `sendMessage` adds `NIMBALYST_PRE_EDIT_DIR` and `ELECTRON_RUN_AS_NODE=1` to `codexEnv` per session
  • `maybeBuildFileChangePreEditSnapshot` reads from sidecar (`<dir>/<sha1(path)>.json`) before falling back to disk-read
  • `packages/electron/package.json` — added `resources/codex-pre-edit-hook.mjs` to `extraResources`

The pre-edit race (the actual bug)

Codex's apply_patch emits `item.started` for `file_change` **at the same time or slightly after** it applies the patch to disk. When `OpenAICodexProvider.maybeBuildFileChangePreEditSnapshot` does `fs.readFileSync(filePath)` at `item.started` time, the read often captures **post-edit** content because the patch already wrote.

The host's existing fallback (use `FileSnapshotCache` from `HooklessAgentFileWatcher`) only helps when chokidar had time to

Read more
Ships withnimbalyst

Nimbalyst is a free, open-source, local, interactive visual editor & session/task manager for developers, product managers, designers, builders.

Get the whole plugin