Move_agent_to_root aborts the in-flight turn and never resumes (“Switching workspace root”, then silence)

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

When a local agent calls move_agent_to_root (UI: “Switching workspace root”), Cursor aborts the current chat, opens a new workspace/composer, restores history as status: “aborted”, then fails to continue the turn. The source conversation is archived. The user sees the spinner, then nothing.

This is not just “remote branch not found”. It also happens when the destination folder already exists, is already a workspace folder, and the current branch exists on origin.

Steps to Reproduce

Open a multi-root .code-workspace (or any workspace whose environment.id is not the single-folder id of repo A).
Start a local Agent chat. Give it a task in repo A.
Let the agent call MCP cursor-app-control / move_agent_to_root with rootPath = repo A (even if A is already in the workspace).
Observe: “Switching workspace root” → generation ends cancelled → new workbench may open on repo A → no agent.turn.start on the new composer.

Also repros:

Worktree on a local-only feat/* (never pushed) → git fetch origin → Remote branch not found on origin.
Calling the tool mid-turn (the common case: the tool itself is the in-flight MCP call).

Expected Behavior

Destination already the current workspace (or a folder in the current multi-root) → no-op, return success, do not abort.
Real move: migrate composer, then continue the same turn with a synthetic tool result (Moved agent root to …), without killing the stream.
Local-only branches: skip fetch origin (same as skipMigrationRemoteRefresh / move_agent_to_cloned_root).

Operating System

MacOS

Version Information

Cursor Desktop (Agent / Composer)
Version: 3.17.19 stable (darwin / arm64)

Additional Information

I had an agent analyze the underlying cursor code, to find the source, hope this helps:

Root cause (client, workbench.glass.main.js)

Pipeline in CursorAppControlMcp:

  1. handleMoveAgentToRoot
  2. isAgentAlreadyAtWorkspace — compares environment.id, not filesystem path. Multi-root vs single-folder → always “different” → full migrate.
  3. moveAgentToRootAndAwaitCompletion:
    • wait other tool calls / flush transcript
    • agentRepoService.beginMigration(composerId)
    • createAgentFromMigration(..., { skipMigrationRemoteRefresh, deferComplete: true })
    • "Resuming in new workspace..."await o.resume()
    • emit({ type: "migrationHandoffRequested", oldAgentId, newAgentId })
    • sourceFinalizer.commitArchive()
    • finally { o.dispose() }

beginMigration (local): if status === "generating" or chatGenerationUUID set:

  • abortChatAndWaitForFinish (15s timeout) — cancels the move_agent_to_root MCP call itself
  • snapshots last human message as interruptedLastUserMessage, sets wasActiveTurn: true
  • if dirty: git add -A + stash create (side effect)

createAgentFromMigration (local only, type: "existing"):

  • createComposer({ skipSelect: true })
  • restore bubbles, then
    status = (oldStatus === "generating") ? "aborted" : oldStatus
  • does not read wasActiveTurn / interruptedLastUserMessage (cloud path does)
  • turn restart only if options.resumeLastMessage or options.messagehandleMoveAgentToRoot passes neither

LoadedAgent.resume():

resumeChat(handle)  // empty user text, { isResume: true }
// resumeChat does NOT await submitChatMaybeAbortCurrent

Then the MCP layer disposes that handle immediately.

MCP return uses the comma operator — cancellation (return false) still replies "Moved agent root to …":

(await this.moveAgentToRootAndAwaitCompletion(...),
 { content: [{ type: "text", text: `Moved agent root to ${l}.` }] })

Git: unless skipMigrationRemoteRefresh (used by create_project from home), destination verification fetches origin/<current branch> and FF-merges. Unpushed feat/*Remote branch not found on origin / couldn't find remote ref.

The tool description still says ALWAYS call this after creating a worktree, so agents hit this constantly.

Suggested fix

A. No-op before migrate (cheap, stops most damage)
isAgentAlreadyAtWorkspace: treat as same if any of:

  • environment.id equal
  • single-folder uri.fsPath equals current folder or any multi-root folder
  • location.type === "worktree" and worktree path matches (already partially there)

Return success without beginMigration.

B. Don’t abort the triggering tool call (the real resume bug)
Do not call abortChatAndWaitForFinish for a move initiated by the in-flight turn.

Instead:

  1. Finish or pause the MCP call with a real tool result: Moved agent root to <path>. Continue in this workspace.
  2. Switch workspace / composer without cancelling that request id.
  3. Or: after restore, inject that tool result into the copied conversation, set status: "generating", then resume the same request.

Empty resumeChat("", { isResume: true }) on a transcript whose last assistant bubble is a cancelled tool_use is not a valid continue.

C. Wire local migration like cloud
Pass resumeLastMessage: true (or resubmit interruptedLastUserMessage only after injecting the tool result). Do not force generatingaborted if you intend to continue.

D. Lifecycle

  • await the actual submitChat / resumeChat promise
  • skipSelect: false or complete migrationHandoffRequested before commitArchive
  • do not dispose() the destination LoadedAgent until resume has started (refcount if the wrapper must be dropped)

E. Git

  • Default skipMigrationRemoteRefresh: true for move_agent_to_root, or fetch origin/<default> (main/master/dev), never the unpushed current branch
  • Never git add -A as a side effect of changing workspace root
  • On fetch failure: return a tool error, don’t abort the chat

F. MCP contract

  • Don’t use the comma operator; if moveAgentToRootAndAwaitCompletion returns false, return an error content block
  • Soften the tool description: do not always re-root; no-op if already there; prefer working_directory / absolute paths when migrate is unsafe

G. Tests to add

  1. Multi-root workspace, rootPath = one of the folders → no abort, same composerId.
  2. Mid-turn MCP move_agent_to_root → destination transcript has a tool result, next model step runs.
  3. Current branch has no origin/<branch> → move still completes or errors on the tool, chat stays alive.
  4. After move, agent.turn.outcome is not cancelled on the source without a generating destination turn.

Does this stop you from using Cursor

Yes - Cursor is unusable

Hi @Eliott_Williams!

Thanks for the report and the detailed investigation, it was genuinely helpful. I was able to reproduce this and have shared a minimal reproducer with the team.

What we pinned down: when the agent calls move_agent_to_root mid-task, the in-flight turn is interrupted while the move happens. If the move’s git step then fails, the most common trigger being that the agent’s current branch was never pushed, so the destination fetch finds nothing on origin, the move is rolled back and the interrupted task never resumes. No error surfaces in the chat, which is why it just goes quiet.

Until a fix ships, there are two workarounds: push the branch before the agent moves workspaces (git push -u origin <branch>), or start a fresh chat in the target folder instead of moving mid-task.

One thing that would help us: in your testing, did it ever go silent when the branch did exist on origin with a clean working tree? We couldn’t reproduce that configuration, so if you have a case like that, the details would let us confirm whether there’s a second issue hiding behind this one.

Been seeing the same issue and I can only ever recall this happening while working in messy git state, especially when working with multiple chats that have overlapping repos. It’s not exactly the answer to you question, but I think it aligns with the expected case you described. Possible that one agent creates the local branch, then the other touches the repo and expects the branch to be on origin.

What I can tell you is that many of my repos are on a self hosted Gitea instance not Github and that I like having multiple agents work at the same time on different parts of a same multi repo project (but the bug happens even when it’s the only agent working)
I do pay attention to putting in the cursorrules and claude.md files not to use the gh tools but to use the Gitea MCP tools I provided in the MCP config to interact with the repo in terms of PRs actions etc… but to use the usual git commands otherwise
My local repos are often dirty when doing that but it also happens when it is clean so don’t think that would be related

Btw would really love to be able to set regular self hosted git remotes as repo sources on cursor cloud agents, my company and myself don’t use Github much but Gitea on our own servers for multiple reasons and there are plenty of very good MCPs that exist to let agents easily work with it, don’t know if you’ve gotten feedback on that but it certainly would be a plus!

Let me know if you guys manage to fix it in a release, thanks for the quick response btw

@Colin — answering your question from Aug 27: yes, it also goes silent with a clean working tree and the branch present on origin.

Repro just now (still on 3.18.9, not only 3.17.x):

  • Linux (Ubuntu 24.04, x64), local Agent (not Cloud)
  • Multi-root .code-workspace with 11 folders (all already open)
  • Agent default root = first folder
  • Mid-turn call: cursor-app-control / move_agent_to_root with rootPath = another folder that is already in the same workspace
  • Destination repo: branch exists on origin, working tree clean, tracking remote OK
  • Result: MCP returns Tool execution error. Aborted — no “Moved agent root…” tool result. The turn dies. Retrying the same call in the next turn aborts again.

This is the “destination already a workspace folder → should be a no-op” case from the OP. isAgentAlreadyAtWorkspace comparing environment.id (multi-root vs single-folder) still treats it as a migrate.

Second issue after the abort: Grep/Glob stay bound to the first folder. An absolute path into a sibling workspace root is rewritten to ../<sibling> and fails with IO error … No such file or directory, even though the directory exists and Shell can open it. So one failed root-switch also breaks search for the rest of the chat.

Expected: if rootPath is already a folder in the open multi-root, return success and continue the same turn — no migrate, no abort, no Grep rewrite.

Workaround we use: never call move_agent_to_root in this workspace; use absolute paths + Shell working_directory.