[BUG] Main process deadlocks on session refocus during MCP re-registration (navigate away from chat and back)
Summary
Navigating away from a chat conversation and back deadlocks the Electron main process. The event loop stops servicing tasks entirely — no error, no crash, no stall warning. Force-quit is the only recovery, and it silently discards conversation content that was never persisted.
Reproduced 5 times in ~25 minutes. The most reliable trigger is the Routines panel, because it opens its own session and therefore forces a genuine focus change on return. It is not specific to Routines — an earlier occurrence was reached via a message with an image attachment and produced an identical log signature.
Environment
| | |
|---|---|
| Claude Desktop | 1.37937.1 |
| Claude Code CLI | 2.1.246 |
| macOS | 27.0 (26A5421a) |
| Hardware | Apple M1 Max, 32 GB |
| Node (Electron) | 24.18.1 |
| MCP servers | 13 total (11 local, 2 remote), 35 tools |
| Sidebar mode | epitaxy |
Reproduction
- Open a chat conversation in an instance with MCP servers connected
- Navigate to Routines in the sidebar
- Navigate back to the chat
- Attempt to send a message
Expected: focus returns, message sends.
Actual: main process deadlocks. Composer shows perpetual Sending… with send disabled, the stop control does not respond, Routines shows a perpetual spinner. Force-quit is the only way out.
Root cause analysis
Every freeze occurs during session activation performed mid-session rather than at app boot. The identical code path succeeds every time at startup. The difference is whether the MCP servers being registered are already connected.
Freeze sequence
00:03:15 [CCD] LocalSessions.setFocusedSession: sessionId=null ← navigate away
00:03:15 [WarmLifecycle:session] Starting idle timeout for local_9c7bc60a…: 900s
00:03:15 [WarmLifecycle:preview] Starting idle timeout for local_9c7bc60a…: 1800s
00:03:21 [WarmLifecycle:preview] Warming up session local_9c7bc60a… ← navigate back
00:03:21 [CCD] LocalSessions.setFocusedSession: sessionId=local_9c7bc60a…
00:03:21 [CCD] LocalSessions.replaceEnabledMcpTools: … toolCount=35
00:03:21 [CCD] LocalSessions.replaceRemoteMcpServers: … serverCount=2
00:03:21 [CCD] [replaceRemoteMcpServers] Calling SDK with 13 total servers
00:03:21 LocalSessions.startShellPty: … cols=80, rows=24
── no further output. event loop never services another task. ──
00:04:29 Starting app ← force-quit + relaunch
The same call at boot succeeds
68 seconds later, after relaunch, the identical registration runs — but the MCP servers connect first:
00:04:31 MCP Server connection requested for: Control Chrome
00:04:31 [LocalMcpServerManager] Connecting to Control Chrome
00:04:31 [LocalMcpServerManager] Control Chrome negotiated protocol version: 2024-11-05
00:04:31 [LocalMcpServerManager] Connected to Control Chrome (10 tools)
00:04:31 [localMcpBridge] announcing Control Chrome: 10 tool(s)
00:04:32 [CCD] LocalSessions.replaceEnabledMcpTools: … toolCount=35
00:04:32 [CCD] LocalSessions.replaceRemoteMcpServers: … serverCount=2
00:04:32 [CCD] [replaceRemoteMcpServers] Calling SDK with 13 total servers
00:04:32 LocalSessions.startShellPty: … cols=80, rows=24
── continues normally ──
In the freeze case there are no connection events at all. The servers are already live from the previous boot, so replaceEnabledMcpTools / replaceRemoteMcpServers are being asked to tear down and replace 13 established connections, and startShellPty to respawn a PTY on a session that already owns one.
Correlation (11/11, no counterexamples)
| Registration | Context | MCP state | Result |
|---|---|---|---|
| 23:31:34 | app boot | connecting fresh | OK |
| 23:40:46 | mid-session refocus | already live | FROZE |
| 23:43:38 | app boot | connecting fresh | OK |
| 23:47:44 | mid-session refocus | already live | FROZE |
| 23:49:27 | app boot | connecting fresh | OK |
| 23:56:15 | mid-session refocus | already live | FROZE |
| 23:57:48 | app boot | connecting fresh | OK |
| 00:00:26 | mid-session refocus | already live | FROZE |
| 00:01:06 | app boot | connecting fresh | OK |
| 00:03:21 | mid-session refocus | already live | FROZE |
| 00:04:32 | app boot | connecting fresh | OK |
Control group
Seven session re-warms occurred that were not followed by a focus change and MCP re-registration. None froze — isolating the refocus + re-registration step specifically, rather than session warming generally.
18:11:41 → idle timeout started ok
18:23:26 → idle timeout started ok
18:23:37 → socket reconnect, idle timeout ok
23:32:54 → respondToToolPermission ok
23:34:51 → event flush, idle timeout ok
23:39:18 → idle timeout started ok
23:39:52 → respondToToolPermission ok
23:40:44 → setFocusedSession → replaceEnabledMcpTools FROZE
23:47:44 → setFocusedSession → replaceEnabledMcpTools FROZE
23:56:16 → setFocusedSession → replaceEnabledMcpTools FROZE
00:00:27 → setFocusedSession → replaceEnabledMcpTools FROZE
00:03:21 → setFocusedSession → replaceEnabledMcpTools FROZE
Deadlock, not a crash or slow frame
- The stop point drifts. Same sequence, different final line each time —
checkTrust,replaceEnabledMcpTools(×2),startShellPty(×3). A deterministic fault would stop consistently. - The app's own stall detector never fires. Zero
[event-loop-stall]warnings at any of the five freeze timestamps, despite that watchdog readily reporting ordinary 500ms hiccups elsewhere in the same log. The loop is not slow; it is not running. - No crash report is generated. Manual Force Quit bypasses macOS's spin/hang reporter.
| Freeze | Last line logged | Silence before restart |
|---|---|---|
| 23:40:46 | LocalSessions.checkTrust | 2m 47s |
| 23:47:44 | replaceEnabledMcpTools | 1m 40s |
| 23:56:17 | replaceEnabledMcpTools | 1m 28s |
| 00:00:27 | replaceEnabledMcpTools | 36s |
| 00:03:21 | startShellPty | 1m 08s |
Ruled out
| Hypothesis | Why it does not hold |
|---|---|
| Duplicate MCP server registration | One server name genuinely appears twice in the 13-server list passed to the SDK. But it is present identically in the boot registrations that succeed, so it is not the differentiator. Still likely worth fixing as a separate defect. |
| Requires repeated Routines visits | Initially suspected, then disproven — a freeze occurred on a single round-trip. |
| Memory pressure | Tree RSS at freeze time 1.9–2.6 GB on a 32 GB machine, in line with normal operation. |
| Sleep/wake recovery failure | A separate real issue in these logs (571s and 898s stalls after wake), but these five freezes occurred with no intervening sleep. |
Impact
Force-quit discards unpersisted conversation content. Verified directly: an exchange that rendered on screen during a frozen window is absent from the session transcript .jsonl. Content generated while deadlocked never reaches disk, and the only available recovery destroys it.
It blocks app updates. Squirrel's ShipIt waits for a clean exit to swap the bundle. Because the deadlock forces a hard kill, staged updates repeatedly fail — Install of 1.37937.3 has failed to apply 1 time(s) at this version appears on each subsequent launch. This machine required a manual reinstall to get from 1.37937.0 to 1.37937.1.
Suggested investigation
- Whether
replaceRemoteMcpServersawaits teardown of live stdio/WebSocket transports while holding a lock the transports' close handlers need. - Whether
startShellPtyis invoked for a session that already holds a PTY, and whether the prior PTY is awaited before the new spawn. - Why the session-activation path re-registers MCP servers at all on refocus, when the server set is unchanged from boot — an early-out on an unchanged list would likely sidestep this entirely.
- Whether the drifting stop point corresponds to which of the 13 servers happens to be mid-request when the replace is issued.
Notes
No automatic crash report exists, since Force Quit bypasses the macOS hang reporter. Happy to capture a sample/spindump during a live freeze if that would help — that would give the native stack showing which thread is blocked on what.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