microsoft / microsoft/vscode

Agent plugins: auto-update permanently fails with "Cannot fast-forward to multiple branches" due to concurrent git fetch/pull in the same plugin clone

Open
#329,516 0 comments 0 reactions 1 assignee Claimed by @connor4312 View on GitHub
agent-plugins bug
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

- VS Code Version: Version: 1.132.0 (Universal), Commit: df53daabb18cd157bdb08c7f01c34df936cf12f4
- OS Version: macOS 26.3.1 (a) (25D771280a), Apple Silicon (also reported on Windows 11)
- Feature: Agent plugins (preview), plugin installed via `Chat: Install Plugin From Source`

## Steps to Reproduce

1. Enable the agent plugins preview.
2. Run `Chat: Install Plugin From Source` and enter a public git URL (repro repo: `https://github.com/Shopify/shopify-ai-toolkit`).
3. Let the upstream repo advance by at least one commit.
4. Restart VS Code. Having a second window (or the Sessions app) open makes it reproduce reliably.

## Expected

The cached plugin clone fast-forwards to the new upstream commit.

## Actual

An error notification appears on every launch:

> Failed to update: https://github.com/Shopify/shopify-ai-toolkit [Show Output]

The plugin never updates again. The state is self-perpetuating: because the update never lands, the clone stays behind, so every subsequent launch has commits to fetch and fails the same way. Two independent users (macOS and Windows 11) report the plugin has never successfully updated since install.

`Show Output` opens the built-in Git extension's output channel, which contains nothing related to plugin updates, so the failure is undiagnosable from the UI.

## Logs

From `sharedprocess.log` (the only place the real error appears):

```
2026-08-06 13:09:52.018 [error] [LocalGitService] git fetch failed: Command failed: git fetch
error: cannot lock ref 'refs/remotes/origin/main': is at cc5af6505c27939222072449278f6356857cb064 but expected 0e06bc35611e505e372de7f8cdf265e6d6dbc311
From https://github.com/Shopify/shopify-ai-toolkit
! 0e06bc3..cc5af65 main -> origin/main (unable to update local ref)
* [new branch] mirror/d8475f49b6ea -> origin/mirror/d8475f49b6ea

2026-08-06 13:09:52.101 [error] [LocalGitService] git fetch failed: Command failed: git fetch
error: cannot lock ref 'refs/remotes/origin/main': is at cc5af6505c27939222072449278f6356857cb064 but expected 0e06bc35611e505e372de7f8cdf265e6d6dbc311
From https://github.com/Shopify/shopify-ai-toolkit
! 0e06bc3..cc5af65 main -> origin/main (unable to update local ref)
* [new branch] mirror/d8475f49b6ea -> origin/mirror/d8475f49b6ea

2026-08-06 13:09:52.912 [error] [LocalGitService] git pull failed: Command failed: git pull --ff-only
fatal: Cannot fast-forward to multiple branches.
```

## Analysis

**1. Concurrent git operations in the same clone.** Two `git fetch` processes fail to lock `refs/remotes/origin/main` 83ms apart, both holding the pre-fetch value, which means a third fetch had already advanced the ref. So at least three `git fetch` processes were running in the same working directory at once.

`AgentPluginRepositoryService` serializes only *clones*:

```ts
private readonly _cloneSequencer = new SequencerByKey();
...
return this._cloneSequencer.queue(repoDir.fsPath, async () => { ... }); // ensureRepository only
```

`pullRepository` and `fetchRepository` take no such lock, and `PluginInstallService.updateAll` fans its per-plugin git work out with `Promise.all(independentGitTasks)` (plus a separate `Promise.all(gitTasks)` for grouped ones). Several plugins/marketplaces backed by the same repository, an update check running alongside an update, or a second window / the Sessions app sharing the same `agent-plugins` directory all produce concurrent `git fetch`/`git pull` in one clone.

**2. Why it becomes a hard failure.** Each `git fetch` truncates and rewrites `FETCH_HEAD`. Concurrent fetches interleave, leaving more than one entry *not* marked `not-for-merge`. `git pull --ff-only` then reads that file and dies in `builtin/pull.c`:

```c
if (merge_heads.nr > 1) {
if (opt_rebase)
die(_("Cannot rebase onto multiple branches."));
if (opt_ff && !strcmp(opt_ff, "--ff-only"))
die(_("Cannot fast-forward to multiple branches."));
}
```

A single, uncontended fetch can never produce this. A manual sequential `git pull --ff-only` in the same clone succeeds immediately.

**3. The recovery path misses it.** `LocalGitService._isFastForwardPullFailure` requires exit code 128 *and* `/not possible to fast-forward|non-fast-forward/i`. `fatal: Cannot fast-forward to multiple branches.` matches neither, so the error is rethrown before the fetch-and-retry and before the `allowHardResetOnDivergence` hard reset that would otherwise repair the clone.

**4. The failure is undiagnosable from the UI.** The notification drops the underlying error:

```ts
message: localize('updateAllFailed', "Failed to update: {0}", failedNames.join(', ')),
actions: {
primary: [new Action('showGitOutput', localize('showOutput', "Show Output"), undefined, true, () => {
this._commandService.executeCommand('git.showOutput');
})],
}
```

`git.showOutput` opens the built-in Git extension's channel, but plugin updates run through `LocalGitService` in the shared process and never write there. Users see an empty/irrelevant log and have no way to reach the real error short of grepping `sharedprocess.log` for `LocalGitService`.

## Suggested fixes

1. Serialize git operations per repository directory, not just clones: reuse `SequencerByKey` keyed on `repoDir.fsPath` for `pullRepository`/`fetchRepository` as well. Because multiple windows and the Sessions app share the same clone, an on-disk lock is needed to fully close it, not just an in-process sequencer.
2. Avoid `FETCH_HEAD` entirely in `LocalGitService.pull`: `git fetch --prune` followed by `git merge --ff-only @{u}` is immune to this whole class of failure, since the merge head is named explicitly.
3. Treat `Cannot fast-forward to multiple branches` as a recoverable fast-forward failure in `_isFastForwardPullFailure`, so the existing retry/hard-reset recovery can repair the clone.
4. Include `err.message` in the `updateAllFailed` notification, and point its action at the log that actually contains the error rather than at `git.showOutput`.

## Workaround

Quit all VS Code windows (including the Sessions app), then run a single sequential pull in the cached clone:

```
git -C ~/.vscode/agent-plugins/ pull --ff-only
```

It fast-forwards cleanly. The error returns the next time upstream advances.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.