[macOS][Desktop 26.715.52143] Sidebar hover/click freezes UI for 3–10s in recursive FSEvents watcher teardown

Open 💬 6 comments Opened Jul 20, 2026 by swordfish444
💡 Likely answer: A maintainer (swordfish444, contributor) responded on this thread — see the highlighted reply below.

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 watcher events 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_card and 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.js opens the task hover card and requests Git data with operationSource: "local_thread_hover_card".
  • fmb7ipbs-D814SoSz.js turns the branch query into a retained live repository watch.
  • worker.js watches Git metadata plus the entire worktree root recursively.
  • src-DU0S2Fqi.js calls Node fs.watch(path, { recursive: true }) through a local execution host constructed by main-D-AEKvtN.js in 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?

  1. On Apple Silicon macOS, launch Codex Desktop 26.715.52143 with several local Git-backed tasks visible in the left sidebar and multiple tasks pinned. The strongest reproduction uses tasks from different managed worktrees.
  2. Move the pointer continuously up and down across pinned task rows for 60 seconds.
  3. During the first ~15 seconds, observe whole-window freezes lasting several seconds. After branch/watcher state warms, hover usually becomes smoother.
  4. Without restarting, click between tasks from different worktrees every 1–2 seconds.
  5. Observe renewed 3–10 second whole-window freezes, even when a selected task has only one turn.
  6. 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
``

  1. 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
``

  1. In the active Desktop Git log, correlate source=local_thread_hover_card and [git-repo-watcher] Starting git repo watcher with the freeze window. Run the logged Git metadata command directly; it should complete in milliseconds, distinguishing Git execution from watcher teardown.
  2. 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:

  1. 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.
  2. Ensure uv_fs_event_stop / FSEvents teardown cannot block the Electron UI thread.
  3. Propagate mouse-leave cancellation into metadata lookup and watcher startup.
  4. Reuse a repository watcher with an idle grace period instead of starting/closing one per hover subscription.
  5. Deduplicate the live subscription's initial branch refresh and add a bounded timeout to detached-HEAD fallback queries.
  6. 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.

View original on GitHub ↗

6 Comments

swordfish444 contributor · 11 hours ago

@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.

github-actions[bot] contributor · 11 hours ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #34287

Powered by Codex Action

swordfish444 contributor · 11 hours ago

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 in uv_fs_event_stop → uv__fsevents_close → semaphore_wait_trap. The issue body includes the matched timing evidence and exact repro.

swordfish444 contributor · 10 hours ago

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.

  • 310-second capture: 0 local_thread_hover_card events, 0 hover Git metadata commands, and 0 Git repo watcher starts.
  • Native main-process samples 4-29: 0 uv_fs_event_stop / HandleWrap::Close teardown hits across 247.08 sampled seconds.
  • The only early recurrence was around a boundary-spanning cold resume: 5.12 seconds total in watcher teardown across the first three samples, with one independently recorded 3.384-second ChatGPT spin.
  • Previous unmitigated click-phase capture: 28.42 of ~38 sampled seconds (~75%) in the same teardown stack.

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:

  • 8 under 1 second
  • 1 at 1.124 seconds
  • 1 at 4.668 seconds

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/list request 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. fseventsd nevertheless 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.

swordfish444 contributor · 6 hours ago

🔴 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:

  • macOS fseventsd was at approximately 22 GB in top (about 8.5+ GiB RSS in ps) and approximately 100% CPU.
  • System compressed memory was approximately 35 GB, with only approximately 1.2 GB free.
  • Codex logged a hover-originated branch lookup (source=local_thread_hover_card) taking 35,183 ms.
  • macOS independently recorded a 36.8-second slow-HID response in the same interval.

I restarted only the unhealthy system fseventsd daemon. I did not archive, rewrite, trim, or delete any task, pin, rollout, repository, or authentication state.

After the daemon replacement:

  • fseventsd dropped to approximately 10–15 MB RSS and returned to idle CPU.
  • Free memory recovered to approximately 27 GB.
  • The same hover Git lookup completed in 5.1 ms, then 4.2 ms on another repetition.
  • The UI immediately became responsive again.

