From 27d805780d7113c173db350c3c82dd9bc621dab2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 11:42:13 +0000 Subject: [PATCH 1/4] fix(server): git status refresh scans the working tree once unless a fetch ran refreshStatus read local status before and after the remote read, so every explicit refresh (window focus, tab switch) ran git status, untracked listing, numstat and the branch-total reads twice. The second read exists because the status fetch can move the base ref behind Changes totals, but most refreshes run no fetch (it is throttled to once per 15 s per remote). The git driver now counts finished status fetches, and refreshStatus reads local status again only when that count moved during its remote read. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/git/GitWorkflowService.ts | 2 + apps/server/src/vcs/GitVcsDriver.ts | 2 + apps/server/src/vcs/GitVcsDriverCore.test.ts | 3 ++ apps/server/src/vcs/GitVcsDriverCore.ts | 8 ++++ .../src/vcs/VcsStatusBroadcaster.test.ts | 41 ++++++++++++++++--- apps/server/src/vcs/VcsStatusBroadcaster.ts | 7 +++- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/apps/server/src/git/GitWorkflowService.ts b/apps/server/src/git/GitWorkflowService.ts index a614ebdfb5687..b5c258dda58c9 100644 --- a/apps/server/src/git/GitWorkflowService.ts +++ b/apps/server/src/git/GitWorkflowService.ts @@ -65,6 +65,7 @@ export class GitWorkflowService extends Context.Service< input: VcsStatusInput, options?: GitManager.GitRemoteStatusOptions, ) => Effect.Effect; + readonly statusFetchCount: Effect.Effect; readonly invalidateLocalStatus: (cwd: string) => Effect.Effect; readonly invalidateRemoteStatus: (cwd: string) => Effect.Effect; readonly invalidateStatus: (cwd: string) => Effect.Effect; @@ -388,6 +389,7 @@ export const make = Effect.gen(function* () { ), ); }), + statusFetchCount: git.statusFetchCount, invalidateLocalStatus: gitManager.invalidateLocalStatus, invalidateRemoteStatus: gitManager.invalidateRemoteStatus, invalidateStatus: gitManager.invalidateStatus, diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 14147854a2ddb..71313dcb5bd9d 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -333,6 +333,8 @@ export class GitVcsDriver extends Context.Service< cwd: string, options?: GitRemoteStatusOptions, ) => Effect.Effect; + /** Status fetches finished so far, in any repository. A change means remote refs may have moved. */ + readonly statusFetchCount: Effect.Effect; readonly prepareCommitContext: ( cwd: string, filePaths?: readonly string[], diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index b748d4f364c84..05b40f72d45ec 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -4642,13 +4642,16 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { yield* git(updater, ["push", "origin", initialBranch]); const driver = yield* GitVcsDriver.GitVcsDriver; + const fetchesBefore = yield* driver.statusFetchCount; const cachedStatus = yield* driver.statusDetailsRemote(cwd, { refreshUpstream: false, }); + assert.equal(yield* driver.statusFetchCount, fetchesBefore); const refreshedStatus = yield* driver.statusDetailsRemote(cwd); assert.equal(cachedStatus.behindCount, 0); assert.equal(refreshedStatus.behindCount, 1); + assert.equal(yield* driver.statusFetchCount, fetchesBefore + 1); }), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 212ccb3a0b449..7870b92c06513 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1671,11 +1671,18 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } } }); + let statusFetchCount = 0; const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* ( cacheKey: StatusRemoteRefreshCacheKey, ) { return yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName).pipe( Effect.as(true as const), + // A failed or interrupted fetch can still have updated some refs. + Effect.ensuring( + Effect.sync(() => { + statusFetchCount += 1; + }), + ), ); }); @@ -5623,6 +5630,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* statusDetails, statusDetailsLocal, statusDetailsRemote, + statusFetchCount: Effect.sync(() => statusFetchCount), prepareCommitContext, commit: Effect.fnUntraced(function* (cwd, subject, body, options) { return yield* withListRefsInvalidation(cwd, commit(cwd, subject, body, options)); diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts index 96e29039abbd2..b1b0773b2a334 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts @@ -91,6 +91,7 @@ function layerTestFor(state: { headPathCalls?: number; failLocalStatus?: boolean; onLocalStatusFailure?: Effect.Effect; + statusFetchCount?: number; }) { return VcsStatusBroadcaster.layer.pipe( Layer.provideMerge( @@ -159,6 +160,7 @@ function layerTestFor(state: { ), ); }), + statusFetchCount: Effect.sync(() => state.statusFetchCount ?? 0), invalidateLocalStatus: () => Effect.sync(() => { state.localInvalidationCalls += 1; @@ -745,6 +747,7 @@ describe("VcsStatusBroadcaster", () => { ), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: () => Effect.succeed(localStatus), currentBranch: () => Effect.succeed(localStatus.refName), remoteStatus: () => Effect.succeed(remoteStatus), @@ -841,6 +844,7 @@ describe("VcsStatusBroadcaster", () => { Layer.provide(layerBackgroundPolicy(() => true)), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: () => Effect.succeed(baseLocalStatus), currentBranch: () => Effect.succeed(baseLocalStatus.refName), remoteStatus: () => @@ -887,6 +891,7 @@ describe("VcsStatusBroadcaster", () => { Layer.provide(layerBackgroundPolicy(() => true)), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: () => Effect.succeed(baseLocalStatus), currentBranch: () => Effect.succeed(baseLocalStatus.refName), remoteStatus: () => @@ -936,6 +941,7 @@ describe("VcsStatusBroadcaster", () => { Layer.provide(layerBackgroundPolicy(() => true)), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: () => Effect.sync(() => { state.localStatusCalls += 1; @@ -1019,6 +1025,7 @@ describe("VcsStatusBroadcaster", () => { Layer.provide(layerBackgroundPolicy(() => true)), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: (input) => Effect.sync(() => { seenCwds.push(input.cwd); @@ -1107,7 +1114,7 @@ describe("VcsStatusBroadcaster", () => { }).pipe(Effect.provide(layerTestFor(state))); }); - it.effect("an explicit refresh reads local totals after the fetch", () => { + it.effect("an explicit refresh reads local totals again only after a fetch", () => { return Effect.gen(function* () { const fetchStarted = yield* Deferred.make(); const finishFetch = yield* Deferred.make(); @@ -1116,7 +1123,14 @@ describe("VcsStatusBroadcaster", () => { branchChanges: { baseRef: "origin/main", insertions: 745, deletions: 24 }, }; let cachedLocal: VcsStatusLocalResult | undefined; - const readLocalStatus = () => Effect.sync(() => (cachedLocal ??= local)); + let localStatusReads = 0; + let fetches = 0; + let fetchNext = true; + const readLocalStatus = () => + Effect.sync(() => { + localStatusReads += 1; + return (cachedLocal ??= local); + }); const layerTest = VcsStatusBroadcaster.layer.pipe( Layer.provideMerge(NodeServices.layer), Layer.provide(layerBackgroundPolicy(() => true)), @@ -1133,11 +1147,19 @@ describe("VcsStatusBroadcaster", () => { }), currentBranch: () => Effect.succeed(baseLocalStatus.refName), invalidateRemoteStatus: () => Effect.void, + statusFetchCount: Effect.sync(() => fetches), remoteStatus: () => - Deferred.succeed(fetchStarted, undefined).pipe( - Effect.andThen(Deferred.await(finishFetch)), - Effect.as(baseRemoteStatus), - ), + fetchNext + ? Deferred.succeed(fetchStarted, undefined).pipe( + Effect.andThen(Deferred.await(finishFetch)), + Effect.andThen( + Effect.sync(() => { + fetches += 1; + }), + ), + Effect.as(baseRemoteStatus), + ) + : Effect.succeed(baseRemoteStatus), }), ), ); @@ -1154,6 +1176,12 @@ describe("VcsStatusBroadcaster", () => { yield* Deferred.succeed(finishFetch, undefined); const status = yield* Fiber.join(refresh); assert.deepStrictEqual(status.branchChanges, local.branchChanges); + + // Without a fetch nothing can move the base, so the working tree is scanned once. + fetchNext = false; + localStatusReads = 0; + yield* broadcaster.refreshStatus("/repo"); + assert.equal(localStatusReads, 1); }).pipe(Effect.provide(layerTest)); }); }); @@ -1761,6 +1789,7 @@ describe("VcsStatusBroadcaster", () => { Layer.provide(layerBackgroundPolicy(() => true)), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ + statusFetchCount: Effect.succeed(0), localStatus: () => Effect.sync(() => { state.localStatusCalls += 1; diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts index 06cc4aff7dfa2..bce228ca3ccf6 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts @@ -607,6 +607,7 @@ export const make = Effect.gen(function* () { "VcsStatusBroadcaster.refreshStatus", )(function* (rawCwd, options) { const cwd = yield* withFileSystem(normalizeCwd(rawCwd)); + const fetchesBeforeLocalRead = yield* workflow.statusFetchCount; yield* refreshLocalStatusCore(cwd); if (options?.localOnly) { // Never cache a remote here: readers take `value: null` as "read and found no PR". @@ -620,8 +621,10 @@ export const make = Effect.gen(function* () { // Bump the PR lookup epoch after earlier remote writers have finished. yield* workflow.invalidateStatus(cwd); const remoteRead = yield* readRemoteStatus(cwd, { refreshUpstream }); - // Fetch can move the base used for local Changes totals. - yield* refreshLocalStatusCore(cwd); + // A fetch can move the base used for local Changes totals; without one, the first read stands. + if ((yield* workflow.statusFetchCount) !== fetchesBeforeLocalRead) { + yield* refreshLocalStatusCore(cwd); + } // Without a fetch, the cached behind count cannot justify an automatic pull. const pulled = refreshUpstream ? yield* maybeAutoPull(cwd, remoteRead, [rawCwd]) : null; if (pulled !== null) return mergeGitStatusParts(pulled.local, pulled.remote); From 184120aa77388e346342d20f2bda9e85fb9cf75c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 11:54:30 +0000 Subject: [PATCH 2/4] fix(server): count a status fetch when git exits, not when its waiter times out Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriverCore.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 7870b92c06513..751c30351da50 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1374,6 +1374,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ); }); + let statusFetchCount = 0; const fetchRemoteForStatus = Effect.fn("GitVcsDriver.fetchRemoteForStatus.wait")(function* ( gitCommonDir: string, remoteName: string, @@ -1423,6 +1424,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* Effect.onExit(() => Effect.sync(() => { statusFetches.delete(key); + // Counted when git exits, not when a waiter times out; a failed fetch can still move refs. + statusFetchCount += 1; }), ), Effect.forkIn(serviceScope, { startImmediately: false, uninterruptible: false }), @@ -1671,18 +1674,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* } } }); - let statusFetchCount = 0; const refreshStatusRemoteCacheEntry = Effect.fn("refreshStatusRemoteCacheEntry")(function* ( cacheKey: StatusRemoteRefreshCacheKey, ) { return yield* fetchRemoteForStatus(cacheKey.gitCommonDir, cacheKey.remoteName).pipe( Effect.as(true as const), - // A failed or interrupted fetch can still have updated some refs. - Effect.ensuring( - Effect.sync(() => { - statusFetchCount += 1; - }), - ), ); }); From 6ce2b9e3242bcb611e616d67c0bb798dfadbe3e4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 12:34:37 +0000 Subject: [PATCH 3/4] fix(server): status refreshes skip the second working-tree scan when a fetch moved no refs The status fetch now runs without --quiet, so Git prints a line for each ref it moves and nothing when every ref was current. Only a fetch that printed something, failed or was interrupted counts as one that may have moved refs. Explicit refreshes and remote polls re-read local status only after such a fetch; polls had re-read on every run since #28600, and with a 30 s poll and 15 s fetch cache every idle poll fetched. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/vcs/GitVcsDriver.ts | 2 +- apps/server/src/vcs/GitVcsDriverCore.test.ts | 5 +++ apps/server/src/vcs/GitVcsDriverCore.ts | 10 +++--- .../src/vcs/VcsStatusBroadcaster.test.ts | 35 ++++++++++++++----- apps/server/src/vcs/VcsStatusBroadcaster.ts | 11 ++++-- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 71313dcb5bd9d..1bf5faee92950 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -333,7 +333,7 @@ export class GitVcsDriver extends Context.Service< cwd: string, options?: GitRemoteStatusOptions, ) => Effect.Effect; - /** Status fetches finished so far, in any repository. A change means remote refs may have moved. */ + /** Status fetches so far, in any repository, that moved refs or failed. A change means refs may have moved. */ readonly statusFetchCount: Effect.Effect; readonly prepareCommitContext: ( cwd: string, diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 05b40f72d45ec..d36c095c82d9f 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -4652,6 +4652,11 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => { assert.equal(cachedStatus.behindCount, 0); assert.equal(refreshedStatus.behindCount, 1); assert.equal(yield* driver.statusFetchCount, fetchesBefore + 1); + + // A later fetch that moves no refs leaves the count alone. + yield* TestClock.adjust("16 seconds"); + yield* driver.statusDetailsRemote(cwd); + assert.equal(yield* driver.statusFetchCount, fetchesBefore + 1); }), ); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 751c30351da50..b7003f5093bc0 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1389,7 +1389,8 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* "--git-dir", gitCommonDir, "fetch", - "--quiet", + // Not `--quiet`: Git prints one line per ref it moved, and nothing when every ref was current. + "--no-progress", "--no-tags", "--no-auto-gc", // FETCH_HEAD is written without a lock; overlapping a user's `git pull` makes that pull fail. @@ -1412,7 +1413,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* fallbackErrorDetail: "Background Git fetch exited with a non-zero status.", timeoutMs: Duration.toMillis(STATUS_UPSTREAM_FETCH_TIMEOUT), }).pipe( - Effect.asVoid, + Effect.map((result) => result.stdout.trim().length + result.stderr.trim().length > 0), Effect.tap(() => Effect.sync(() => { statusRemoteRefreshFailures.delete(key); @@ -1421,13 +1422,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* Effect.tapError(() => recordStatusRemoteRefreshFailure(key)), Effect.tapCause((cause) => Effect.logWarning("Background Git fetch failed", cause)), statusFetchProcesses.withPermits(1), - Effect.onExit(() => + Effect.onExit((exit) => Effect.sync(() => { statusFetches.delete(key); // Counted when git exits, not when a waiter times out; a failed fetch can still move refs. - statusFetchCount += 1; + if (Exit.isFailure(exit) || exit.value) statusFetchCount += 1; }), ), + Effect.asVoid, Effect.forkIn(serviceScope, { startImmediately: false, uninterruptible: false }), ); statusFetches.set(key, started); diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts index b1b0773b2a334..30e2f2f5c9e88 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts @@ -1075,7 +1075,7 @@ describe("VcsStatusBroadcaster", () => { }, ); - it.effect("a remote poll refreshes Changes totals when divergence counts stay the same", () => { + it.effect("a remote poll refreshes Changes totals only when its fetch moved refs", () => { const state: Parameters[0] = { currentLocalStatus: { ...baseLocalStatus, @@ -1092,25 +1092,44 @@ describe("VcsStatusBroadcaster", () => { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; yield* broadcaster.getStatus({ cwd: "/repo" }); const subscribed = yield* Deferred.make(); - const polled = yield* Deferred.make(); + const polled = yield* Queue.unbounded(); yield* broadcaster.streamStatus({ cwd: "/repo" }).pipe( Stream.runForEach((event) => { if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined); - if (event._tag === "remoteUpdated") return Deferred.succeed(polled, undefined); + if (event._tag === "remoteUpdated") return Queue.offer(polled, undefined); return Effect.void; }), Effect.forkScoped, ); yield* Deferred.await(subscribed); + const movedTotals = { baseRef: "origin/main", insertions: 107, deletions: 4 }; + state.currentLocalStatus = { ...baseLocalStatus, branchChanges: movedTotals }; + state.currentRemoteStatus = remoteStatusWithPr; + state.beforeRemoteStatus = Effect.sync(() => { + state.statusFetchCount = (state.statusFetchCount ?? 0) + 1; + }); + yield* TestClock.adjust("30 seconds"); + yield* Queue.take(polled); + assert.deepStrictEqual( + (yield* broadcaster.getStatus({ cwd: "/repo" })).branchChanges, + movedTotals, + ); + + // This fetch moved nothing, so the poll publishes its remote read without a working-tree scan. + delete state.beforeRemoteStatus; + state.currentRemoteStatus = baseRemoteStatus; + const localReads = state.localStatusCalls; state.currentLocalStatus = { ...baseLocalStatus, - branchChanges: { baseRef: "origin/main", insertions: 107, deletions: 4 }, + branchChanges: { baseRef: "origin/main", insertions: 1, deletions: 1 }, }; - state.currentRemoteStatus = remoteStatusWithPr; yield* TestClock.adjust("30 seconds"); - yield* Deferred.await(polled); - const status = yield* broadcaster.getStatus({ cwd: "/repo" }); - assert.deepStrictEqual(status.branchChanges, state.currentLocalStatus.branchChanges); + yield* Queue.take(polled); + assert.deepStrictEqual( + (yield* broadcaster.getStatus({ cwd: "/repo" })).branchChanges, + movedTotals, + ); + assert.equal(state.localStatusCalls, localReads); }).pipe(Effect.provide(layerTestFor(state))); }); diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts index bce228ca3ccf6..3886cde736dcb 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts @@ -590,14 +590,21 @@ export const make = Effect.gen(function* () { return yield* withRemoteWriteLock( cwd, Effect.gen(function* () { + const fetchesBeforeRemoteRead = yield* workflow.statusFetchCount; const remoteRead = yield* readRemoteStatus(cwd, options); const pulled = options?.refreshUpstream === false ? null : yield* maybeAutoPull(cwd, remoteRead, options?.policyCwds ?? [cwd]); if (pulled !== null) return pulled.remote; - // Base refs and merge bases can move without changing divergence counts. - yield* refreshLocalStatusCore(cwd); + // Base refs and merge bases can move without changing divergence counts; a poll whose + // fetch moved no refs leaves them where they were. + if ( + options?.refreshUpstream === false || + (yield* workflow.statusFetchCount) !== fetchesBeforeRemoteRead + ) { + yield* refreshLocalStatusCore(cwd); + } return yield* updateCachedRemoteStatus(cwd, remoteRead, { publish: true }); }), ); From b584ae432cd9278ffaa0b70e09c829ed6fd88f58 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 12:38:47 +0000 Subject: [PATCH 4/4] fix(server): keep the poll's local status read for working-tree edits The background poll is what picks up edits made outside the app between explicit refreshes, so it reads local status on every run again. Explicit refreshes still skip their second read when no fetch moved refs. Co-Authored-By: Claude Opus 5.5 --- .../src/vcs/VcsStatusBroadcaster.test.ts | 35 +++++-------------- apps/server/src/vcs/VcsStatusBroadcaster.ts | 11 ++---- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts index 30e2f2f5c9e88..b1b0773b2a334 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts @@ -1075,7 +1075,7 @@ describe("VcsStatusBroadcaster", () => { }, ); - it.effect("a remote poll refreshes Changes totals only when its fetch moved refs", () => { + it.effect("a remote poll refreshes Changes totals when divergence counts stay the same", () => { const state: Parameters[0] = { currentLocalStatus: { ...baseLocalStatus, @@ -1092,44 +1092,25 @@ describe("VcsStatusBroadcaster", () => { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; yield* broadcaster.getStatus({ cwd: "/repo" }); const subscribed = yield* Deferred.make(); - const polled = yield* Queue.unbounded(); + const polled = yield* Deferred.make(); yield* broadcaster.streamStatus({ cwd: "/repo" }).pipe( Stream.runForEach((event) => { if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined); - if (event._tag === "remoteUpdated") return Queue.offer(polled, undefined); + if (event._tag === "remoteUpdated") return Deferred.succeed(polled, undefined); return Effect.void; }), Effect.forkScoped, ); yield* Deferred.await(subscribed); - const movedTotals = { baseRef: "origin/main", insertions: 107, deletions: 4 }; - state.currentLocalStatus = { ...baseLocalStatus, branchChanges: movedTotals }; - state.currentRemoteStatus = remoteStatusWithPr; - state.beforeRemoteStatus = Effect.sync(() => { - state.statusFetchCount = (state.statusFetchCount ?? 0) + 1; - }); - yield* TestClock.adjust("30 seconds"); - yield* Queue.take(polled); - assert.deepStrictEqual( - (yield* broadcaster.getStatus({ cwd: "/repo" })).branchChanges, - movedTotals, - ); - - // This fetch moved nothing, so the poll publishes its remote read without a working-tree scan. - delete state.beforeRemoteStatus; - state.currentRemoteStatus = baseRemoteStatus; - const localReads = state.localStatusCalls; state.currentLocalStatus = { ...baseLocalStatus, - branchChanges: { baseRef: "origin/main", insertions: 1, deletions: 1 }, + branchChanges: { baseRef: "origin/main", insertions: 107, deletions: 4 }, }; + state.currentRemoteStatus = remoteStatusWithPr; yield* TestClock.adjust("30 seconds"); - yield* Queue.take(polled); - assert.deepStrictEqual( - (yield* broadcaster.getStatus({ cwd: "/repo" })).branchChanges, - movedTotals, - ); - assert.equal(state.localStatusCalls, localReads); + yield* Deferred.await(polled); + const status = yield* broadcaster.getStatus({ cwd: "/repo" }); + assert.deepStrictEqual(status.branchChanges, state.currentLocalStatus.branchChanges); }).pipe(Effect.provide(layerTestFor(state))); }); diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts index 3886cde736dcb..bce228ca3ccf6 100644 --- a/apps/server/src/vcs/VcsStatusBroadcaster.ts +++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts @@ -590,21 +590,14 @@ export const make = Effect.gen(function* () { return yield* withRemoteWriteLock( cwd, Effect.gen(function* () { - const fetchesBeforeRemoteRead = yield* workflow.statusFetchCount; const remoteRead = yield* readRemoteStatus(cwd, options); const pulled = options?.refreshUpstream === false ? null : yield* maybeAutoPull(cwd, remoteRead, options?.policyCwds ?? [cwd]); if (pulled !== null) return pulled.remote; - // Base refs and merge bases can move without changing divergence counts; a poll whose - // fetch moved no refs leaves them where they were. - if ( - options?.refreshUpstream === false || - (yield* workflow.statusFetchCount) !== fetchesBeforeRemoteRead - ) { - yield* refreshLocalStatusCore(cwd); - } + // Base refs and merge bases can move without changing divergence counts. + yield* refreshLocalStatusCore(cwd); return yield* updateCachedRemoteStatus(cwd, remoteRead, { publish: true }); }), );