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:
handleMoveAgentToRootisAgentAlreadyAtWorkspace— comparesenvironment.id, not filesystem path. Multi-root vs single-folder → always “different” → full migrate.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 themove_agent_to_rootMCP call itself- snapshots last human message as
interruptedLastUserMessage, setswasActiveTurn: 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.resumeLastMessageoroptions.message—handleMoveAgentToRootpasses 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.idequal- single-folder
uri.fsPathequals 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:
- Finish or pause the MCP call with a real tool result:
Moved agent root to <path>. Continue in this workspace. - Switch workspace / composer without cancelling that request id.
- 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 generating → aborted if you intend to continue.
D. Lifecycle
awaitthe actualsubmitChat/resumeChatpromiseskipSelect: falseor completemigrationHandoffRequestedbeforecommitArchive- do not
dispose()the destinationLoadedAgentuntil resume has started (refcount if the wrapper must be dropped)
E. Git
- Default
skipMigrationRemoteRefresh: trueformove_agent_to_root, or fetchorigin/<default>(main/master/dev), never the unpushed current branch - Never
git add -Aas 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
moveAgentToRootAndAwaitCompletionreturnsfalse, 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
- Multi-root workspace,
rootPath= one of the folders → no abort, samecomposerId. - Mid-turn MCP
move_agent_to_root→ destination transcript has a tool result, next model step runs. - Current branch has no
origin/<branch>→ move still completes or errors on the tool, chat stays alive. - After move,
agent.turn.outcomeis notcancelledon the source without a generating destination turn.
Does this stop you from using Cursor
Yes - Cursor is unusable