Extension hosts silently restart mid-agent-run with no logged reason (3.16.17, macOS)

UPDATE (Aug 19): Root cause found — see reply below. browser_cdp with {"method":"Page.reload"} reloads the Cursor workbench window, not the browser view. Six calls, six teardowns, matched to the second. Please disregard the feature-flag hypothesis in this post.

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Cursor repeatedly tears down all extension hosts and re-initializes the workbench with no user action, killing in-flight agent turns. Five times in four hours. FOUR OF THE FIVE EVENTS HAVE NO LOGGED REASON ANYWHERE IN CURSOR’S LOGS.

It is NOT a crash — verified:

  • The renderer OS process is never replaced (same pid, alive 4+ hours across all five events), so this is a workbench reload, not a renderer crash.
  • The app process is never replaced.
  • All extension hosts exit code 0 — a deliberate, graceful shutdown.
  • No macOS crash report for any event (~/Library/Logs/DiagnosticReports).
  • No jetsam / memorystatus kill in the unified system log across the window.

Signature, identical all five times:
exthost.log: “Extension host terminating: renderer closed the MessagePort”
main.log: “Extension host with pid exited with code: 0, signal: unknown” (x4 hosts)
main.log: “[PowerMainService] Releasing N orphaned wakelock(s) … releaseReason=ipc-client-disconnect”
then full workbench re-init:
renderer.log: “[CursorExtensionIsolationService] Extension isolation is enabled”
renderer.log: “Placing extension(s) … on a separate extension host”

Everything I searched for that would explain it — ZERO hits, all of them:
“terminated unexpectedly” — 0
“Automatically restarting the extension host” — 0
“Extension host was not stopped because of veto” — 0
“[MemoryMonitor]” — 0
“[RendererPing] window … blocked” — 0
“Extension host watchdog reported” — 0
“is unresponsive” — 0

Also ruled out: multi-root first-folder change (this is a single-folder window), workspace trust change, profile switch, extension install/update, MCP config change.

THE CONTRAST THAT SHOWS THIS IS A LOGGING GAP, NOT A MYSTERY:
A sixth event the same afternoon — the idle extension-host killer — logged its reason completely:

[IdleTimeContribution] Hard stopping cursor builtin extension hosts after 30 idle minutes (threshold=30, freeMemory=3.1%, freeMemoryThreshold=10%).
Hard stopping local extension hosts (Idle extension host killer) [cursor builtin hosts only].

That event also behaved correctly: builtin hosts only, user host left alive, NO workbench re-init, hosts restarted on refocus.

So Cursor CAN log this class of action, and does — on one path. The other four events took a path that emits nothing on success. That gap is the bug I am reporting.

Steps to Reproduce

I cannot trigger it on demand — it arrives unattended. It reproduced five times in four hours under this workload, and also in the prior session that morning, so it is not rare on this setup.

Conditions present every time:

  1. Open a single-folder window (not a .code-workspace) on a large JavaScript monorepo with sibling git worktrees.
  2. Run several agent conversations concurrently, with MCP servers connected and Cursor hooks configured.
  3. Leave long agent turns generating (multi-minute turns are the ones that get killed).
  4. Wait. Within roughly 10–90 minutes, all extension hosts exit code 0 and the workbench re-initializes, cutting off whatever was generating.

Event times observed (local):
session 20260818T085806 — 09:33:39
session 20260818T133849 — 15:28:22, 16:53:41, 17:02:58, 17:22:57
(plus 15:59:40, which WAS the idle killer and logged its reason — include as the control case)

Gaps between events shortened over the session: 85 min, then 9 min, then 19 min.

TIMING FINGERPRINT — this may be the fastest route to reproducing it internally. Cursor polls feature gates every 5 minutes (“Mirrored gate (reason=gate-change, …)”), and two events sit right on that boundary:
gate-change 15:28:04 → event 15:28:22 (18 seconds later)
gate-change 17:18:10, next poll due 17:23:10 → event 17:22:57 (13 seconds before)

If a server-pushed flag rollout is rearranging extension hosts on live clients, flipping the relevant gate against a client with an agent mid-generation should reproduce it immediately.