A final two-minute controlled hover/click capture on the same app build confirmed the recovery:

  • 5 local_thread_hover_card Git lookups: 6–7 ms each.
  • 3 foreground thread/read calls: 4–6 ms.
  • 3 thread/resume calls: 303–430 ms.
  • fseventsd: approximately 19 MB RSS, 0.58% average CPU, 3.6% peak CPU.
  • 0 macOS ChatGPT spindumps and 0 slow-HID events.
  • The pin set and visible sidebar pin order remained preserved.

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 watcher events 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:

Skills/list request cwdsCount=27 forceReload=false
skills/list durationMs=2326
Skills/list missing short_description affectedCwdsCount=27 missingShortDescriptionCount=3375

Skills/list request cwdsCount=27 forceReload=false
skills/list durationMs=2181
Skills/list missing short_description affectedCwdsCount=27 missingShortDescriptionCount=3375

3,375 = 27 CWDs × 125 skills. Despite forceReload=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 duplicate plugin/list pairs 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
  1. local_thread_hover_card must use one-shot cached/snapshot Git metadata. It must not construct or retain a recursive worktree watcher.
  2. If an opened task needs a live repository watch, key one shared watcher by canonical repository root, reference-count it, and reuse it across hover/task subscribers.
  3. Propagate mouse-leave/task-switch cancellation through metadata lookup and watcher startup so a stale hover can never register a watcher after cancellation.
  4. Move watcher stop/close fully off the Electron main thread. uv_fs_event_stop / uv__fsevents_close must never gate input handling.
  5. Add a circuit breaker: when Git/FSEvents work exceeds a small UI budget or the system daemon is unhealthy, return cached/no branch metadata and keep the sidebar responsive.
Skills/plugins/task-switch path
  1. Make skills/list(forceReload=false) a real cache lookup keyed by skill-root/config generation, not a full scan per CWD.
  2. Load global and plugin skills once. For each CWD, overlay only genuinely workspace-local skill roots.
  3. Coalesce identical in-flight skills/list and plugin/list requests; the duplicate pairs should share one result.
  4. Compute a short_description fallback once per skill. A missing field must not trigger 3,375 repeated checks/log entries per request.
  5. Commit the selected task route immediately and run skill/plugin discovery asynchronously; discovery must not block visible task switching.

Acceptance criteria for a targeted build

  • 60 seconds of rapid pinned-row hover produces 0 repository watcher starts with source=local_thread_hover_card, 0 main-thread watcher teardown, and 0 slow-HID events.
  • Repeated skills/list(forceReload=false) calls after the first load are cache hits and do not rescan 27 CWDs or drive multi-core CPU saturation.
  • Ten rapid cross-worktree task switches remain input-responsive while resume/discovery finishes in the background.
  • No task, pin, rollout, MCP capability, or work-mode setting needs to be removed or changed.

I installed a conservative local health guard that can recover a runaway fseventsd as 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.

swordfish444 contributor · 5 hours ago

Additional verified cold-switch mitigation: share tools/list catalogs across task runtimes

I 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:

  • First live tools/list: 15,855 ms
  • Next independent runtime using the shared catalog: 1,834 ms (88.4% faster)
  • Four concurrent warm runtimes: 1,820–2,201 ms each
  • Exact equality for tool count, schemas, route index, server identity, capabilities, and instructions
  • A safe tools/call still executed live and returned the exact expected result
  • Empty stderr and no degradation of fseventsd

The local implementation caches only immutable tools/list metadata 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/list metadata across task runtimes instead of synchronously rediscovering the same catalog for every task click:

  1. Key the shared catalog by MCP configuration generation, authenticated principal/authorization context, negotiated protocol, and server version.
  2. Return a valid cached catalog immediately and refresh it asynchronously; coalesce identical in-flight refreshes.
  3. Invalidate on identity, authorization, configuration, protocol, or server-generation changes.
  4. Cache metadata only. Every tools/call must continue to route live with current authorization.
  5. Commit visible task navigation before MCP discovery completes, and reclaim or pool inactive per-task stdio runtimes.

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.