Fix Git ref refresh resource storms#4727

merged8
main
t3code/git-ref-refresh-resource-storm
openedJul 28, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago
25 unresolved threads Β· Jump to file
Selected thread: packages/client-runtime/src/state/vcs.ts:123 β€” ### Reconnect refetch skips server refresh **Medium Severity** <!-- DESCRIPTION START --> On each connection-generation refetch, `makeCachedVcsRefsChanges` calls `vcs.listRefs` with the subscribed input unchanged. Atoms that omit `refresh: true` (for example DiffPanel local/remote base-ref queries with `refKind` and `includeMatchingRemoteRefs`) now receive cached server snapshots for up to two minutes instead of a Git rescan. Removing the five-second poll makes out-of-band ref updates visible much later on those paths. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: 87c2f2da-92cd-44ea-9f1c-0741e3d3c50d --> <!-- LOCATIONS START packages/client-runtime/src/state/vcs.ts#L106-L123 LOCATIONS END --> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjQ5NmU4MGU1LWZlMjctNGM2ZS1iOWJlLTgwNzNlZjllOWI5ZiIsImVuY3J5cHRpb25LZXkiOiJWZl9yOVM1eHFreEhVM1dIWXVlSmUzOHRNNnM1N0xqOUFCcnUzQ01sWkZvIiwiYnJhbmNoIjoidDNjb2RlL2dpdC1yZWYtcmVmcmVzaC1yZXNvdXJjZS1zdG9ybSIsInJlcG9Pd25lciI6InBpbmdkb3RnZyIsInJlcG9OYW1lIjoidDNjb2RlIn19" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjQ5NmU4MGU1LWZlMjctNGM2ZS1iOWJlLTgwNzNlZjllOWI5ZiIsImVuY3J5cHRpb25LZXkiOiJWZl9yOVM1eHFreEhVM1dIWXVlSmUzOHRNNnM1N0xqOUFCcnUzQ01sWkZvIiwiYnJhbmNoIjoidDNjb2RlL2dpdC1yZWYtcmVmcmVzaC1yZXNvdXJjZS1zdG9ybSIsInJlcG9Pd25lciI6InBpbmdkb3RnZyIsInJlcG9OYW1lIjoidDNjb2RlIiwicHJOdW1iZXIiOjQ3MjcsImNvbW1pdFNoYSI6IjhmYjg3ZmQ3N2JlMzg5MDg5YzQzOWU3OGJiODJlMjVjNjZkODQzYmMiLCJwcm92aWRlciI6ImdpdGh1YiJ9fQ" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8fb87fd77be389089c439e78bb82e25c66d843bc. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
0 / 25 viewed25 files changedLine totals unavailable

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 8fb87fd7 Β· left 123