How to confirm from my side: check the logs for the absence of every marker listed in the description while the MessagePort-close + code-0 exits are present.

Expected Behavior

Four things, in order of how cheap they are for you:

  1. LOG THE CALLER on the graceful extension-host stop path — command id, contribution name, or flag id — exactly as the idle killer already does. This is a one-line change and the single highest-value fix here. Right now the success path of that stop emits nothing, so this is undiagnosable by users and unsupportable by your team.

  2. DO NOT RESTART EXTENSION HOSTS WHILE AGENT GENERATIONS ARE IN PROGRESS. The idle killer already performs exactly this check (it skips when there is in-progress AI work or a background-task wait). Whatever path this is should honor the same guard, or defer until idle.

  3. GATE REMOTE-CONFIG-DRIVEN HOST REARRANGEMENT BEHIND IDLE, if a server-pushed flag refresh is the trigger. Rolling out a flag should never interrupt a running agent on a live client.

  4. SURFACE IT IN THE UI when hosts must be restarted, so it does not read to the user as a crash.

Expected user-visible behavior: either the restart does not happen while I have agents running, or — if it must — I am told it happened and why, and I can find the reason in the logs afterward.

Operating System

MacOS

Version Information

Cursor 3.16.17
Electron 40.10.3 | Chromium 144.0.7559.236 | Node 24.15.0
macOS 26.6.1 (build 25G76) | Mac15,6 — Apple M3 Pro | 18 GB RAM

For AI issues: add Request ID with privacy disabled

69e7aea3-2eb5-4831-870a-c34bb0094337

Additional Information

IMPACT

  1. In-flight agent work is destroyed. Wakelocks release as ipc-client-disconnect mid-generation. The longest-running agent turns are hit hardest, because they are the most likely to be running when the restart lands.
  2. Unattributable. With no logged reason this is indistinguishable from a crash. I spent hours ruling out RAM, browsers, git worktrees and the TypeScript server before proving Cursor did it deliberately.
  3. Escalating within a session: gaps of 85 min, then 9 min, then 19 min.
  4. Unsupportable. Anyone who reports “Cursor keeps restarting” for this cause cannot be helped, because the product recorded nothing.

LEADING HYPOTHESIS
The graceful stopExtensionHosts success path emits no log line. The only unattended trigger consistent with this window’s configuration is an extension-enablement / host-placement change (updateRunningExtensions) — which is precisely what the log shows Cursor doing immediately after every event (“Extension isolation is enabled”, “Placing extension(s) … on a separate extension host”). Combined with the 5-minute gate-poll timing above, a server-pushed feature-flag refresh is the most likely trigger.


SECONDARY BUG (related, separate): diagnostics ignore jsconfig.json exclusions and OOM the semantic TypeScript server.

ReadLints / VscodeDiagnosticsExecutor forces files into the SEMANTIC TypeScript server even when those files are excluded by the project’s jsconfig.json.

On a JavaScript monolith with sibling git worktrees, a single agent lint call on an excluded path pulled roughly 19,000 additional files into the semantic server and drove it into its maxTsServerMemory cap:

crashing thread: tsserver[6.0.3]: semantic
node::OOMErrorHandler(char const*, v8::OOMDetails const&)
v8::internal::V8::FatalProcessOutOfMemory(…)

Reproduced twice in one afternoon, same sequence each time:
lint request on excluded worktree paths
→ ~6s later “Couldn’t wait on latest linter errors: Timeout has occurred”
→ tsserver SIGABRT (OOM), macOS .ips crash report written

Expected: a diagnostics request for a path excluded by jsconfig.json should not load that file’s entire transitive project into the semantic server. Honor the exclusion, or fail fast.

Current workaround: typescript.tsserver.useSyntaxServer: “always”, which disables semantic language features entirely. That is not an acceptable long-term answer for a JavaScript codebase.


EVIDENCE AVAILABLE ON REQUEST (happy to send privately)

  • Both full Cursor log sessions covering all events
  • The two macOS .ips crash reports for the tsserver OOM aborts
  • A 20-second-interval process sampler covering the window, confirming the renderer pid never changes

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor

UPDATE — root cause found. My feature-flag hypothesis above was wrong; please disregard it.

