[App/Windows/WSL] git-init-watcher retries permanent EISDIR on workspace root every five seconds
Summary
Codex Desktop 26.707.8168.0 on Windows 11 repeatedly retries a Windows-side file watcher after receiving EISDIR for a WSL2 workspace root. The retry interval eventually settles at approximately five seconds and continues while the application remains open.
Related to #32183, but this reproduces without using the in-app browser and appears to involve the workspace-level git-init-watcher rather than only the browser-related git-repo-watcher path.
Sanitized observation
Workspace form:
\wsl$<distro>home<user><workspace>
Observed behavior:
- Windows Node fs.watch against the WSL workspace root returns EISDIR.
- Watching the nested Git repository root produces the same result.
- The behavior also reproduces through the equivalent \wsl.localhost path and a mapped drive.
- The git-init-watcher retries after failure, with exponential delay capped at approximately five seconds.
- One application run recorded 121 failures in about ten minutes.
- Historical application logs contained more than 17,000 failures from the same error family.
- Git and repository operations performed inside WSL itself continue to work normally.
User names, repository names, task identifiers, full paths, and raw logs have been omitted.
Steps to reproduce
- On Windows 11 with WSL2, open a Git workspace located under the Linux home directory in Codex Desktop.
- Use a workspace path exposed through \wsl$ or \wsl.localhost.
- Leave the desktop application open.
- Inspect the Codex application logs for git-init-watcher or file-watch failures.
Actual behavior
A permanent EISDIR watcher failure enters an effectively unbounded retry loop, producing repeated work and log churn approximately every five seconds.
Expected behavior
- Detect unsupported or permanent watcher errors such as EISDIR.
- Fall back to bounded polling when appropriate, or disable that watcher after a small number of failures.
- Avoid retrying permanent watcher failures indefinitely.
- Prefer a WSL-native watcher when the workspace is handled by the WSL execution backend.
Environment
- Codex Desktop: 26.707.8168.0
- OS: Windows 11 x64
- WSL2: Ubuntu
- Workspace stored in the Linux filesystem under the WSL home directory
- Reproduces without invoking the in-app browser
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Adding another independent reproduction of this same behavior.
Environment:
Observed behavior:
\\wsl$\Ubuntu\home\<wsl-user>\workspace\<workspace>.fs.watchrepeatedly fails with EISDIR.I don't consider the following to be fixes, only workarounds: running
git initjust to silence the watcher; keeping an artificial Windows project open; moving projects to Windows; or switching the agent to native Windows mode.This reinforces the suggestion already noted here: detect the permanent error and apply backoff/disable the watcher, or run the watcher in the same environment as the filesystem (WSL), instead of retrying EISDIR indefinitely. I don't have confirmation this is the root cause — this is only another reproduction of the symptom.
Same defect reproduces on Linux with a different errno —
ENOENTinstead ofEISDIR— which suggests the retry logic, not the platform or the specific error, is the problem.Environment
| | |
|---|---|
| App | Codex Desktop for Linux,
chatgpt-26.810.52044-1.x86_64(official RPM) || OS | Fedora 44, GNOME / Wayland, x86_64 |
| Kernel | 7.1.8 |
| Workspace | plain local ext4 path — no WSL, no SMB, no mapped drive |
Observation
The recorded workspace root simply does not exist on this host (it belongs to a different machine).
fs.watch()returnsENOENT, andgit-init-watcherretries indefinitely:Measured 55 occurrences in 30 minutes (~one every 5s), matching the cap described in the original report. A second independent 1-hour sample showed the same line still dominating the journal. No crash, no coredump — the app is otherwise usable.
Why this may be worth generalising
For this particular stale path,
ENOENTnever resolved — the directory stayed absent for the life of the process, so retrying could not succeed.ENOENTis not permanent in general (a directory may be created later), so the useful distinction is probably "has not resolved after N attempts" rather than "permanent by errno". Related reports show repeated Git-watcher failures in other environments, though not necessarily in the same watcher or code path: #33764, #29084, and #36961. My Linux observation is specificallygit-init-watcherreturningENOENT. A fix that classifies non-transientfs.watch()errors and stops retrying — rather than special-casing WSL or SMB paths — would cover all of them.On a laptop the cost is a wakeup every 5s for the lifetime of the process, plus journal spam that buries other diagnostics.
Correction to my earlier comment — I said I couldn't find where the stale workspace root came from and guessed at synced account state. That was wrong: I searched the wrong directory. It is plain local config.
The path is a project entry in
~/.codex/config.toml:That directory does not exist on this host, and
git-init-watcherretriesfs.watch()against it every ~5s for the life of the process.Local reproduction (no account state, no WSL/SMB needed)
[projects."<absolute path that does not exist>"]entry to~/.codex/config.toml(or add a workspace, then delete the directory).journalctl --user -f | grep git-init-watcherThe
ENOENTline repeats indefinitely, no backoff, no give-up.Superseded — see my later comment. The
config.tomlentry turned out not to be the source: removing it did not stop the loop. The path is restored from persisted session state. I have only tested Linux, so I can't say how this behaves on macOS or Windows — which fits this being one watcher bug across platforms (ENOENThere,EISDIRon WSL in the original report) rather than several platform-specific ones.Trigger isolated on Linux: the stale path survived every configuration-level removal I could find.
Same host as my earlier comments (Fedora 44,
chatgpt-26.810.52044-1.x86_64, Wayland). Following theENOENTloop back, the missing workspace root kept returning after every config-level removal. The evidence points at historical rollout files as the source, though I have not read the code and cannot confirm the mechanism.Chain of custody
Three rollout files dated 2026-05-01:
each containing:
That directory no longer exists. After a restart the rows were observed recreated in
local_thread_catalog;state_5.sqlitewas separately observed to hold the same path, though I did not watch it being repopulated. The two stores werelocal_thread_catalogin~/.codex/sqlite/codex-dev.db, andthreadsin~/.codex/state_5.sqlite. The thread IDs inlocal_thread_catalogmatch the rollout filenames exactly; I did not perform the same check againststate_5.sqlite.Why config-level fixes do not work
Verified in this order, measuring after each step:
[projects."…"]entry fromconfig.toml→ thread persisted./tmpand zero[projects]entries → thread persisted.local_thread_catalog→ rows were recreated on next start, and the thread came back heavier than before.Only step 4 held — but note it changed three things at once (moved the rollouts and cleared both stores), so it does not isolate which of them was authoritative. What it does establish is that no configuration-level change I tried was sufficient; clearing persisted state was required.
Measured cost (instantaneous
/procsampling, notpslifetime averages)| state | app CPU | RSS | Tctl |
|---|---|---|---|
| as installed, settled idle | 30.0% | 3417 MB | 64.9 °C |
| as installed, peak observed | 41.8% | 3437 MB | 74.8 °C |
| after moving rollouts aside and clearing both stores | 2.0% | 2328 MB | 58.0 °C |
A thread named
gitinside the main process accounted for 99 of 121 ticks (82% of the main process's work) in one sample, and 242 ticks in a later one.git-init-watcherlog lines went from ~55 per 30 min to 0. Across the full mitigation sequence, settled Tctl fell from 64.9 °C to 58.0 °C; the pre-change peak was 74.8 °C. These measurements do not isolate the temperature contribution of each change.Observed effects associated with the same stale path
cwdthat no longer exists should be skipped rather than watched. (I measured CPU in a thread namedgitand the watcher's log output; I did not instrument what it was reading, so "scanning" would be my assumption rather than a measurement.)Unrelated but found alongside — the Linux app launches its GPU process with
--ozone-platform=x11on a Wayland session, Under X11, bothGetVSyncParametersIfAvailable() failedandUnable to get gpu adapterappeared in the journal and the GPU process measured 17.7% CPU on an idle window. Under Wayland, it measured 6.5%, and both error classes were absent after restart. Happy to open that separately if it is out of scope here.---
Edited after posting for accuracy. Two changes worth flagging: (a) the measurement table originally showed a single "as installed" row of 41.8% / 74.8 °C, which was the peak sample rather than the settled idle baseline and overstated the improvement — both pre-cleanup samples are now shown separately; (b) several statements about the app's internals were phrased as fact when they are inferred from observed behaviour, and have been reworded. I have not read the source. The measurements themselves are unchanged.
Further edit: narrowed several statements that outran the evidence — which store was observed being recreated versus merely holding the path, which table had its IDs cross-checked, "scanning" (not instrumented), "free-running" (an inference), and the fact that the final step was a compound intervention rather than an isolated one.