[macOS][Desktop 26.715.52143] Sidebar hover/click freezes UI for 3–10s in recursive FSEvents watcher teardown
What version of the Codex App are you using (From “About Codex” dialog)?
26.715.52143 (build 5591), bundled codex-cli 0.145.0-alpha.18
What subscription do you have?
Pro
What platform is your computer?
Darwin 24.6.0 arm64 arm (Apple Silicon macOS)
What issue are you seeing?
Codex Desktop repeatedly freezes its entire window for 3–10 seconds when the pointer crosses local Git-backed task rows in the left sidebar. Hover becomes smoother after roughly 15 seconds, but clicking between tasks causes the multi-second freezes to return. During a controlled one-minute reproduction, macOS recorded matching ChatGPT spins and slow-HID responses.
This is not a slow repository, large conversation, SQLite, GPU, or network issue. Repeated 20 ms native samples captured the Electron main thread synchronously closing recursive FSEvents watchers:
node::worker::MessagePort::OnMessage
→ JavaScript / V8
→ node::HandleWrap::Close
→ uv_close
→ uv_fs_event_stop
→ uv__fsevents_close
→ semaphore_wait_trap
The captured main-thread blocks were approximately:
Initial hover: 2.94 s
Warm hover: 0.12 s
Task clicks: 4.40 s, 9.12 s, 8.04 s, 6.86 s
Across the click phase, the main thread spent approximately 28.42 of 38 sampled seconds blocked in uv_fs_event_stop (~75%). macOS independently recorded ChatGPT spins of 4.617 s, 1.845 s, 5.484 s, and 7.375 s, plus slow-HID responses of 1.4 s and 5.6 s during the same intervals.
The triggering Git work itself was fast:
- 49
Starting git repo watcherevents during the active test (4 on initial hover, 45 while switching tasks). - All UI-triggered Git commands completed in 5–13 ms; eight commands consumed only 58 ms cumulatively.
- A hover-originated branch lookup was explicitly tagged
source=local_thread_hover_cardand completed in 5 ms. - Independent repetitions of the same Git metadata commands complete in roughly 9–11 ms.
The packaged build explains the lifecycle:
j34jmud9-BTI02UXW.jsopens the task hover card and requests Git data withoperationSource: "local_thread_hover_card".fmb7ipbs-D814SoSz.jsturns the branch query into a retained live repository watch.worker.jswatches Git metadata plus the entire worktree root recursively.src-DU0S2Fqi.jscalls Nodefs.watch(path, { recursive: true })through a local execution host constructed bymain-D-AEKvtN.jsin the Electron main process.- Mouse-leave releases the renderer subscription after 250 ms, but cancellation is not propagated through stable metadata, current-branch resolution, or watcher startup. Work can therefore continue after the pointer has moved away, only to be synchronously torn down on the UI thread.
One clicked task had only one turn. Its thread/read completed in 4 ms, but thread/resume took 10.075 s and overlapped a 7.375 s macOS spin plus 6.86 s in main-thread watcher teardown. That rules out large-history hydration as the primary cause.
There is a severe click-side amplifier: each resumed task starts a complete 12-process stdio MCP/plugin fleet, and the groups remain resident after switching away. At the end of the controlled capture, app-server retained 338 direct children using 12.62 GiB RSS, with approximately 28 copies of each configured stdio server. A task click created exactly 12 children in the next second and those processes were still alive more than 13 minutes later. This overlaps existing MCP lifecycle reports #30408, #11324, and #33946; I am including it here because it materially amplifies this sidebar interaction, not as a duplicate MCP report.
fseventsd simultaneously reached approximately 98–163% CPU and 7.35 GiB RSS. That is consistent with watcher churn/backlog and amplifies the stalls, but the native samples directly identify synchronous watcher close on the ChatGPT main thread as the hard UI blocker.
No task, pin, conversation, repository, or authentication data was modified during diagnosis. The ordered set of 26 pinned tasks was hashed before and after the capture and remained byte-for-byte identical.
What steps can reproduce the bug?
- On Apple Silicon macOS, launch Codex Desktop
26.715.52143with several local Git-backed tasks visible in the left sidebar and multiple tasks pinned. The strongest reproduction uses tasks from different managed worktrees. - Move the pointer continuously up and down across pinned task rows for 60 seconds.
- During the first ~15 seconds, observe whole-window freezes lasting several seconds. After branch/watcher state warms, hover usually becomes smoother.
- Without restarting, click between tasks from different worktrees every 1–2 seconds.
- Observe renewed 3–10 second whole-window freezes, even when a selected task has only one turn.
- In Terminal, sample the main process during the reproduction:
``bash``
MAIN_PID="$(pgrep -x ChatGPT | head -1)"
/usr/bin/sample "$MAIN_PID" 10 20 -file /tmp/chatgpt-main.sample.txt
- Inspect the sample. The blocked main-thread stack should end in:
``text``
node::HandleWrap::Close
uv_close
uv_fs_event_stop
uv__fsevents_close
semaphore_wait_trap
- In the active Desktop Git log, correlate
source=local_thread_hover_cardand[git-repo-watcher] Starting git repo watcherwith the freeze window. Run the logged Git metadata command directly; it should complete in milliseconds, distinguishing Git execution from watcher teardown. - Optionally inspect app-server children before and after each task click. With N enabled stdio MCP/plugin servers, each resumed task adds approximately N new persistent direct children.
What is the expected behavior?
- Pointer hover must never block the Desktop UI.
- A transient hover card should use cached/snapshot Git metadata and should not create a recursive repository watcher.
- Watcher startup and teardown must be cancellable, deduplicated by repository, and kept off the Electron main thread.
- Rapid task switching should cancel stale hover/resume work.
- MCP/plugin discovery should be lazy or shared across task runtimes, and inactive runtime-owned processes should be reclaimed.
Additional information
This is distinct from generic sidebar/history lag reports because the reproduction is deterministic and every sampled hard freeze lands in synchronous FSEvents watcher teardown. Relevant adjacent reports include #11011, #16158, #20547, #17394, and the MCP lifecycle issues linked above.
Suggested fix order:
- Do not retain or recursively watch a repository for a transient task hover card; use snapshot metadata or disable Git-enriched hover for local/pinned rows.
- Ensure
uv_fs_event_stop/ FSEvents teardown cannot block the Electron UI thread. - Propagate mouse-leave cancellation into metadata lookup and watcher startup.
- Reuse a repository watcher with an idle grace period instead of starting/closing one per hover subscription.
- Deduplicate the live subscription's initial branch refresh and add a bounded timeout to detached-HEAD fallback queries.
- Cancel stale task-resume/plugin work and pool or lazily start MCP services.
I have a sanitized synchronized evidence set containing the 20 ms main/app-server/renderer samples, app Git/main logs, macOS spin/HID records, and process time series. I can provide it directly and can run a targeted build immediately.
6 Comments
@etraut-openai — this is a deterministic Desktop repro rather than the generic slowdown described in #11011/#16158. A controlled one-minute capture matched every hard freeze to the Electron main thread synchronously waiting in
uv__fsevents_close; the Git subprocesses themselves completed in 5–13 ms. Could you route this to the Desktop/sidebar owner? I can provide the sanitized process samples and correlated app/macOS logs and run targeted builds immediately.Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I reviewed #34287. It reports generic sidebar scrolling plus RTL rendering on Intel macOS. This report is distinct: Apple Silicon; deterministic task-row hover/click reproduction;
source=local_thread_hover_card; 49 recursive watcher starts; and native main-thread samples ending inuv_fs_event_stop → uv__fsevents_close → semaphore_wait_trap. The issue body includes the matched timing evidence and exact repro.Post-restart controlled A/B on the same build (
26.715.52143) materially isolates the hover trigger and the remaining cold-open path.Hover-card bypass result
I enabled the app's supported Work/ChatGPT sidebar mode, whose unified Codex rows are rendered with
disableHoverCard.local_thread_hover_cardevents, 0 hover Git metadata commands, and 0 Git repo watcher starts.uv_fs_event_stop/HandleWrap::Closeteardown hits across 247.08 sampled seconds.This is a very strong A/B indication that the rich local-thread hover card is the steady-state hover-freeze trigger. Disabling that card removes the repeated hover stalls without changing task data.
Residual cold-open/tool lifecycle problem
In a later rapid-switch burst, 10 task resumes completed as follows:
Each cold-resumed task still created its own 12-process stdio MCP/plugin cohort, and those cohorts remained resident after switching away. A separate 13.6-second slow-HID response coincided with an
app/listrequest taking 30.392 seconds while tool/subagent updates were being delivered; another 1.0-second slow-HID response occurred during the click burst.The machine was not swapping.
fseventsdnevertheless remained pathological throughout the controlled window: 103.8% average CPU (97.0-173.4%), with RSS growing from 6.397 to 6.488 GiB.So the supported hover-card bypass materially restores steady-state sidebar responsiveness, but the app is not internally healthy: cold task opens, retained per-task MCP fleets, and tool/app-list delivery can still starve the UI. This reinforces the need for both fixes described in the issue: remove transient recursive hover watchers from the UI path, and pool/lazily start/reclaim task-owned MCP processes and app/tool discovery work.
🔴 Root cause confirmed by a controlled daemon-reset A/B; second cold-switch CPU bug isolated
@etraut-openai — I now have a decisive before/after that separates slow Git execution, conversation size, and MCP startup from the catastrophic UI freeze.
1. Catastrophic 35–60 second freeze: FSEvents watcher amplification on the hover path
Immediately before the worst reproduction:
fseventsdwas at approximately 22 GB intop(about 8.5+ GiB RSS inps) and approximately 100% CPU.source=local_thread_hover_card) taking 35,183 ms.I restarted only the unhealthy system
fseventsddaemon. I did not archive, rewrite, trim, or delete any task, pin, rollout, repository, or authentication state.After the daemon replacement:
fseventsddropped to approximately 10–15 MB RSS and returned to idle CPU.A final two-minute controlled hover/click capture on the same app build confirmed the recovery:
local_thread_hover_cardGit lookups: 6–7 ms each.thread/readcalls: 4–6 ms.thread/resumecalls: 303–430 ms.fseventsd: approximately 19 MB RSS, 0.58% average CPU, 3.6% peak CPU.This is a strong causal A/B: the 35.183-second Codex hover lookup and the 36.8-second OS input stall disappeared when the pathological FSEvents state was reset, with no task-data change.
However, the app still emitted 17
Starting git repo watcherevents during the healthy capture. The reset removes the accumulated system failure; it does not fix the product behavior that can recreate it.2. Remaining early task-switch hitch: uncached 27-CWD skill rescans
The clean capture exposed a second deterministic path that explains why initial task switches can still hitch before becoming smooth:
3,375 = 27 CWDs × 125 skills. DespiteforceReload=false, the app rescanned the same global/plugin skill inventory for every CWD twice within approximately five seconds. Those calls aligned with app-server CPU spikes up to 379.2%. The same task switches also issued duplicateplugin/listpairs taking 270–413 ms each.The task itself was not the bottleneck: the associated reads/resumes remained in the 4–430 ms range. Six speculative background hydration reads for unloaded threads failed quickly in 58–62 ms, which is wasteful but not the multi-second blocker in this capture.
Exact product changes needed
Hover/Git/FSEvents path
local_thread_hover_cardmust use one-shot cached/snapshot Git metadata. It must not construct or retain a recursive worktree watcher.uv_fs_event_stop/uv__fsevents_closemust never gate input handling.Skills/plugins/task-switch path
skills/list(forceReload=false)a real cache lookup keyed by skill-root/config generation, not a full scan per CWD.skills/listandplugin/listrequests; the duplicate pairs should share one result.short_descriptionfallback once per skill. A missing field must not trigger 3,375 repeated checks/log entries per request.Acceptance criteria for a targeted build
source=local_thread_hover_card, 0 main-thread watcher teardown, and 0 slow-HID events.skills/list(forceReload=false)calls after the first load are cache hits and do not rescan 27 CWDs or drive multi-core CPU saturation.I installed a conservative local health guard that can recover a runaway
fseventsdas an operational safety net, but that is only mitigation. The product must stop creating transient hover watchers and must cache/coalesce the repeated skill/plugin discovery work.I can provide the sanitized correlated Git log, app-server log, process time series, and macOS HID evidence, and I can test a targeted build immediately.
Additional verified cold-switch mitigation: share
tools/listcatalogs across task runtimesI isolated and fixed one more task-start contributor locally. This is separate from the confirmed FSEvents hover/watcher defect above and does not replace that product fix.
Each newly started task runtime was making a fresh federated MCP catalog discovery before it could serve tool metadata. A protocol-level production canary with 283 tools measured:
tools/list: 15,855 mstools/callstill executed live and returned the exact expected resultfseventsdThe local implementation caches only immutable
tools/listmetadata and routing indexes. It never caches tool execution. The cache is isolated by a one-way hash of the authenticated principal, scopes/groups, endpoint set, negotiated protocols, client protocol, and bridge source version. It uses private permissions, bounded entry size/count, atomic replacement, corruption checks, background refresh, and fail-open live discovery; invalid authentication still fails closed. Account/config changes invalidate the key automatically.Exact product change this supports
Codex app-server should cache and coalesce MCP
tools/listmetadata across task runtimes instead of synchronously rediscovering the same catalog for every task click:tools/callmust continue to route live with current authorization.This removes a verified ~14-second repeated startup stage after the first discovery, but the Desktop product still needs the hover watcher/FSEvents correction and process-lifecycle changes documented above. Acceptance should include ten rapid cross-worktree task switches after one catalog load with no repeated live catalog enumeration and no input-blocking wait on discovery.