browser_cdp with {"method":"Page.reload"} does not reload the browser view. It reloads the Cursor workbench window. All extension hosts exit code 0, the workbench re-initializes, and every in-flight agent turn is destroyed.

Evidence: 6 for 6, matched to the second

Every Page.reload sent that day against every workbench teardown that day. Exactly six of each, and nothing else in the day matched:

Page.reload sent (UTC) Extension hosts exited (PDT)
22:28:22.700 15:28:22
23:53:41.146 16:53:41
00:02:58.160 17:02:58
00:22:57.602 17:22:57
01:26:09.075 18:26:09
03:43:28.881 20:43:28

The last one, captured with log.level: trace enabled, is the clearest:

20:43:28.881  browser_cdp {"method":"Page.reload","params":{"ignoreCache":true},"viewId":"efafb5"}
20:43:28.956  Extension host with pid 40771 exited with code: 0, signal: unknown   (x4 hosts)
20:43:30.401  [trace] window#load: window reported ready (id: 1)

75 milliseconds between the CDP call and the teardown.

Page.navigate is safe — this is specific to Page.reload

One Page.navigate was sent the same day (22:14:31 UTC) with no teardown. So the browser view is addressed correctly for navigation and incorrectly for reload.

Why nothing is logged

With trace on: window#load: window reported ready appears, but Lifecycle#unload never appears anywhere in the trace at all. The reload bypasses the lifecycle path, which is why no reason is recorded and why this is indistinguishable from a crash. The renderer OS process is never replaced, confirming reload rather than crash.

Minimal reproduction

  1. Open a folder window. Start a long agent turn.
  2. Have the agent open the Cursor browser: browser_navigate to any URL.
  3. Have it send: browser_cdp {"method":"Page.reload","params":{"ignoreCache":true}}
  4. Observe: all extension hosts exit code 0, the workbench re-initializes, the agent turn is destroyed, and no reason is logged.

Red herrings, so you don’t re-tread them

  • Idle extension-host killer — not this. It logged, correctly and repeatedly, [IdleTimeContribution] Skipping idle extension host stop after 46 idle minutes because 2 AI generation(s) are in progress. Its in-progress-AI guard works.
  • Memory pressure — not sufficient. Cursor’s own memLow flag was true for 11 consecutive samples (~2 min) with no teardown.
  • Renderer hang[RendererPing] ... blocked never fired.
  • Nested subagents — innocent. Browser-verifier subagents completed normally many times. They only looked guilty because verifiers are the agents that measure cold-load performance, and therefore the ones sending Page.reload.

Requests

  1. Route Page.reload to the browser view’s webContents, as Page.navigate already is. Or add it to the denied-methods list — that list already blocks browser-wide, storage, cookie, permission, download, and target-management commands for exactly this class of reason. Page.reload, Page.crash, Page.close, Browser.close, and Target.closeTarget belong there too.
  2. Log the caller on the graceful extension-host stop / window reload path, as the idle killer already does. This request from my original post stands, and its absence is what cost me a day of misdiagnosis.
  3. Don’t tear down extension hosts while agent generations are in progress. The idle killer already performs this check; this path does not.

Impact worth weighing

Any agent with browser access can destroy every other running agent in the window, silently, with one supported tool call. Cold-load performance testing is a normal verification task, browser_navigate doesn’t bust cache, so Page.reload with ignoreCache is the obvious thing to reach for.

Workaround for anyone else hitting this: never send Page.reload through browser_cdp. Use browser_navigate with a cache-busting query parameter, or Network.setCacheDisabled followed by browser_navigate. We now block it with a preToolUse hook set to fail closed, because a written instruction wasn’t enough — ours was violated within the hour.


The secondary bug in my original post (diagnostics ignoring jsconfig.json exclusions and OOM-ing the semantic TypeScript server) is unchanged and still reproducible.

Hey @Joolian, thanks for the detailed follow-up, and apologies for the slow response here!

We’ve confirmed what you found: sending Page.reload through browser_cdp reloads the whole Cursor window rather than the browser tab. It comes from how the underlying browser engine handles reload for embedded views, which is also why every host shut down cleanly with nothing in the logs.

I’ve flagged this to the team.