Original diff
@@ -104,34 +104,30 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange
   ).pipe(
     Stream.map((connection) => (connection.phase === "connected" ? connection.generation : null)),
     Stream.changes,
-    Stream.switchMap((generation) =>
-      generation === null
-        ? Stream.empty
-        : Stream.tick(VCS_REFS_REVALIDATE_INTERVAL).pipe(
-            Stream.mapEffect(
-              () =>
-                refresh().pipe(
-                  Effect.map(Option.some),
-                  Effect.catch((error) =>
-                    Effect.logWarning("Could not refresh Git refs.").pipe(
-                      Effect.annotateLogs({
-                        environmentId,
-                        cwd: input.cwd,
-                        ...safeErrorLogAttributes(error),
-                      }),
-                      Effect.as(Option.none<VcsListRefsResult>()),
-                    ),
C
cursorBotJul 28, 2026

Reconnect refetch skips server refresh

Medium Severity

On each connection-generation refetch, makeCachedVcsRefsChanges calls vcs.listRefs with the subscribed input unchanged. Atoms that omit refresh: true (for example DiffPanel local/remote base-ref queries with refKind and includeMatchingRemoteRefs) now receive cached server snapshots for up to two minutes instead of a Git rescan. Removing the five-second poll makes out-of-band ref updates visible much later on those paths.

Reviewed by Cursor Bugbot for commit 8fb87fd77be389089c439e78bb82e25c66d843bc. Configure here.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision 8fb87fd7 Β· right 2283

Original diff
@@ -1987,257 +2070,226 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
       Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
     );
 
-  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
-    function* (input) {
-      const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
-        Effect.orElseSucceed(() => new Map<string, number>()),
-      );
-      const localBranchResult = yield* executeGitWithStableDiagnostics(
-        "GitVcsDriver.listRefs.branchNoColor",
-        input.cwd,
-        ["branch", "--no-color", "--no-column"],
-        {
-          timeoutMs: 10_000,
+  const readGitRefsSnapshot = Effect.fn("readGitRefsSnapshot")(function* (gitCommonDir: string) {
+    const fetchCwd =
+      path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
+    const gitDirArgs = ["--git-dir", gitCommonDir] as const;
+    const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(
+      [
+        executeGitWithStableDiagnostics(
+          "GitVcsDriver.listRefs.snapshotRefs",
+          fetchCwd,
+          [
+            ...gitDirArgs,
+            "for-each-ref",
+            "--format=%(refname)%09%(committerdate:unix)%09%(symref)",
+            "refs/heads",
+            "refs/remotes",
+          ],
+          {
+            timeoutMs: 30_000,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.defaultRef",
+          fetchCwd,
+          [...gitDirArgs, "symbolic-ref", "refs/remotes/origin/HEAD"],
+          {
+            timeoutMs: 5_000,
+            allowNonZeroExit: true,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.worktreeList",
+          fetchCwd,
+          [...gitDirArgs, "worktree", "list", "--porcelain"],
+          {
+            timeoutMs: 30_000,
+            allowNonZeroExit: true,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit("GitVcsDriver.listRefs.remoteNames", fetchCwd, [...gitDirArgs, "remote"], {
+          timeoutMs: 5_000,
           allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            isMissingGitCwdError(error)
-              ? Effect.succeed({
-                  exitCode: ChildProcessSpawner.ExitCode(128),
-                  stdout: "",
-                  stderr: "fatal: not a git repository",
-                  stdoutTruncated: false,
-                  stderrTruncated: false,
-                })
-              : Effect.fail(error),
         }),
-      );
+      ],
+      { concurrency: 2 },
+    );
 
-      if (localBranchResult.exitCode !== 0) {
-        const stderr = localBranchResult.stderr.trim();
-        if (isNonRepositoryGitStderr(stderr)) {
-          return {
-            refs: [],
-            isRepo: false,
-            hasPrimaryRemote: false,
-            nextCursor: null,
-            totalCount: 0,
-          };
-        }
-        return yield* new GitCommandError({
-          ...gitCommandContext({
-            operation: "GitVcsDriver.listRefs",
-            cwd: input.cwd,
-            args: ["branch", "--no-color", "--no-column"],
-          }),
-          detail: "Git branch listing failed.",
-          exitCode: localBranchResult.exitCode,
-          stdoutLength: localBranchResult.stdout.length,
-          stderrLength: localBranchResult.stderr.length,
+    const remoteNames =
+      remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
+    if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
+      yield* Effect.logWarning(
+        `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+      );
+    }
+    const defaultBranch =
+      defaultRefResult.exitCode === 0
+        ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
+        : null;
+    const worktreeMap =
+      worktreeListResult.exitCode === 0
+        ? new Map(
+            [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(
+              ([branchName, worktreePath]) =>
+                [branchName, path.normalize(path.resolve(worktreePath))] as const,
+            ),
+          )
+        : new Map<string, string>();
+    const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+    const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+
+    for (const line of refsResult.stdout.split("\n")) {
+      if (line.length === 0) continue;
+      const [fullRefName, lastCommitRaw, symbolicTarget] = line.split("\t");
+      if (!fullRefName || symbolicTarget) continue;
+      const parsedLastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
+      const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;
+
+      if (fullRefName.startsWith("refs/heads/")) {
+        const name = fullRefName.slice("refs/heads/".length);
+        localBranches.push({
+          ref: {
+            name,
+            current: false,
+            isRemote: false,
+            isDefault: name === defaultBranch,
+            worktreePath: worktreeMap.get(name) ?? null,
+          },
+          lastCommit,
         });
+        continue;
       }
+      if (!fullRefName.startsWith("refs/remotes/")) continue;
+
+      const name = fullRefName.slice("refs/remotes/".length);
+      const parsedRemoteRef = parseRemoteRefWithRemoteNames(name, remoteNames);
+      const remoteBranch: VcsRef = {
+        name,
+        current: false,
+        isRemote: true,
+        isDefault:
+          defaultBranch !== null &&
+          parsedRemoteRef?.remoteName === "origin" &&
+          parsedRemoteRef.branchName === defaultBranch,
+        worktreePath: null,
+        ...(parsedRemoteRef ? { remoteName: parsedRemoteRef.remoteName } : {}),
+      };
+      remoteBranches.push({ ref: remoteBranch, lastCommit });
+    }
 
-      const remoteBranchResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteBranches",
-        input.cwd,
-        ["branch", "--no-color", "--no-column", "--remotes"],
-        {
-          timeoutMs: 10_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote ref lookup failed; falling back to an empty remote ref list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
-
-      const remoteNamesResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteNames",
-        input.cwd,
-        ["remote"],
-        {
-          timeoutMs: 5_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote name lookup failed; falling back to an empty remote name list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
+    const byRecencyThenName = (
+      left: { readonly ref: VcsRef; readonly lastCommit: number },
+      right: { readonly ref: VcsRef; readonly lastCommit: number },
+    ) =>
+      left.lastCommit !== right.lastCommit
+        ? right.lastCommit - left.lastCommit
+        : left.ref.name.localeCompare(right.ref.name);
 
-      const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
-        yield* Effect.all(
-          [
-            executeGit(
-              "GitVcsDriver.listRefs.defaultRef",
-              input.cwd,
-              ["symbolic-ref", "refs/remotes/origin/HEAD"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            executeGit(
-              "GitVcsDriver.listRefs.worktreeList",
-              input.cwd,
-              ["worktree", "list", "--porcelain"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            remoteBranchResultEffect,
-            remoteNamesResultEffect,
-            branchRecencyPromise,
-          ],
-          { concurrency: "unbounded" },
-        );
+    return {
+      localBranches: localBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      remoteBranches: remoteBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      hasPrimaryRemote: remoteNames.includes("origin"),
+    } satisfies GitRefsSnapshot;
+  });
 
-      const remoteNames =
-        remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
-      if (remoteBranchResult.exitCode !== 0 && remoteBranchResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote refName lookup returned code ${remoteBranchResult.exitCode} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote refName list.`,
-        );
+  const listRefsEpochByCommonDir = new Map<string, number>();
+  let listRefsEpochSequence = 0;
+  const bumpListRefsEpoch = (gitCommonDir: string): number => {
+    const nextEpoch = ++listRefsEpochSequence;
+    listRefsEpochByCommonDir.delete(gitCommonDir);
+    listRefsEpochByCommonDir.set(gitCommonDir, nextEpoch);
+    if (listRefsEpochByCommonDir.size > LIST_REFS_SNAPSHOT_CACHE_CAPACITY) {
+      const oldestKey = listRefsEpochByCommonDir.keys().next().value;
+      if (oldestKey !== undefined) {
+        listRefsEpochByCommonDir.delete(oldestKey);
       }
-      if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+    }
+    return nextEpoch;
+  };
+  const listRefsSnapshotCache = yield* Cache.makeWith(
+    (cacheKey: GitRefsSnapshotCacheKey) => readGitRefsSnapshot(cacheKey.gitCommonDir),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_REFS_SNAPSHOT_CACHE_TTL : Duration.zero),
+    },
+  );
+  const listRefsRefreshSnapshotCache = yield* Cache.makeWith(
+    (gitCommonDir: string) =>
+      Effect.suspend(() => {
+        const epoch = bumpListRefsEpoch(gitCommonDir);
+        return Cache.get(
+          listRefsSnapshotCache,
+          new GitRefsSnapshotCacheKey({ gitCommonDir, epoch }),
         );
-      }
+      }),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_REFS_REFRESH_COALESCE_TTL : Duration.zero),
+    },
+  );
+  const resolveListRefsSnapshot = (gitCommonDir: string, refresh: boolean) => {
+    const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);
+    if (refresh || currentEpoch === undefined) {
+      // The refresh cache owns the complete snapshot read, rather than only the
+      // epoch bump. Slow repositories therefore remain singleflight for the
+      // entire Git scan even when more refresh requests arrive after the
+      // coalescing TTL would otherwise have elapsed.
+      return Cache.get(listRefsRefreshSnapshotCache, gitCommonDir);
+    }
+    return Cache.get(
+      listRefsSnapshotCache,
+      new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),
+    );
+  };
+  const invalidateListRefsSnapshot = Effect.fn("invalidateListRefsSnapshot")(function* (
+    cwd: string,
+  ) {
+    const repositoryPaths = yield* resolveRepositoryPaths(cwd);
+    if (repositoryPaths === null) return;
+    yield* Cache.invalidate(listRefsRefreshSnapshotCache, repositoryPaths.gitCommonDir);
+    bumpListRefsEpoch(repositoryPaths.gitCommonDir);
+  });
 
-      const defaultBranch =
-        defaultRef.exitCode === 0
-          ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
-          : null;
-
-      const worktreeMap = new Map<string, string>();
-      if (worktreeList.exitCode === 0) {
-        let currentPath: string | null = null;
-        for (const line of worktreeList.stdout.split("\n")) {
-          if (line.startsWith("worktree ")) {
-            const candidatePath = line.slice("worktree ".length);
-            const exists = yield* fileSystem.stat(candidatePath).pipe(
-              Effect.map(() => true),
-              Effect.orElseSucceed(() => false),
-            );
-            currentPath = exists ? candidatePath : null;
-          } else if (line.startsWith("branch refs/heads/") && currentPath) {
-            worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
-          } else if (line === "") {
-            currentPath = null;
-          }
-        }
+  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
+    function* (input) {
+      const repositoryPaths = yield* resolveRepositoryPaths(input.cwd).pipe(
+        Effect.catchTags({
+          GitCommandError: (error) =>
+            isMissingGitCwdError(error) ? Effect.succeed(null) : Effect.fail(error),
+        }),
+      );
+      if (repositoryPaths === null) {
+        return {
+          refs: [],
+          isRepo: false,
+          hasPrimaryRemote: false,
+          nextCursor: null,
+          totalCount: 0,
+        };
       }
 
-      const localBranches = Arr.filterMap(localBranchResult.stdout.split("\n"), (line) => {
-        const refName = parseBranchLine(line);
-        return refName === null
-          ? Result.failVoid
-          : Result.succeed({
-              name: refName.name,
-              current: refName.current,
-              isRemote: false,
-              isDefault: refName.name === defaultBranch,
-              worktreePath: worktreeMap.get(refName.name) ?? null,
-            });
-      }).toSorted((a, b) => {
-        const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
-        const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
-        if (aPriority !== bPriority) return aPriority - bPriority;
-
-        const aLastCommit = branchLastCommit.get(a.name) ?? 0;
-        const bLastCommit = branchLastCommit.get(b.name) ?? 0;
-        if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
-        return a.name.localeCompare(b.name);
-      });
-
-      const remoteBranches =
-        remoteBranchResult.exitCode === 0
-          ? Arr.filterMap(remoteBranchResult.stdout.split("\n"), (line) => {
-              const refName = parseBranchLine(line);
-              if (refName === null) {
-                return Result.failVoid;
-              }
-              const parsedRemoteRef = parseRemoteRefWithRemoteNames(refName.name, remoteNames);
-              const remoteBranch: {
-                name: string;
-                current: boolean;
-                isRemote: boolean;
-                remoteName?: string;
-                isDefault: boolean;
-                worktreePath: string | null;
-              } = {
-                name: refName.name,
-                current: false,
-                isRemote: true,
-                // origin/HEAD's target is the repo default even when no local
-                // copy of the default branch exists.
-                isDefault:
-                  defaultBranch !== null &&
-                  parsedRemoteRef?.remoteName === "origin" &&
-                  parsedRemoteRef.branchName === defaultBranch,
-                worktreePath: null,
-              };
-              if (parsedRemoteRef) {
-                remoteBranch.remoteName = parsedRemoteRef.remoteName;
-              }
-              return Result.succeed(remoteBranch);
-            }).toSorted((a, b) => {
-              const aLastCommit = branchLastCommit.get(a.name) ?? 0;
-              const bLastCommit = branchLastCommit.get(b.name) ?? 0;
-              if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
-              return a.name.localeCompare(b.name);
-            })
-          : [];
-
+      const snapshot = yield* resolveListRefsSnapshot(
+        repositoryPaths.gitCommonDir,
+        input.refresh === true,
+      );
+      const localBranches = snapshot.localBranches.map((ref) => ({
+        ...ref,
+        current:
+          repositoryPaths.worktreeRoot !== null &&
+          ref.worktreePath === repositoryPaths.worktreeRoot,
+      }));
C
cursorBotJul 28, 2026

Current branch never marked

Medium Severity

current is set only when ref.worktreePath equals cached worktreeRoot. If git worktree list fails or rev-parse --show-toplevel once fails, worktreeRoot stays null and the worktree map is empty, so no ref is marked current even though the repo and branches are valid.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 8fb87fd77be389089c439e78bb82e25c66d843bc. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in f2449b5152. Repository path discovery now also captures symbolic-ref HEAD, and listRefs uses it only as a fallback when the worktree snapshot cannot identify the current branch. The normal worktree mapping still wins, and mutation invalidation refreshes the fallback branch state. A focused test forces both worktree metadata calls to fail and verifies the active branch remains marked current.

packages/client-runtime/src/state/vcsRefInvalidation.ts Β· Earlier revision 5a352e1a Β· right 66

Original diff
@@ -0,0 +1,69 @@
+import type { EnvironmentId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
+import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
+
+import { safeErrorLogAttributes } from "../errors/safeLog.ts";
+import { EnvironmentCacheStore } from "../platform/persistence.ts";
+
+export interface VcsRefsInvalidationTarget {
+  readonly environmentId: EnvironmentId;
+}
+
+export interface CachedVcsRefsInvalidationTarget extends VcsRefsInvalidationTarget {
+  readonly cwd: string;
+}
+
+const revisionByEnvironment = Atom.family((environmentId: EnvironmentId) =>
+  Atom.make(0).pipe(Atom.withLabel(`environment-data:vcs:list-refs-revision:${environmentId}`)),
+);
+const persistenceLock = PartitionedSemaphore.makeUnsafe<EnvironmentId>({ permits: 1 });
+
+export function vcsRefsRevisionAtom(target: VcsRefsInvalidationTarget) {
+  return revisionByEnvironment(target.environmentId);
+}
+
+export function invalidateVcsRefs(
+  registry: AtomRegistry.AtomRegistry,
+  target: VcsRefsInvalidationTarget,
+): void {
+  registry.update(vcsRefsRevisionAtom(target), (revision) => revision + 1);
+}
+
+export function withVcsRefsPersistenceLock<A, E, R>(
+  environmentId: EnvironmentId,
+  effect: Effect.Effect<A, E, R>,
+): Effect.Effect<A, E, R> {
+  return persistenceLock.withPermit(environmentId)(effect);
+}
+
+/**
+ * Clears persisted snapshots and then restarts live ref streams while holding
+ * the same lock used by generation-checked refresh writes. Clearing first
+ * prevents the restarted stream from rehydrating the invalidated snapshot. A
+ * refresh that wins the lock is cleared afterward; one that loses observes the
+ * new revision and cannot repersist its pre-mutation result.
+ */
+export const invalidateCachedVcsRefs = Effect.fn("VcsRefsState.invalidateCached")(function* (
+  registry: AtomRegistry.AtomRegistry,
+  target: CachedVcsRefsInvalidationTarget,
+) {
+  const cache = yield* EnvironmentCacheStore;
+  yield* withVcsRefsPersistenceLock(
+    target.environmentId,
+    Effect.gen(function* () {
+      yield* cache.clearVcsRefs(target.environmentId).pipe(
+        Effect.catch((error) =>
+          Effect.logWarning("Could not remove invalidated cached Git refs.").pipe(
+            Effect.annotateLogs({
+              environmentId: target.environmentId,
+              cwd: target.cwd,
+              ...safeErrorLogAttributes(error),
+            }),
+          ),
+        ),
+      );
+      invalidateVcsRefs(registry, target);
C
cursorBotJul 28, 2026

Clear failure still rehydrates stale cache

Medium Severity

When clearVcsRefs fails, invalidation still bumps the ref revision and restarts live streams. makeCachedVcsRefsChanges emits whatever loadVcsRefs returns first, so a failed clear can replay pre-mutation branch snapshots until a live refresh succeedsβ€”especially misleading offline or on slow connections.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 5a352e1a3aa4a04b69f4169af4fa3b4c5d48bb07. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in b3f2a38da2. Ref cache state now atomically tracks both revision and whether persisted snapshots are readable. A failed environment-wide clear publishes a new generation with persisted reads disabled, so restarted/offline streams cannot replay stale data; live refreshes can still repopulate storage. The regression test forces clear failure and verifies no persisted load occurs.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision 25c0c7ab Β· right 246

Original diff
@@ -210,6 +228,37 @@ function paginateBranches(input: {
   };
 }
 
+function parseWorktreeBranchPaths(stdout: string): ReadonlyMap<string, string> {
+  const worktreePaths = new Map<string, string>();
+  let currentPath: string | null = null;
+  let currentBranch: string | null = null;
+  let currentPrunable = false;
+
+  const flush = () => {
+    if (currentPath !== null && currentBranch !== null && !currentPrunable) {
+      worktreePaths.set(currentBranch, currentPath);
+    }
+    currentPath = null;
+    currentBranch = null;
+    currentPrunable = false;
+  };
+
+  for (const line of stdout.split("\n")) {
M
macroscopeappBotJul 28, 2026

🟑 Medium vcs/GitVcsDriverCore.ts:246

parseWorktreeBranchPaths splits git worktree list --porcelain output on \n, but worktree paths can legally contain newline characters. A worktree whose path contains a newline is split into multiple pseudo-lines, causing currentPath to be overwritten or misparsed, and the branch is dropped from worktreePaths. The caller then incorrectly reports worktreePath: null for that checked-out branch. Git's porcelain format requires parsing NUL-delimited (-z) output to handle paths with embedded newlines. Consider invoking the command with -z and parsing NUL-delimited records instead of splitting on \n.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/vcs/GitVcsDriverCore.ts around line 246:
2
3`parseWorktreeBranchPaths` splits `git worktree list --porcelain` output on `\n`, but worktree paths can legally contain newline characters. A worktree whose path contains a newline is split into multiple pseudo-lines, causing `currentPath` to be overwritten or misparsed, and the branch is dropped from `worktreePaths`. The caller then incorrectly reports `worktreePath: null` for that checked-out branch. Git's porcelain format requires parsing NUL-delimited (`-z`) output to handle paths with embedded newlines. Consider invoking the command with `-z` and parsing NUL-delimited records instead of splitting on `\n`.
J
Julius MarmingeJul 28, 2026

Fixed in 7fa76c88e2. Worktree enumeration now requests git worktree list --porcelain -z and parses NUL-delimited fields/records, preserving embedded newlines. A backend integration test creates a real newline-containing worktree path and verifies the listed branch retains its canonical path.

apps/web/src/state/paginatedBranches.ts Β· Earlier revision 33c01c57 Β· right 31–31

Original diff
@@ -0,0 +1,36 @@
+import type { VcsListRefsResult } from "@t3tools/contracts";
+import * as Option from "effect/Option";
+import { AsyncResult } from "effect/unstable/reactivity";
+
+const DEFAULT_NEXT_PAGE_DISTANCE_PX = 96;
+
+export function isPaginatedBranchesNextPagePending<E>(
+  results: ReadonlyArray<AsyncResult.AsyncResult<VcsListRefsResult, E>>,
+): boolean {
+  const lastResult = results.at(-1);
+  return (
+    results.length > 1 &&
+    lastResult?.waiting === true &&
+    Option.isNone(AsyncResult.value(lastResult))
+  );
+}
+
+export function shouldLoadNextBranchPageAfterScroll({
+  previousScrollTop,
+  scrollTop,
+  scrollHeight,
+  clientHeight,
+  distanceThreshold = DEFAULT_NEXT_PAGE_DISTANCE_PX,
+}: {
+  previousScrollTop: number | null;
+  scrollTop: number;
+  scrollHeight: number;
+  clientHeight: number;
+  distanceThreshold?: number;
+}): boolean {
+  if (previousScrollTop === null || scrollTop <= previousScrollTop + 1) {

apps/web/src/state/paginatedBranches.ts Β· right 31–31

M
macroscopeappBotJul 28, 2026

🟑 Medium state/paginatedBranches.ts:31

shouldLoadNextBranchPageAfterScroll never loads the next page during slow or smooth scrolling. The guard scrollTop <= previousScrollTop + 1 treats any downward movement of 1px or less as "not scrolling down," so successive subpixel or 1px scroll events are all rejected β€” even when the user reaches the bottom, fetchNextBranchPage is never called. Because scrollTop is subpixel-precise on modern browsers/trackpads, this is a common case, not an edge case. The direction check should reject only non-increasing movement (scrollTop <= previousScrollTop), not deltas <= 1.

suggestion
1 if (previousScrollTop === null || scrollTop <= previousScrollTop) {
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/state/paginatedBranches.ts around line 31:
2
3`shouldLoadNextBranchPageAfterScroll` never loads the next page during slow or smooth scrolling. The guard `scrollTop <= previousScrollTop + 1` treats any downward movement of 1px or less as "not scrolling down," so successive subpixel or 1px scroll events are all rejected β€” even when the user reaches the bottom, `fetchNextBranchPage` is never called. Because `scrollTop` is subpixel-precise on modern browsers/trackpads, this is a common case, not an edge case. The direction check should reject only non-increasing movement (`scrollTop <= previousScrollTop`), not deltas `<= 1`.
J
Julius MarmingeJul 28, 2026

Fixed in 6deb060e97. The direction guard now rejects only non-increasing positions, so 1px and subpixel trackpad movement can trigger the next page at the threshold. The focused regression test covers a 0.5px downward movement at the boundary.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision ff4df7fe Β· left 90

Original diff
@@ -82,15 +90,7 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange
     return refs;
   });
 
-  const cachedRefs = Stream.fromEffect(
-    SubscriptionRef.get(supervisor.state).pipe(
-      Effect.flatMap((connection) =>
-        connection.phase === "connected"
-          ? Effect.succeed(Option.none<VcsListRefsResult>())
-          : Effect.succeed(cached),
C
cursorBotJul 28, 2026

Late refresh repersists stale refs

Medium Severity

An in-flight vcsListRefs refresh can finish after a ref mutation runs clearVcsRefs and bumps the environment revision. refresh always calls saveVcsRefs with whatever the RPC returns, without tying the write to the current revision or cancelling superseded requests, so a pre-mutation snapshot can be written back to persistence and replayed on remount.

Reviewed by Cursor Bugbot for commit ff4df7fe6374103df56fa648a4a8584a3af54b4c. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 4ba5ea3a5d. Each refresh now captures the environment ref revision, and its result is committed/emitted only while holding the same per-environment lock used by invalidation. Invalidation advances the revision and clears persisted refs under that lock, so either a prior save is subsequently cleared or a late result observes the new revision and is discarded. Added regression coverage for stale rejection and current-generation persistence; focused tests plus client-runtime/web/mobile typechecks pass.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 5a352e1a Β· right 297

Original diff
@@ -193,30 +266,35 @@ export function createVcsEnvironmentAtoms<R, E>(
       tag: WS_METHODS.vcsCreateWorktree,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     removeWorktree: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:remove-worktree",
       tag: WS_METHODS.vcsRemoveWorktree,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     createRef: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:create-ref",
       tag: WS_METHODS.vcsCreateRef,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     switchRef: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:switch-ref",
       tag: WS_METHODS.vcsSwitchRef,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     init: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:init",
       tag: WS_METHODS.vcsInit,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
C
cursorBotJul 28, 2026

Failed mutations skip ref invalidation

High Severity

Several mutating RPCs clear persisted ref snapshots only via onSuccess, while pull uses onSettled. If Git already changed (for example publishRepository adds a remote then push fails), the RPC error skips cache clearing and revision bump, so mounted ref streams keep serving stale branch data until reconnect because background polling was removed.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 5a352e1a3aa4a04b69f4169af4fa3b4c5d48bb07. Configure here.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision 25c0c7ab Β· right 2344

Original diff
@@ -1987,257 +2148,242 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
       Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
     );
 
-  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
-    function* (input) {
-      const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
-        Effect.orElseSucceed(() => new Map<string, number>()),
-      );
-      const localBranchResult = yield* executeGitWithStableDiagnostics(
-        "GitVcsDriver.listRefs.branchNoColor",
-        input.cwd,
-        ["branch", "--no-color", "--no-column"],
-        {
-          timeoutMs: 10_000,
+  const readGitRefsSnapshot = Effect.fn("readGitRefsSnapshot")(function* (gitCommonDir: string) {
+    const fetchCwd =
+      path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
+    const gitDirArgs = ["--git-dir", gitCommonDir] as const;
+    const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(
+      [
+        executeGitWithStableDiagnostics(
+          "GitVcsDriver.listRefs.snapshotRefs",
+          fetchCwd,
+          [
+            ...gitDirArgs,
+            "for-each-ref",
+            "--format=%(refname)%09%(committerdate:unix)%09%(symref)",
+            "refs/heads",
+            "refs/remotes",
+          ],
+          {
+            timeoutMs: 30_000,
+            maxOutputBytes: 16 * 1024 * 1024,
+            fallbackErrorDetail: "Git ref snapshot enumeration failed.",
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.defaultRef",
+          fetchCwd,
+          [...gitDirArgs, "symbolic-ref", "refs/remotes/origin/HEAD"],
+          {
+            timeoutMs: 5_000,
+            allowNonZeroExit: true,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.worktreeList",
+          fetchCwd,
+          [...gitDirArgs, "worktree", "list", "--porcelain"],
+          {
+            timeoutMs: 30_000,
+            allowNonZeroExit: true,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit("GitVcsDriver.listRefs.remoteNames", fetchCwd, [...gitDirArgs, "remote"], {
+          timeoutMs: 5_000,
           allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            isMissingGitCwdError(error)
-              ? Effect.succeed({
-                  exitCode: ChildProcessSpawner.ExitCode(128),
-                  stdout: "",
-                  stderr: "fatal: not a git repository",
-                  stdoutTruncated: false,
-                  stderrTruncated: false,
-                })
-              : Effect.fail(error),
         }),
-      );
+      ],
+      { concurrency: 2 },
+    );
 
-      if (localBranchResult.exitCode !== 0) {
-        const stderr = localBranchResult.stderr.trim();
-        if (isNonRepositoryGitStderr(stderr)) {
-          return {
-            refs: [],
-            isRepo: false,
-            hasPrimaryRemote: false,
-            nextCursor: null,
-            totalCount: 0,
-          };
-        }
-        return yield* new GitCommandError({
-          ...gitCommandContext({
-            operation: "GitVcsDriver.listRefs",
-            cwd: input.cwd,
-            args: ["branch", "--no-color", "--no-column"],
-          }),
-          detail: "Git branch listing failed.",
-          exitCode: localBranchResult.exitCode,
-          stdoutLength: localBranchResult.stdout.length,
-          stderrLength: localBranchResult.stderr.length,
+    const remoteNames =
+      remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
+    if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
+      yield* Effect.logWarning(
+        `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+      );
+    }
+    const defaultBranch =
+      defaultRefResult.exitCode === 0
+        ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
+        : null;
+    const parsedWorktreeEntries =
+      worktreeListResult.exitCode === 0
+        ? [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(
+            ([branchName, worktreePath]) =>
+              [branchName, path.normalize(path.resolve(worktreePath))] as const,
+          )
+        : [];
+    const existingWorktreeEntries = yield* Effect.filter(
+      parsedWorktreeEntries,
+      ([, worktreePath]) =>
+        fileSystem.stat(worktreePath).pipe(
+          Effect.as(true),
+          Effect.orElseSucceed(() => false),
+        ),
+      { concurrency: 16 },
+    );
+    const worktreeMap = new Map(existingWorktreeEntries);
+    const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+    const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+
+    for (const line of refsResult.stdout.split("\n")) {
+      if (line.length === 0) continue;
+      const [fullRefName, lastCommitRaw, symbolicTarget] = line.split("\t");
+      if (!fullRefName || symbolicTarget) continue;
+      const parsedLastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
+      const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;
+
+      if (fullRefName.startsWith("refs/heads/")) {
+        const name = fullRefName.slice("refs/heads/".length);
+        localBranches.push({
+          ref: {
+            name,
+            current: false,
+            isRemote: false,
+            isDefault: name === defaultBranch,
+            worktreePath: worktreeMap.get(name) ?? null,
+          },
+          lastCommit,
         });
+        continue;
       }
+      if (!fullRefName.startsWith("refs/remotes/")) continue;
+
+      const name = fullRefName.slice("refs/remotes/".length);
+      const parsedRemoteRef = parseRemoteRefWithRemoteNames(name, remoteNames);
+      const remoteBranch: VcsRef = {
+        name,
+        current: false,
+        isRemote: true,
+        isDefault:
+          defaultBranch !== null &&
+          parsedRemoteRef?.remoteName === "origin" &&
+          parsedRemoteRef.branchName === defaultBranch,
+        worktreePath: null,
+        ...(parsedRemoteRef ? { remoteName: parsedRemoteRef.remoteName } : {}),
+      };
+      remoteBranches.push({ ref: remoteBranch, lastCommit });
+    }
 
-      const remoteBranchResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteBranches",
-        input.cwd,
-        ["branch", "--no-color", "--no-column", "--remotes"],
-        {
-          timeoutMs: 10_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote ref lookup failed; falling back to an empty remote ref list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
-
-      const remoteNamesResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteNames",
-        input.cwd,
-        ["remote"],
-        {
-          timeoutMs: 5_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote name lookup failed; falling back to an empty remote name list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
+    const byRecencyThenName = (
+      left: { readonly ref: VcsRef; readonly lastCommit: number },
+      right: { readonly ref: VcsRef; readonly lastCommit: number },
+    ) =>
+      left.lastCommit !== right.lastCommit
+        ? right.lastCommit - left.lastCommit
+        : left.ref.name.localeCompare(right.ref.name);
 
-      const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
-        yield* Effect.all(
-          [
-            executeGit(
-              "GitVcsDriver.listRefs.defaultRef",
-              input.cwd,
-              ["symbolic-ref", "refs/remotes/origin/HEAD"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            executeGit(
-              "GitVcsDriver.listRefs.worktreeList",
-              input.cwd,
-              ["worktree", "list", "--porcelain"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            remoteBranchResultEffect,
-            remoteNamesResultEffect,
-            branchRecencyPromise,
-          ],
-          { concurrency: "unbounded" },
-        );
+    return {
+      localBranches: localBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      remoteBranches: remoteBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      hasPrimaryRemote: remoteNames.includes("origin"),
+    } satisfies GitRefsSnapshot;
+  });
 
-      const remoteNames =
-        remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
-      if (remoteBranchResult.exitCode !== 0 && remoteBranchResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote refName lookup returned code ${remoteBranchResult.exitCode} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote refName list.`,
-        );
+  const listRefsEpochByCommonDir = new Map<string, number>();
+  let listRefsEpochSequence = 0;
+  const bumpListRefsEpoch = (gitCommonDir: string): number => {
+    const nextEpoch = ++listRefsEpochSequence;
+    listRefsEpochByCommonDir.delete(gitCommonDir);
+    listRefsEpochByCommonDir.set(gitCommonDir, nextEpoch);
+    if (listRefsEpochByCommonDir.size > LIST_REFS_SNAPSHOT_CACHE_CAPACITY) {
+      const oldestKey = listRefsEpochByCommonDir.keys().next().value;
+      if (oldestKey !== undefined) {
+        listRefsEpochByCommonDir.delete(oldestKey);
       }
-      if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+    }
+    return nextEpoch;
+  };
+  const listRefsSnapshotCache = yield* Cache.makeWith(
+    (cacheKey: GitRefsSnapshotCacheKey) => readGitRefsSnapshot(cacheKey.gitCommonDir),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_REFS_SNAPSHOT_CACHE_TTL : Duration.zero),
+    },
+  );
+  const listRefsRefreshSnapshotCache = yield* Cache.makeWith(
+    (gitCommonDir: string) =>
+      Effect.suspend(() => {
+        const epoch = bumpListRefsEpoch(gitCommonDir);
+        return Cache.get(
+          listRefsSnapshotCache,
+          new GitRefsSnapshotCacheKey({ gitCommonDir, epoch }),
         );
-      }
+      }),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) =>
+        Exit.isSuccess(exit) ? LIST_REFS_REFRESH_COALESCE_TTL : LIST_REFS_REFRESH_FAILURE_COOLDOWN,
+    },
+  );
+  const resolveListRefsSnapshot = (gitCommonDir: string, refresh: boolean) => {
+    const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);
+    if (refresh || currentEpoch === undefined) {
+      // The refresh cache owns the complete snapshot read, rather than only the
+      // epoch bump. Slow repositories therefore remain singleflight for the
+      // entire Git scan even when more refresh requests arrive after the
+      // coalescing TTL would otherwise have elapsed.
+      return Cache.get(listRefsRefreshSnapshotCache, gitCommonDir);
+    }
+    return Cache.get(
+      listRefsSnapshotCache,
+      new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),
+    );
+  };
+  const invalidateListRefsSnapshot = Effect.fn("invalidateListRefsSnapshot")(function* (
+    cwd: string,
+  ) {
+    const repositoryPathsCacheKey = normalizeRepositoryPathsCacheKey(cwd);
+    const repositoryPaths = yield* Cache.get(repositoryPathsCache, repositoryPathsCacheKey);
+    if (repositoryPaths === null) return;
+    yield* Cache.invalidate(listRefsRefreshSnapshotCache, repositoryPaths.gitCommonDir);
+    bumpListRefsEpoch(repositoryPaths.gitCommonDir);
+    yield* Cache.invalidate(repositoryPathsRefreshCache, repositoryPathsCacheKey);
+    yield* Cache.invalidate(repositoryPathsCache, repositoryPathsCacheKey);
C
cursorBotJul 28, 2026

Coalesced refresh ignores invalidation

Medium Severity

A listRefs refresh singleflight started before a mutating command can still finish and satisfy waiters after invalidateListRefsSnapshot runs. Those callers may receive a snapshot taken before the mutation, so branch lists and current-branch markers stay wrong until another explicit refresh.

Reviewed by Cursor Bugbot for commit 25c0c7abc81393ea9f52fb054823b928589327ba. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 5a352e1a3a. Server ref-snapshot refreshes now carry a monotonic per-repository generation. Invalidation advances both generation and snapshot epoch before yielding; any older in-flight result detects the mismatch and retries through a generation-keyed single-flight, so it cannot commit stale data after the mutation. A deterministic backend regression test pauses the first snapshot after its ref scan, mutates refs, resumes it, and verifies the waiter retries and sees the new branch.

packages/client-runtime/src/state/vcsRefInvalidation.ts Β· Earlier revision 4f3a6609 Β· right 52

Original diff
@@ -0,0 +1,53 @@
+import type { EnvironmentId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
+
+import { safeErrorLogAttributes } from "../errors/safeLog.ts";
+import { EnvironmentCacheStore } from "../platform/persistence.ts";
+
+export interface VcsRefsInvalidationTarget {
+  readonly environmentId: EnvironmentId;
+}
+
+export interface CachedVcsRefsInvalidationTarget extends VcsRefsInvalidationTarget {
+  readonly cwd: string;
+}
+
+const revisionByEnvironment = Atom.family((environmentId: EnvironmentId) =>
+  Atom.make(0).pipe(Atom.withLabel(`environment-data:vcs:list-refs-revision:${environmentId}`)),
+);
+
+export function vcsRefsRevisionAtom(target: VcsRefsInvalidationTarget) {
+  return revisionByEnvironment(target.environmentId);
+}
+
+export function invalidateVcsRefs(
+  registry: AtomRegistry.AtomRegistry,
+  target: VcsRefsInvalidationTarget,
+): void {
+  registry.update(vcsRefsRevisionAtom(target), (revision) => revision + 1);
+}
+
+/**
+ * Removes the persisted snapshot before restarting live ref streams. This
+ * prevents a remount during the refresh window from replaying pre-mutation
+ * branch data.
+ */
+export const invalidateCachedVcsRefs = Effect.fn("VcsRefsState.invalidateCached")(function* (
+  registry: AtomRegistry.AtomRegistry,
+  target: CachedVcsRefsInvalidationTarget,
+) {
+  const cache = yield* EnvironmentCacheStore;
+  yield* cache.removeVcsRefs(target.environmentId, target.cwd).pipe(
+    Effect.catch((error) =>
+      Effect.logWarning("Could not remove invalidated cached Git refs.").pipe(
+        Effect.annotateLogs({
+          environmentId: target.environmentId,
+          cwd: target.cwd,
+          ...safeErrorLogAttributes(error),
+        }),
+      ),
+    ),
+  );
+  invalidateVcsRefs(registry, target);
C
cursorBotJul 28, 2026

Cross-worktree stale persisted refs

Medium Severity

The invalidateCachedVcsRefs function clears the persisted VCS ref cache only for the specific cwd it's called with. Because it triggers an environment-wide revision bump, other linked worktrees in the same environment can briefly display stale branch data from their local cache until live data loads.

Reviewed by Cursor Bugbot for commit 4f3a66092b7edf4f27fc45a24ad79fbb501d7a3f. Configure here.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision f2449b51 Β· right 236

Original diff
@@ -210,6 +227,37 @@ function paginateBranches(input: {
   };
 }
 
+function parseWorktreeBranchPaths(stdout: string): ReadonlyMap<string, string> {
+  const worktreePaths = new Map<string, string>();
+  let currentPath: string | null = null;
+  let currentBranch: string | null = null;
+  let currentPrunable = false;
+
+  const flush = () => {
M
macroscopeappBotJul 28, 2026

🟑 Medium vcs/GitVcsDriverCore.ts:236

parseWorktreeBranchPaths returns worktree paths without verifying they exist on disk. Git omits the prunable marker until its prune-expiry threshold elapses, so a worktree directory deleted manually still appears as a valid entry β€” the returned map points the branch at a nonexistent path. Downstream branch selection can then try to reuse that stale worktree instead of checking out the branch normally. The previous implementation explicitly stated each path, which dropped these entries. Consider re-statting each path before adding it to the map (or pruning stale worktrees first) so deleted directories are not treated as usable.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/vcs/GitVcsDriverCore.ts around line 236:
2
3`parseWorktreeBranchPaths` returns worktree paths without verifying they exist on disk. Git omits the `prunable` marker until its prune-expiry threshold elapses, so a worktree directory deleted manually still appears as a valid entry β€” the returned map points the branch at a nonexistent path. Downstream branch selection can then try to reuse that stale worktree instead of checking out the branch normally. The previous implementation explicitly `stat`ed each path, which dropped these entries. Consider re-statting each path before adding it to the map (or pruning stale worktrees first) so deleted directories are not treated as usable.
J
Julius MarmingeJul 28, 2026

Fixed in 6caf60955e. Ref snapshots now stat every non-prunable worktree path with bounded concurrency and omit entries that no longer exist. The regression test supplies a stale porcelain entry for a deleted directory and verifies the branch is returned without a reusable worktree path.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision 8fb87fd7 Β· right 2195

Original diff
@@ -1987,257 +2070,226 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
       Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
     );
 
-  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
-    function* (input) {
-      const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
-        Effect.orElseSucceed(() => new Map<string, number>()),
-      );
-      const localBranchResult = yield* executeGitWithStableDiagnostics(
-        "GitVcsDriver.listRefs.branchNoColor",
-        input.cwd,
-        ["branch", "--no-color", "--no-column"],
-        {
-          timeoutMs: 10_000,
+  const readGitRefsSnapshot = Effect.fn("readGitRefsSnapshot")(function* (gitCommonDir: string) {
+    const fetchCwd =
+      path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
+    const gitDirArgs = ["--git-dir", gitCommonDir] as const;
+    const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(
+      [
+        executeGitWithStableDiagnostics(
+          "GitVcsDriver.listRefs.snapshotRefs",
+          fetchCwd,
+          [
+            ...gitDirArgs,
+            "for-each-ref",
+            "--format=%(refname)%09%(committerdate:unix)%09%(symref)",
+            "refs/heads",
+            "refs/remotes",
+          ],
+          {
+            timeoutMs: 30_000,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.defaultRef",
+          fetchCwd,
+          [...gitDirArgs, "symbolic-ref", "refs/remotes/origin/HEAD"],
+          {
+            timeoutMs: 5_000,
+            allowNonZeroExit: true,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.worktreeList",
+          fetchCwd,
+          [...gitDirArgs, "worktree", "list", "--porcelain"],
+          {
+            timeoutMs: 30_000,
+            allowNonZeroExit: true,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit("GitVcsDriver.listRefs.remoteNames", fetchCwd, [...gitDirArgs, "remote"], {
+          timeoutMs: 5_000,
           allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            isMissingGitCwdError(error)
-              ? Effect.succeed({
-                  exitCode: ChildProcessSpawner.ExitCode(128),
-                  stdout: "",
-                  stderr: "fatal: not a git repository",
-                  stdoutTruncated: false,
-                  stderrTruncated: false,
-                })
-              : Effect.fail(error),
         }),
-      );
+      ],
+      { concurrency: 2 },
+    );
 
-      if (localBranchResult.exitCode !== 0) {
-        const stderr = localBranchResult.stderr.trim();
-        if (isNonRepositoryGitStderr(stderr)) {
-          return {
-            refs: [],
-            isRepo: false,
-            hasPrimaryRemote: false,
-            nextCursor: null,
-            totalCount: 0,
-          };
-        }
-        return yield* new GitCommandError({
-          ...gitCommandContext({
-            operation: "GitVcsDriver.listRefs",
-            cwd: input.cwd,
-            args: ["branch", "--no-color", "--no-column"],
-          }),
-          detail: "Git branch listing failed.",
-          exitCode: localBranchResult.exitCode,
-          stdoutLength: localBranchResult.stdout.length,
-          stderrLength: localBranchResult.stderr.length,
+    const remoteNames =
+      remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
+    if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
+      yield* Effect.logWarning(
+        `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+      );
+    }
+    const defaultBranch =
+      defaultRefResult.exitCode === 0
+        ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
+        : null;
+    const worktreeMap =
+      worktreeListResult.exitCode === 0
+        ? new Map(
+            [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(
+              ([branchName, worktreePath]) =>
+                [branchName, path.normalize(path.resolve(worktreePath))] as const,
+            ),
+          )
+        : new Map<string, string>();
+    const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+    const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+
+    for (const line of refsResult.stdout.split("\n")) {
+      if (line.length === 0) continue;
+      const [fullRefName, lastCommitRaw, symbolicTarget] = line.split("\t");
+      if (!fullRefName || symbolicTarget) continue;
+      const parsedLastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
+      const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;
+
+      if (fullRefName.startsWith("refs/heads/")) {
+        const name = fullRefName.slice("refs/heads/".length);
+        localBranches.push({
+          ref: {
+            name,
+            current: false,
+            isRemote: false,
+            isDefault: name === defaultBranch,
+            worktreePath: worktreeMap.get(name) ?? null,
+          },
+          lastCommit,
         });
+        continue;
       }
+      if (!fullRefName.startsWith("refs/remotes/")) continue;
+
+      const name = fullRefName.slice("refs/remotes/".length);
+      const parsedRemoteRef = parseRemoteRefWithRemoteNames(name, remoteNames);
+      const remoteBranch: VcsRef = {
+        name,
+        current: false,
+        isRemote: true,
+        isDefault:
+          defaultBranch !== null &&
+          parsedRemoteRef?.remoteName === "origin" &&
+          parsedRemoteRef.branchName === defaultBranch,
+        worktreePath: null,
+        ...(parsedRemoteRef ? { remoteName: parsedRemoteRef.remoteName } : {}),
+      };
+      remoteBranches.push({ ref: remoteBranch, lastCommit });
+    }
 
-      const remoteBranchResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteBranches",
-        input.cwd,
-        ["branch", "--no-color", "--no-column", "--remotes"],
-        {
-          timeoutMs: 10_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote ref lookup failed; falling back to an empty remote ref list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
-
-      const remoteNamesResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteNames",
-        input.cwd,
-        ["remote"],
-        {
-          timeoutMs: 5_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote name lookup failed; falling back to an empty remote name list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
+    const byRecencyThenName = (
+      left: { readonly ref: VcsRef; readonly lastCommit: number },
+      right: { readonly ref: VcsRef; readonly lastCommit: number },
+    ) =>
+      left.lastCommit !== right.lastCommit
+        ? right.lastCommit - left.lastCommit
+        : left.ref.name.localeCompare(right.ref.name);
 
-      const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
-        yield* Effect.all(
-          [
-            executeGit(
-              "GitVcsDriver.listRefs.defaultRef",
-              input.cwd,
-              ["symbolic-ref", "refs/remotes/origin/HEAD"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            executeGit(
-              "GitVcsDriver.listRefs.worktreeList",
-              input.cwd,
-              ["worktree", "list", "--porcelain"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            remoteBranchResultEffect,
-            remoteNamesResultEffect,
-            branchRecencyPromise,
-          ],
-          { concurrency: "unbounded" },
-        );
+    return {
+      localBranches: localBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      remoteBranches: remoteBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      hasPrimaryRemote: remoteNames.includes("origin"),
+    } satisfies GitRefsSnapshot;
C
cursorBotJul 28, 2026

Ignored for-each-ref failures

High Severity

readGitRefsSnapshot parses refsResult.stdout without checking refsResult.exitCode or truncation, unlike the remote, default-ref, and worktree lookups. Because Git subprocess calls allow non-zero exits, a failed for-each-ref can produce an empty snapshot that listRefs still serves with isRepo: true and may cache for minutes.

Reviewed by Cursor Bugbot for commit 8fb87fd77be389089c439e78bb82e25c66d843bc. Configure here.

J
Julius MarmingeJul 28, 2026

Confirmed that executeGit already turns a non-zero exit into GitCommandError when allowNonZeroExit is omitted; the snapshot call does omit it. I made that contract explicit with a fallback error detail and added a regression test that forces for-each-ref to exit 128 and verifies listRefs fails with GitCommandError instead of caching an empty repository snapshot.

packages/client-runtime/src/state/vcsAction.ts Β· Earlier revision 561c0357 Β· right 497

Original diff
@@ -489,6 +490,11 @@ export function createVcsActionManager<R, E>(
               }),
           },
         ).pipe(
+          Effect.tap(() =>
+            Effect.sync(() => {
+              invalidateVcsRefs(registry, target);
+            }),
+          ),
C
cursorBotJul 28, 2026

Failed stacked actions skip ref invalidation

Medium Severity

runStackedAction bumps the environment ref revision only when the stacked-action stream completes successfully. If Git already applied an earlier phase (such as creating a feature branch) and a later phase ends in action_failed, the client keeps serving cached ref streams with no reconnect and no periodic polling, so new branches can stay missing while the session remains connected.

Reviewed by Cursor Bugbot for commit 561c035782fce4bbd1901f8213f4cc6f8393d43b. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 4f3a66092b. Stacked-action ref invalidation now runs in Effect.ensuring, so success, failure after a mutating phase, and interruption all clear persisted refs and advance the live ref revision. The focused test now covers both the completed and action_failed terminal paths.

packages/client-runtime/src/state/vcs.ts Β· File comment

M
macroscopeappBotJul 28, 2026

🟑 Medium

https://github.com/pingdotgg/t3code/blob/25c0c7abc81393ea9f52fb054823b928589327ba/packages/client-runtime/src/state/vcs.ts#L251-L256

pull only invalidates cached VCS refs in onSuccess, so a git pull that fetches new remote-tracking refs but then fails during merge/rebase leaves stale cached refs. The RPC reports failure, so onSuccess never fires, yet the remote refs have already changed β€” branch pickers keep showing the pre-pull snapshot until reconnect or another mutation. Consider invalidating on both success and failure (e.g. via Effect.tapBoth), or invalidating before the RPC so the next ref stream always re-fetches.

diff
1- onSuccess: invalidateRefs,
2+ onSettled: invalidateRefs,
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/client-runtime/src/state/vcs.ts around lines 251-256:
2
3`pull` only invalidates cached VCS refs in `onSuccess`, so a `git pull` that fetches new remote-tracking refs but then fails during merge/rebase leaves stale cached refs. The RPC reports failure, so `onSuccess` never fires, yet the remote refs have already changed β€” branch pickers keep showing the pre-pull snapshot until reconnect or another mutation. Consider invalidating on both success and failure (e.g. via `Effect.tapBoth`), or invalidating before the RPC so the next ref stream always re-fetches.
J
Julius MarmingeJul 28, 2026

Fixed in 7fa76c88e2. The shared RPC command helper now supports an onSettled hook, and pull uses it to clear persisted refs and publish a new ref generation on success or failure. The focused client regression test forces pull to fail and verifies both cache clearing and the revision bump.

packages/client-runtime/src/state/vcsRefInvalidation.ts Β· Earlier revision b3f2a38d Β· right 39

Original diff
@@ -0,0 +1,83 @@
+import type { EnvironmentId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
+import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
+
+import { safeErrorLogAttributes } from "../errors/safeLog.ts";
+import { EnvironmentCacheStore } from "../platform/persistence.ts";
+
+export interface VcsRefsInvalidationTarget {
+  readonly environmentId: EnvironmentId;
+}
+
+export interface CachedVcsRefsInvalidationTarget extends VcsRefsInvalidationTarget {
+  readonly cwd: string;
+}
+
+export interface VcsRefsCacheState {
+  readonly revision: number;
+  readonly persistedCacheReadable: boolean;
+}
+
+const stateByEnvironment = Atom.family((environmentId: EnvironmentId) =>
+  Atom.make<VcsRefsCacheState>({
+    revision: 0,
+    persistedCacheReadable: true,
+  }).pipe(Atom.withLabel(`environment-data:vcs:list-refs-state:${environmentId}`)),
+);
+const persistenceLock = PartitionedSemaphore.makeUnsafe<EnvironmentId>({ permits: 1 });
+
+export function vcsRefsCacheStateAtom(target: VcsRefsInvalidationTarget) {
+  return stateByEnvironment(target.environmentId);
+}
+
+export function invalidateVcsRefs(
+  registry: AtomRegistry.AtomRegistry,
+  target: VcsRefsInvalidationTarget,
+  persistedCacheReadable?: boolean,
+): void {
+  registry.update(vcsRefsCacheStateAtom(target), (state) => ({
M
macroscopeappBotJul 28, 2026

🟑 Medium state/vcsRefInvalidation.ts:39

invalidateVcsRefs makes persistedCacheReadable sticky: once cache.clearVcsRefs fails and sets it to false, nothing in invalidateCachedVcsRefs or ordinary revision-gated refreshes ever restores it to true. After a one-time persistence deletion error, later atom instances keep skipping the offline cache β€” which may contain fresh data written by a subsequent successful refresh β€” until another VCS mutation happens to clear successfully. A single clearVcsRefs failure thus disables offline branch data indefinitely. The persistedCacheReadable: false state should be transient, scoped to the current cache generation, so a successful refresh clears it.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/client-runtime/src/state/vcsRefInvalidation.ts around line 39:
2
3`invalidateVcsRefs` makes `persistedCacheReadable` sticky: once `cache.clearVcsRefs` fails and sets it to `false`, nothing in `invalidateCachedVcsRefs` or ordinary revision-gated refreshes ever restores it to `true`. After a one-time persistence deletion error, later atom instances keep skipping the offline cache β€” which may contain fresh data written by a subsequent successful refresh β€” until another VCS mutation happens to clear successfully. A single `clearVcsRefs` failure thus disables offline branch data indefinitely. The `persistedCacheReadable: false` state should be transient, scoped to the current cache generation, so a successful refresh clears it.
J
Julius MarmingeJul 28, 2026

Fixed in 70b295c2b0. The fail-closed state is retained across UI unmounts, and an unfiltered live refresh now retries the environment-wide clear under the shared persistence lock. Only after that cleanup succeeds does it save the fresh snapshot and re-enable persisted reads; failed retries remain fail-closed. The regression test covers clear failure, unmounted/offline suppression, successful live recovery, and subsequent offline rehydration of the fresh snapshot.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 70b295c2 Β· right 291

Original diff
@@ -181,6 +288,7 @@ export function createVcsEnvironmentAtoms<R, E>(
       tag: WS_METHODS.vcsPull,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSettled: invalidateRefs,
C
cursorBotJul 28, 2026

Refresh status skips ref invalidation

Medium Severity

refreshStatus is not wired to invalidateCachedVcsRefs, unlike other VCS mutations in this change. After a status refresh updates remote refs on the server, the client still serves the old persisted branch snapshot and does not run another live listRefs until the connection generation changes, so branch UI can stay wrong for a long-lived session.

Reviewed by Cursor Bugbot for commit 70b295c2b06f73a10c9e32805da4616898a679f7. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in f948f0a634. refreshStatus now uses the same settlement invalidation as every other ref-affecting command, covering server-side remote refresh even when the operation fails after partial Git changes. The command-level regression test now verifies both failed pull and successful status refresh clear persistence and advance the ref generation.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 2a9d07ea Β· right 185

Original diff
@@ -160,6 +171,18 @@ export function createVcsEnvironmentAtoms<R, E>(
     readonly environmentId: EnvironmentId;
     readonly input: VcsListRefsInput;
   }) => listRefsByEnvironment(target.environmentId)(JSON.stringify(target.input));
+  const invalidateRefs = (
+    target: {
+      readonly environmentId: EnvironmentId;
+      readonly input: { readonly cwd: string };
+    },
+    registry: AtomRegistry.AtomRegistry,
+  ) => {
+    refsInvalidation.invalidate(registry, {
+      environmentId: target.environmentId,
+      cwd: target.input.cwd,
+    });
+  };
C
cursorBotJul 28, 2026

Ref invalidation misses list cwd

Medium Severity

invalidateRefs bumps a revision keyed only by the mutation RPC’s input.cwd, while listRefs atoms depend on the same key using each query’s input.cwd. Linked worktrees often use different paths for mutations versus branch lists, so successful pulls and checkouts can leave active ref streams stale until idle TTL, reconnect, or a manual refresh.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 2a9d07ea73a787280ddc661f26e832c867ca58cd. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in dfc1459eec: mutation invalidation is now environment-scoped, so every mounted ref stream in that environment revalidates once regardless of which linked-worktree cwd issued the command. Other environments remain isolated, covered by the focused regression.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 45ac080c Β· left 124

Original diff
@@ -107,31 +106,19 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange
     Stream.switchMap((generation) =>
       generation === null
         ? Stream.empty
-        : Stream.tick(VCS_REFS_REVALIDATE_INTERVAL).pipe(
-            Stream.mapEffect(
-              () =>
-                refresh().pipe(
-                  Effect.map(Option.some),
-                  Effect.catch((error) =>
-                    Effect.logWarning("Could not refresh Git refs.").pipe(
-                      Effect.annotateLogs({
-                        environmentId,
-                        cwd: input.cwd,
-                        ...safeErrorLogAttributes(error),
-                      }),
-                      Effect.as(Option.none<VcsListRefsResult>()),
-                    ),
-                  ),
C
cursorBotJul 28, 2026

Pull leaves ref lists stale

Medium Severity

Replacing the five-second ref polling with one live vcsListRefs per connection generation means branch-list atoms stop updating unless something explicitly refreshes them. Server-side pull still invalidates ref snapshots, but pull success handlers only refresh git status, so branch pickers can show outdated refs (including remotes) until reconnect or a manual branch refresh.

Reviewed by Cursor Bugbot for commit 45ac080cc4eef3a08b13bc6b7f079116974dc2cf. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 2a9d07ea73: successful ref-mutating commands now bump a repository-scoped invalidation atom. Every mounted ref query for that environment/cwd depends on that revision, so pull/create/switch/worktree/init refresh once without restoring polling, while the persisted result keeps selectors populated during revalidation. Added focused invalidation coverage.

packages/client-runtime/src/state/vcsRefInvalidation.ts Β· Earlier revision 4ba5ea3a Β· right 53–55

Original diff
@@ -0,0 +1,68 @@
+import type { EnvironmentId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
+import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
+
+import { safeErrorLogAttributes } from "../errors/safeLog.ts";
+import { EnvironmentCacheStore } from "../platform/persistence.ts";
+
+export interface VcsRefsInvalidationTarget {
+  readonly environmentId: EnvironmentId;
+}
+
+export interface CachedVcsRefsInvalidationTarget extends VcsRefsInvalidationTarget {
+  readonly cwd: string;
+}
+
+const revisionByEnvironment = Atom.family((environmentId: EnvironmentId) =>
+  Atom.make(0).pipe(Atom.withLabel(`environment-data:vcs:list-refs-revision:${environmentId}`)),
+);
+const persistenceLock = PartitionedSemaphore.makeUnsafe<EnvironmentId>({ permits: 1 });
+
+export function vcsRefsRevisionAtom(target: VcsRefsInvalidationTarget) {
+  return revisionByEnvironment(target.environmentId);
+}
+
+export function invalidateVcsRefs(
+  registry: AtomRegistry.AtomRegistry,
+  target: VcsRefsInvalidationTarget,
+): void {
+  registry.update(vcsRefsRevisionAtom(target), (revision) => revision + 1);
+}
+
+export function withVcsRefsPersistenceLock<A, E, R>(
+  environmentId: EnvironmentId,
+  effect: Effect.Effect<A, E, R>,
+): Effect.Effect<A, E, R> {
+  return persistenceLock.withPermit(environmentId)(effect);
+}
+
+/**
+ * Restarts live ref streams and clears persisted snapshots while holding the
+ * same lock used by generation-checked refresh writes. A refresh that wins the
+ * lock is cleared afterward; one that loses observes the new revision and
+ * cannot repersist its pre-mutation result.
+ */
+export const invalidateCachedVcsRefs = Effect.fn("VcsRefsState.invalidateCached")(function* (
+  registry: AtomRegistry.AtomRegistry,
+  target: CachedVcsRefsInvalidationTarget,
+) {
+  const cache = yield* EnvironmentCacheStore;
+  yield* withVcsRefsPersistenceLock(
+    target.environmentId,
+    Effect.gen(function* () {
+      invalidateVcsRefs(registry, target);
+      yield* cache.clearVcsRefs(target.environmentId).pipe(

packages/client-runtime/src/state/vcsRefInvalidation.ts Β· right 53–55

M
macroscopeappBotJul 28, 2026

🟑 Medium state/vcsRefInvalidation.ts:53

invalidateCachedVcsRefs bumps the revision atom before cache.clearVcsRefs finishes, so active ref streams restart synchronously, call loadVcsRefs outside the lock, and emit the stale persisted snapshot before it is deleted. After a successful Git mutation, users can temporarily be shown the pre-mutation branch list. Swap the order so the cache is cleared before the revision is published β€” the surrounding persistence lock already prevents stale refresh writes.

suggestion
1 yield* cache.clearVcsRefs(target.environmentId).pipe(
2 Effect.catch((error) =>
3 Effect.logWarning("Could not remove invalidated cached Git refs.").pipe(
4 Effect.annotateLogs({
5 environmentId: target.environmentId,
6 cwd: target.cwd,
7 ...safeErrorLogAttributes(error),
8 }),
9 ),
10 ),
11 );
12 invalidateVcsRefs(registry, target);
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/client-runtime/src/state/vcsRefInvalidation.ts around lines 53-55:
2
3`invalidateCachedVcsRefs` bumps the revision atom before `cache.clearVcsRefs` finishes, so active ref streams restart synchronously, call `loadVcsRefs` outside the lock, and emit the stale persisted snapshot before it is deleted. After a successful Git mutation, users can temporarily be shown the pre-mutation branch list. Swap the order so the cache is cleared before the revision is published β€” the surrounding persistence lock already prevents stale refresh writes.
J
Julius MarmingeJul 28, 2026

Fixed in 25c0c7abc8. Invalidation now clears the persisted environment-wide ref namespace while holding the shared persistence lock, and only then publishes the new revision that restarts active streams. The regression test records the revision observed by and verifies it remains at the old generation until clearing completes.

J
Julius MarmingeJul 28, 2026

Correction: fixed in 25c0c7abc8. Invalidation now clears the persisted environment-wide ref namespace while holding the shared persistence lock, and only then publishes the new revision that restarts active streams. The regression test records the revision observed by clearVcsRefs and verifies it remains at the old generation until clearing completes.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision f2449b51 Β· right 2348

Original diff
@@ -1987,257 +2129,233 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
       Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
     );
 
-  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
-    function* (input) {
-      const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
-        Effect.orElseSucceed(() => new Map<string, number>()),
-      );
-      const localBranchResult = yield* executeGitWithStableDiagnostics(
-        "GitVcsDriver.listRefs.branchNoColor",
-        input.cwd,
-        ["branch", "--no-color", "--no-column"],
-        {
-          timeoutMs: 10_000,
+  const readGitRefsSnapshot = Effect.fn("readGitRefsSnapshot")(function* (gitCommonDir: string) {
+    const fetchCwd =
+      path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
+    const gitDirArgs = ["--git-dir", gitCommonDir] as const;
+    const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(
+      [
+        executeGitWithStableDiagnostics(
+          "GitVcsDriver.listRefs.snapshotRefs",
+          fetchCwd,
+          [
+            ...gitDirArgs,
+            "for-each-ref",
+            "--format=%(refname)%09%(committerdate:unix)%09%(symref)",
+            "refs/heads",
+            "refs/remotes",
+          ],
+          {
+            timeoutMs: 30_000,
+            maxOutputBytes: 16 * 1024 * 1024,
+            fallbackErrorDetail: "Git ref snapshot enumeration failed.",
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.defaultRef",
+          fetchCwd,
+          [...gitDirArgs, "symbolic-ref", "refs/remotes/origin/HEAD"],
+          {
+            timeoutMs: 5_000,
+            allowNonZeroExit: true,
+          },
+        ),
+        executeGit(
+          "GitVcsDriver.listRefs.worktreeList",
+          fetchCwd,
+          [...gitDirArgs, "worktree", "list", "--porcelain"],
+          {
+            timeoutMs: 30_000,
+            allowNonZeroExit: true,
+            maxOutputBytes: 16 * 1024 * 1024,
+          },
+        ),
+        executeGit("GitVcsDriver.listRefs.remoteNames", fetchCwd, [...gitDirArgs, "remote"], {
+          timeoutMs: 5_000,
           allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            isMissingGitCwdError(error)
-              ? Effect.succeed({
-                  exitCode: ChildProcessSpawner.ExitCode(128),
-                  stdout: "",
-                  stderr: "fatal: not a git repository",
-                  stdoutTruncated: false,
-                  stderrTruncated: false,
-                })
-              : Effect.fail(error),
         }),
-      );
+      ],
+      { concurrency: 2 },
+    );
 
-      if (localBranchResult.exitCode !== 0) {
-        const stderr = localBranchResult.stderr.trim();
-        if (isNonRepositoryGitStderr(stderr)) {
-          return {
-            refs: [],
-            isRepo: false,
-            hasPrimaryRemote: false,
-            nextCursor: null,
-            totalCount: 0,
-          };
-        }
-        return yield* new GitCommandError({
-          ...gitCommandContext({
-            operation: "GitVcsDriver.listRefs",
-            cwd: input.cwd,
-            args: ["branch", "--no-color", "--no-column"],
-          }),
-          detail: "Git branch listing failed.",
-          exitCode: localBranchResult.exitCode,
-          stdoutLength: localBranchResult.stdout.length,
-          stderrLength: localBranchResult.stderr.length,
+    const remoteNames =
+      remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
+    if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
+      yield* Effect.logWarning(
+        `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+      );
+    }
+    const defaultBranch =
+      defaultRefResult.exitCode === 0
+        ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
+        : null;
+    const worktreeMap =
+      worktreeListResult.exitCode === 0
+        ? new Map(
+            [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(
+              ([branchName, worktreePath]) =>
+                [branchName, path.normalize(path.resolve(worktreePath))] as const,
+            ),
+          )
+        : new Map<string, string>();
+    const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+    const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];
+
+    for (const line of refsResult.stdout.split("\n")) {
+      if (line.length === 0) continue;
+      const [fullRefName, lastCommitRaw, symbolicTarget] = line.split("\t");
+      if (!fullRefName || symbolicTarget) continue;
+      const parsedLastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
+      const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;
+
+      if (fullRefName.startsWith("refs/heads/")) {
+        const name = fullRefName.slice("refs/heads/".length);
+        localBranches.push({
+          ref: {
+            name,
+            current: false,
+            isRemote: false,
+            isDefault: name === defaultBranch,
+            worktreePath: worktreeMap.get(name) ?? null,
+          },
+          lastCommit,
         });
+        continue;
       }
+      if (!fullRefName.startsWith("refs/remotes/")) continue;
+
+      const name = fullRefName.slice("refs/remotes/".length);
+      const parsedRemoteRef = parseRemoteRefWithRemoteNames(name, remoteNames);
+      const remoteBranch: VcsRef = {
+        name,
+        current: false,
+        isRemote: true,
+        isDefault:
+          defaultBranch !== null &&
+          parsedRemoteRef?.remoteName === "origin" &&
+          parsedRemoteRef.branchName === defaultBranch,
+        worktreePath: null,
+        ...(parsedRemoteRef ? { remoteName: parsedRemoteRef.remoteName } : {}),
+      };
+      remoteBranches.push({ ref: remoteBranch, lastCommit });
+    }
 
-      const remoteBranchResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteBranches",
-        input.cwd,
-        ["branch", "--no-color", "--no-column", "--remotes"],
-        {
-          timeoutMs: 10_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote ref lookup failed; falling back to an empty remote ref list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
-
-      const remoteNamesResultEffect = executeGit(
-        "GitVcsDriver.listRefs.remoteNames",
-        input.cwd,
-        ["remote"],
-        {
-          timeoutMs: 5_000,
-          allowNonZeroExit: true,
-        },
-      ).pipe(
-        Effect.catchTags({
-          GitCommandError: (error) =>
-            Effect.logWarning(
-              "Git remote name lookup failed; falling back to an empty remote name list.",
-              {
-                operation: error.operation,
-                command: error.command,
-                cwd: error.cwd,
-                detail: error.detail,
-                cause: error,
-              },
-            ).pipe(
-              Effect.as({
-                exitCode: ChildProcessSpawner.ExitCode(1),
-                stdout: "",
-                stderr: "",
-                stdoutTruncated: false,
-                stderrTruncated: false,
-              } satisfies GitVcsDriver.ExecuteGitResult),
-            ),
-        }),
-      );
+    const byRecencyThenName = (
+      left: { readonly ref: VcsRef; readonly lastCommit: number },
+      right: { readonly ref: VcsRef; readonly lastCommit: number },
+    ) =>
+      left.lastCommit !== right.lastCommit
+        ? right.lastCommit - left.lastCommit
+        : left.ref.name.localeCompare(right.ref.name);
 
-      const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
-        yield* Effect.all(
-          [
-            executeGit(
-              "GitVcsDriver.listRefs.defaultRef",
-              input.cwd,
-              ["symbolic-ref", "refs/remotes/origin/HEAD"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            executeGit(
-              "GitVcsDriver.listRefs.worktreeList",
-              input.cwd,
-              ["worktree", "list", "--porcelain"],
-              {
-                timeoutMs: 5_000,
-                allowNonZeroExit: true,
-              },
-            ),
-            remoteBranchResultEffect,
-            remoteNamesResultEffect,
-            branchRecencyPromise,
-          ],
-          { concurrency: "unbounded" },
-        );
+    return {
+      localBranches: localBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      remoteBranches: remoteBranches.toSorted(byRecencyThenName).map(({ ref }) => ref),
+      hasPrimaryRemote: remoteNames.includes("origin"),
+    } satisfies GitRefsSnapshot;
+  });
 
-      const remoteNames =
-        remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
-      if (remoteBranchResult.exitCode !== 0 && remoteBranchResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote refName lookup returned code ${remoteBranchResult.exitCode} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote refName list.`,
-        );
+  const listRefsEpochByCommonDir = new Map<string, number>();
+  let listRefsEpochSequence = 0;
+  const bumpListRefsEpoch = (gitCommonDir: string): number => {
+    const nextEpoch = ++listRefsEpochSequence;
+    listRefsEpochByCommonDir.delete(gitCommonDir);
+    listRefsEpochByCommonDir.set(gitCommonDir, nextEpoch);
+    if (listRefsEpochByCommonDir.size > LIST_REFS_SNAPSHOT_CACHE_CAPACITY) {
+      const oldestKey = listRefsEpochByCommonDir.keys().next().value;
+      if (oldestKey !== undefined) {
+        listRefsEpochByCommonDir.delete(oldestKey);
       }
-      if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {
-        yield* Effect.logWarning(
-          `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
+    }
+    return nextEpoch;
+  };
+  const listRefsSnapshotCache = yield* Cache.makeWith(
+    (cacheKey: GitRefsSnapshotCacheKey) => readGitRefsSnapshot(cacheKey.gitCommonDir),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_REFS_SNAPSHOT_CACHE_TTL : Duration.zero),
+    },
+  );
+  const listRefsRefreshSnapshotCache = yield* Cache.makeWith(
+    (gitCommonDir: string) =>
+      Effect.suspend(() => {
+        const epoch = bumpListRefsEpoch(gitCommonDir);
+        return Cache.get(
+          listRefsSnapshotCache,
+          new GitRefsSnapshotCacheKey({ gitCommonDir, epoch }),
         );
-      }
+      }),
+    {
+      capacity: LIST_REFS_SNAPSHOT_CACHE_CAPACITY,
+      timeToLive: (exit) =>
+        Exit.isSuccess(exit) ? LIST_REFS_REFRESH_COALESCE_TTL : LIST_REFS_REFRESH_FAILURE_COOLDOWN,
+    },
+  );
+  const resolveListRefsSnapshot = (gitCommonDir: string, refresh: boolean) => {
+    const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);
+    if (refresh || currentEpoch === undefined) {
+      // The refresh cache owns the complete snapshot read, rather than only the
+      // epoch bump. Slow repositories therefore remain singleflight for the
+      // entire Git scan even when more refresh requests arrive after the
+      // coalescing TTL would otherwise have elapsed.
+      return Cache.get(listRefsRefreshSnapshotCache, gitCommonDir);
+    }
+    return Cache.get(
+      listRefsSnapshotCache,
+      new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),
+    );
+  };
+  const invalidateListRefsSnapshot = Effect.fn("invalidateListRefsSnapshot")(function* (
+    cwd: string,
+  ) {
+    const repositoryPathsCacheKey = normalizeRepositoryPathsCacheKey(cwd);
+    const repositoryPaths = yield* Cache.get(repositoryPathsCache, repositoryPathsCacheKey);
+    if (repositoryPaths === null) return;
+    yield* Cache.invalidate(listRefsRefreshSnapshotCache, repositoryPaths.gitCommonDir);
+    bumpListRefsEpoch(repositoryPaths.gitCommonDir);
+    yield* Cache.invalidate(repositoryPathsCache, repositoryPathsCacheKey);
+  });
 
-      const defaultBranch =
-        defaultRef.exitCode === 0
-          ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
-          : null;
-
-      const worktreeMap = new Map<string, string>();
-      if (worktreeList.exitCode === 0) {
-        let currentPath: string | null = null;
-        for (const line of worktreeList.stdout.split("\n")) {
-          if (line.startsWith("worktree ")) {
-            const candidatePath = line.slice("worktree ".length);
-            const exists = yield* fileSystem.stat(candidatePath).pipe(
-              Effect.map(() => true),
-              Effect.orElseSucceed(() => false),
-            );
-            currentPath = exists ? candidatePath : null;
-          } else if (line.startsWith("branch refs/heads/") && currentPath) {
-            worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
-          } else if (line === "") {
-            currentPath = null;
-          }
-        }
+  const listRefs: GitVcsDriver.GitVcsDriver["Service"]["listRefs"] = Effect.fn("listRefs")(
+    function* (input) {
+      const repositoryPaths = yield* resolveRepositoryPaths(input.cwd).pipe(
+        Effect.catchTags({
+          GitCommandError: (error) =>
+            isMissingGitCwdError(error) ? Effect.succeed(null) : Effect.fail(error),
+        }),
+      );
+      if (repositoryPaths === null) {
+        return {
+          refs: [],
+          isRepo: false,
+          hasPrimaryRemote: false,
+          nextCursor: null,
+          totalCount: 0,
+        };
       }
 
-      const localBranches = Arr.filterMap(localBranchResult.stdout.split("\n"), (line) => {
-        const refName = parseBranchLine(line);
-        return refName === null
-          ? Result.failVoid
-          : Result.succeed({
-              name: refName.name,
-              current: refName.current,
-              isRemote: false,
-              isDefault: refName.name === defaultBranch,
-              worktreePath: worktreeMap.get(refName.name) ?? null,
-            });
-      }).toSorted((a, b) => {
-        const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
-        const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
-        if (aPriority !== bPriority) return aPriority - bPriority;
-
-        const aLastCommit = branchLastCommit.get(a.name) ?? 0;
-        const bLastCommit = branchLastCommit.get(b.name) ?? 0;
-        if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
-        return a.name.localeCompare(b.name);
-      });
-
-      const remoteBranches =
-        remoteBranchResult.exitCode === 0
-          ? Arr.filterMap(remoteBranchResult.stdout.split("\n"), (line) => {
-              const refName = parseBranchLine(line);
-              if (refName === null) {
-                return Result.failVoid;
-              }
-              const parsedRemoteRef = parseRemoteRefWithRemoteNames(refName.name, remoteNames);
-              const remoteBranch: {
-                name: string;
-                current: boolean;
-                isRemote: boolean;
-                remoteName?: string;
-                isDefault: boolean;
-                worktreePath: string | null;
-              } = {
-                name: refName.name,
-                current: false,
-                isRemote: true,
-                // origin/HEAD's target is the repo default even when no local
-                // copy of the default branch exists.
-                isDefault:
-                  defaultBranch !== null &&
-                  parsedRemoteRef?.remoteName === "origin" &&
-                  parsedRemoteRef.branchName === defaultBranch,
-                worktreePath: null,
-              };
-              if (parsedRemoteRef) {
-                remoteBranch.remoteName = parsedRemoteRef.remoteName;
-              }
-              return Result.succeed(remoteBranch);
-            }).toSorted((a, b) => {
-              const aLastCommit = branchLastCommit.get(a.name) ?? 0;
-              const bLastCommit = branchLastCommit.get(b.name) ?? 0;
-              if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
-              return a.name.localeCompare(b.name);
-            })
-          : [];
-
+      const snapshot = yield* resolveListRefsSnapshot(
+        repositoryPaths.gitCommonDir,
+        input.refresh === true,
+      );
+      const hasCurrentWorktreeBranch =
+        repositoryPaths.worktreeRoot !== null &&
+        snapshot.localBranches.some((ref) => ref.worktreePath === repositoryPaths.worktreeRoot);
+      const localBranches = snapshot.localBranches.map((ref) => ({
+        ...ref,
+        current: hasCurrentWorktreeBranch
+          ? ref.worktreePath === repositoryPaths.worktreeRoot
+          : ref.name === repositoryPaths.currentBranch,
C
cursorBotJul 28, 2026

Stale cached current branch

Medium Severity

listRefs rebuilds the shared ref snapshot when refresh is true, but it still marks current using repositoryPathsCache (currentBranch / worktreeRoot) with a ten-minute TTL. That cache is only cleared in invalidateListRefsSnapshot for the mutation’s cwd, so a fresh branch list can show the wrong current ref after checkout or rename until the cache expires.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit f2449b5152a05b3fa0fd90b129935cd608381f9b. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 6caf60955e. Explicit ref refreshes now also refresh repository identity through a five-second singleflight cache, keeping current-branch data fresh without multiplying Git subprocesses during concurrent refreshes. The regression test performs a raw external checkout and verifies the refreshed list moves the current marker.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 4f3a6609 Β· right 154

Original diff
@@ -160,6 +151,14 @@ export function createVcsEnvironmentAtoms<R, E>(
     readonly environmentId: EnvironmentId;
     readonly input: VcsListRefsInput;
   }) => listRefsByEnvironment(target.environmentId)(JSON.stringify(target.input));
+  const invalidateRefs = (
M
macroscopeappBotJul 28, 2026

🟑 Medium state/vcs.ts:154

invalidateRefs only deletes the persisted cache entry for the cwd of the worktree that performed the mutation, even though Git refs are shared across all linked worktrees. When a command mutates refs in worktree A while worktree B's atom is idle, B's persisted entry survives; remounting B later replays the stale branch list before its live refresh completes. The revision bump restarts only currently retained atoms but does not remove persisted entries stored under other worktree paths. Invalidation should remove all cache keys for the repository, or persistence should key them by a shared repository identity rather than per-cwd.

Also found in 2 other location(s)

packages/client-runtime/src/state/runtime.ts:651

The new onSuccess hook invalidates only the cache entry for target.input.cwd. Git ref mutations affect every worktree sharing the repository, while persisted vcs-refs entries are keyed by each worktree's cwd; an idle/unmounted sibling worktree therefore retains its stale record and can replay obsolete refs when remounted. The revision bump refreshes mounted atoms, but does not delete those other persisted entries.

packages/client-runtime/src/state/vcsRefInvalidation.ts:41

invalidateCachedVcsRefs removes only the persisted entry for target.cwd, although ref mutations (create/switch/push/stacked actions) can change repository-wide refs visible from every linked worktree. The environment-wide revision restarts all streams, but caches stored under other worktree paths remain and can later be replayed while offline, showing branches that predate the mutation. Invalidation needs to remove all cache keys for the repository (or persistence must key them by a shared repository identity).

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/client-runtime/src/state/vcs.ts around line 154:
2
3`invalidateRefs` only deletes the persisted cache entry for the `cwd` of the worktree that performed the mutation, even though Git refs are shared across all linked worktrees. When a command mutates refs in worktree A while worktree B's atom is idle, B's persisted entry survives; remounting B later replays the stale branch list before its live refresh completes. The revision bump restarts only currently retained atoms but does not remove persisted entries stored under other worktree paths. Invalidation should remove all cache keys for the repository, or persistence should key them by a shared repository identity rather than per-`cwd`.
4
5Also found in 2 other location(s):
6- packages/client-runtime/src/state/runtime.ts:651 -- The new `onSuccess` hook invalidates only the cache entry for `target.input.cwd`. Git ref mutations affect every worktree sharing the repository, while persisted `vcs-refs` entries are keyed by each worktree's `cwd`; an idle/unmounted sibling worktree therefore retains its stale record and can replay obsolete refs when remounted. The revision bump refreshes mounted atoms, but does not delete those other persisted entries.
7- packages/client-runtime/src/state/vcsRefInvalidation.ts:41 -- `invalidateCachedVcsRefs` removes only the persisted entry for `target.cwd`, although ref mutations (create/switch/push/stacked actions) can change repository-wide refs visible from every linked worktree. The environment-wide revision restarts all streams, but caches stored under other worktree paths remain and can later be replayed while offline, showing branches that predate the mutation. Invalidation needs to remove all cache keys for the repository (or persistence must key them by a shared repository identity).
J
Julius MarmingeJul 28, 2026

Fixed in ff4df7fe63. Ref invalidation now clears the entire persisted VCS-ref cache namespace for the environment, so linked worktrees cannot replay stale entries, while unrelated thread/shell/settings caches are preserved. Added mobile persistence coverage for two worktree keys plus another environment; focused tests and web/mobile/client-runtime typechecks pass.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision dfc1459e Β· right 243

Original diff
@@ -193,30 +212,35 @@ export function createVcsEnvironmentAtoms<R, E>(
       tag: WS_METHODS.vcsCreateWorktree,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     removeWorktree: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:remove-worktree",
       tag: WS_METHODS.vcsRemoveWorktree,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     createRef: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:create-ref",
       tag: WS_METHODS.vcsCreateRef,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     switchRef: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:switch-ref",
       tag: WS_METHODS.vcsSwitchRef,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
     }),
     init: createEnvironmentRpcCommand(runtime, {
       label: "environment-data:vcs:init",
       tag: WS_METHODS.vcsInit,
       scheduler: vcsCommandScheduler,
       concurrency: vcsCommandConcurrency,
+      onSuccess: invalidateRefs,
C
cursorBotJul 28, 2026

Stacked commits skip ref invalidation

High Severity

The new mutation-driven invalidation for VCS listRefs atoms is incomplete. gitRunStackedAction and publishRepository don't trigger invalidateRefs on success, leading to stale branch lists and remote status. Additionally, listRefs atoms with filtered inputs may not refresh repository identity or snapshots from the server on reconnect, causing current-branch metadata to lag.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit dfc1459eec125c057291b04910c3a44cbdf9b6ed. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 561c035782. Ref invalidation now uses one module-scoped environment revision shared by the VCS, stacked-action, and source-control atom factories. Successful gitRunStackedAction and publishRepository commands both increment it, with direct command-level regression tests. Connected/reconnected list requests also force refresh: true, including filtered requests, so repository identity and snapshots are refreshed before applying query/pagination filters.

apps/server/src/vcs/GitVcsDriverCore.ts Β· Earlier revision 501a7381 Β· right 1109

Original diff
@@ -944,28 +990,138 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
     ).pipe(Effect.asVoid);
   };
 
+  const resolveRepositoryPathsUncached = Effect.fn("resolveRepositoryPathsUncached")(function* (
+    cwd: string,
+  ) {
+    const commonDirResult = yield* executeGitWithStableDiagnostics(
+      "GitVcsDriver.resolveRepositoryPaths.commonDir",
+      cwd,
+      ["rev-parse", "--git-common-dir"],
+      {
+        timeoutMs: 5_000,
+        allowNonZeroExit: true,
+      },
+    );
+    if (commonDirResult.exitCode !== 0) {
+      const stderr = commonDirResult.stderr.trim();
+      if (isNonRepositoryGitStderr(stderr)) {
+        return null;
+      }
+      return yield* new GitCommandError({
+        ...gitCommandContext({
+          operation: "GitVcsDriver.resolveRepositoryPaths.commonDir",
+          cwd,
+          args: ["rev-parse", "--git-common-dir"],
+        }),
+        detail: "Failed to resolve the Git common directory.",
+        exitCode: commonDirResult.exitCode,
+        stdoutLength: commonDirResult.stdout.length,
+        stderrLength: commonDirResult.stderr.length,
+      });
+    }
+
+    const commonDirOutput = commonDirResult.stdout.trim();
+    const resolvedGitCommonDir = path.isAbsolute(commonDirOutput)
+      ? path.normalize(commonDirOutput)
+      : path.resolve(cwd, commonDirOutput);
+    const gitCommonDir = yield* fileSystem
+      .realPath(resolvedGitCommonDir)
+      .pipe(Effect.orElseSucceed(() => resolvedGitCommonDir));
+    const worktreeRootResult = yield* executeGit(
+      "GitVcsDriver.resolveRepositoryPaths.worktreeRoot",
+      cwd,
+      ["rev-parse", "--show-toplevel"],
+      {
+        timeoutMs: 5_000,
+        allowNonZeroExit: true,
+      },
+    );
+    const worktreeRootOutput = worktreeRootResult.stdout.trim();
+    const worktreeRoot =
+      worktreeRootResult.exitCode === 0 && worktreeRootOutput.length > 0
+        ? path.normalize(
+            path.isAbsolute(worktreeRootOutput)
+              ? worktreeRootOutput
+              : path.resolve(cwd, worktreeRootOutput),
+          )
+        : null;
+
+    return {
+      gitCommonDir,
+      worktreeRoot,
+    } satisfies GitRepositoryPaths;
+  });
+
+  const repositoryPathsCache = yield* Cache.makeWith(
+    (cwd: string) => resolveRepositoryPathsUncached(cwd),
+    {
+      capacity: REPOSITORY_PATHS_CACHE_CAPACITY,
+      timeToLive: Exit.match({
+        onSuccess: (repositoryPaths) =>
+          repositoryPaths === null ? NON_REPOSITORY_PATHS_CACHE_TTL : REPOSITORY_PATHS_CACHE_TTL,
+        onFailure: () => Duration.zero,
+      }),
+    },
+  );
+  const normalizeRepositoryPathsCacheKey = (cwd: string) => path.normalize(path.resolve(cwd));
+  const resolveRepositoryPaths = (cwd: string) =>
+    Cache.get(repositoryPathsCache, normalizeRepositoryPathsCacheKey(cwd));
+
   const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {
-    const gitCommonDir = yield* runGitStdout("GitVcsDriver.resolveGitCommonDir", cwd, [
-      "rev-parse",
-      "--git-common-dir",
-    ]).pipe(Effect.map((stdout) => stdout.trim()));
-    return path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(cwd, gitCommonDir);
+    const repositoryPaths = yield* resolveRepositoryPaths(cwd);
+    if (repositoryPaths !== null) {
+      return repositoryPaths.gitCommonDir;
+    }
+    return yield* new GitCommandError({
+      ...gitCommandContext({
+        operation: "GitVcsDriver.resolveGitCommonDir",
+        cwd,
+        args: ["rev-parse", "--git-common-dir"],
+      }),
+      detail: "Cannot resolve a Git common directory outside a repository.",
+    });
   });
 
+  const statusRemoteRefreshFailureCounts = new Map<string, number>();
+  const statusRemoteRefreshFailureKey = (cacheKey: StatusRemoteRefreshCacheKey) =>
+    `${cacheKey.gitCommonDir}\0${cacheKey.remoteName}`;
+  const recordStatusRemoteRefreshFailure = (cacheKey: StatusRemoteRefreshCacheKey) => {
+    const key = statusRemoteRefreshFailureKey(cacheKey);
+    const nextCount = (statusRemoteRefreshFailureCounts.get(key) ?? 0) + 1;
+    statusRemoteRefreshFailureCounts.delete(key);
+    statusRemoteRefreshFailureCounts.set(key, nextCount);
+    if (statusRemoteRefreshFailureCounts.size > STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY) {
+      const oldestKey = statusRemoteRefreshFailureCounts.keys().next().value;
+      if (oldestKey !== undefined) {
+        statusRemoteRefreshFailureCounts.delete(oldestKey);
+      }
+    }
+  };
+  const clearStatusRemoteRefreshFailures = (cacheKey: StatusRemoteRefreshCacheKey) => {
+    statusRemoteRefreshFailureCounts.delete(statusRemoteRefreshFailureKey(cacheKey));
+  };
   const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* (
     cacheKey: StatusRemoteRefreshCacheKey,
   ) {
-    yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName);
-    return true as const;
+    return yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName).pipe(
+      Effect.tap(() => Effect.sync(() => clearStatusRemoteRefreshFailures(cacheKey))),
+      Effect.tapError(() => Effect.sync(() => recordStatusRemoteRefreshFailure(cacheKey))),
+      Effect.as(true as const),
C
cursorBotJul 28, 2026

Backoff cleared on failed fetch

Medium Severity

refreshStatusRemoteCacheEntry clears the consecutive failure counter whenever fetchRemoteForStatus completes as a successful Effect, but that helper uses allowNonZeroExit, so a git fetch that exits with an error still runs clearStatusRemoteRefreshFailures. That resets exponential backoff after earlier spawn or timeout failures and stores a success TTL instead of backing off.

Reviewed by Cursor Bugbot for commit 501a7381787030d9adec0bd871f7966438e0e75d. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in f2449b5152. Background fetch no longer allows non-zero exits, so a failed git fetch is retained as a cache failure and advances the exponential cooldown instead of clearing it. The backoff test now exercises repeated non-zero child exits at 30s and 60s boundaries.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 501a7381 Β· left 92

Original diff
@@ -82,15 +82,7 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange
     return refs;
   });
 
-  const cachedRefs = Stream.fromEffect(
-    SubscriptionRef.get(supervisor.state).pipe(
-      Effect.flatMap((connection) =>
-        connection.phase === "connected"
-          ? Effect.succeed(Option.none<VcsListRefsResult>())
-          : Effect.succeed(cached),
-      ),
-    ),
C
cursorBotJul 28, 2026

Stale refs after refresh failure

Medium Severity

The refs stream always emits persisted loadVcsRefs data first, including while connected. If the subsequent live vcsListRefs call fails, the stream emits nothing further, so branch-list atoms can keep showing outdated refs (wrong current branch or missing branches) until reconnect or cache expiry.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 501a7381787030d9adec0bd871f7966438e0e75d. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in f2449b5152. While connected, the client now retries a failed live ref refresh with capped exponential backoff instead of leaving persisted refs indefinitely. switchMap cancels the in-flight refresh and its retry timer on disconnect or generation change, and the server caches snapshot failures for 30s so retries cannot recreate a Git subprocess storm. Focused tests cover connected recovery and cancellation.

packages/client-runtime/src/state/vcs.ts Β· Earlier revision 561c0357 Β· right 93

Original diff
@@ -82,15 +90,7 @@ export const makeCachedVcsRefsChanges = Effect.fn("CachedVcsRefsState.makeChange
     return refs;
   });
 
-  const cachedRefs = Stream.fromEffect(
-    SubscriptionRef.get(supervisor.state).pipe(
-      Effect.flatMap((connection) =>
-        connection.phase === "connected"
-          ? Effect.succeed(Option.none<VcsListRefsResult>())
-          : Effect.succeed(cached),
-      ),
-    ),
-  ).pipe(
+  const cachedRefs = Stream.fromEffect(Effect.succeed(cached)).pipe(
C
cursorBotJul 28, 2026

Stale persisted refs after invalidation

Medium Severity

After a successful ref mutation bumps vcsRefsRevisionAtom, remounted ref streams still emit the on-disk loadVcsRefs snapshot before the live refresh completes. That snapshot was saved from the pre-mutation fetch, so branch UI can briefly revert to outdated refs right after create, switch, or pull succeeds.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 561c035782fce4bbd1901f8213f4cc6f8393d43b. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 4f3a66092b. Ref invalidation now removes the persisted (environmentId, cwd) snapshot before incrementing the revision that restarts the live stream, so a remount during the refresh window cannot replay stale branch data. The web and mobile cache adapters both implement the removal operation, with focused persistence and invalidation tests.