From e51cfae112f4bade6c3b7390f15a9eef4e2125f1 Mon Sep 17 00:00:00 2001 From: ifBars Date: Wed, 25 Mar 2026 23:26:12 -0700 Subject: [PATCH 1/2] fix(server): keep background git auth alive during status refresh --- apps/server/src/git/Layers/GitCore.test.ts | 87 +++++++++++++++++++++- apps/server/src/git/Layers/GitCore.ts | 2 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 635b9e8bc4..ea8554710a 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -3,7 +3,7 @@ import path from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; -import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect"; +import { Effect, Fiber, FileSystem, Layer, PlatformError, Scope } from "effect"; import { describe, expect, vi } from "vitest"; import { GitCoreLive, makeGitCore } from "./GitCore.ts"; @@ -1610,6 +1610,91 @@ it.layer(TestLayer)("git integration", (it) => { }), ); + it.effect("uses an extended timeout for status upstream refresh fetches", () => + Effect.gen(function* () { + const remote = yield* makeTmpDir(); + const source = yield* makeTmpDir(); + yield* git(remote, ["init", "--bare"]); + + const { initialBranch } = yield* initRepoWithCommit(source); + yield* git(source, ["remote", "add", "origin", remote]); + yield* git(source, ["push", "-u", "origin", initialBranch]); + + const realGitCore = yield* GitCore; + let statusRefreshTimeoutMs: number | undefined; + const core = yield* makeIsolatedGitCore((input) => { + if (input.operation === "GitCore.fetchUpstreamRefForStatus") { + statusRefreshTimeoutMs = input.timeoutMs; + return Effect.succeed({ code: 0, stdout: "", stderr: "" }); + } + return realGitCore.execute(input); + }); + + const details = yield* core.statusDetails(source); + + expect(details.branch).toBe(initialBranch); + expect(statusRefreshTimeoutMs).toBe(300_000); + }), + ); + + it.effect("deduplicates in-flight status upstream refresh fetches", () => + Effect.gen(function* () { + const remote = yield* makeTmpDir(); + const source = yield* makeTmpDir(); + yield* git(remote, ["init", "--bare"]); + + const { initialBranch } = yield* initRepoWithCommit(source); + yield* git(source, ["remote", "add", "origin", remote]); + yield* git(source, ["push", "-u", "origin", initialBranch]); + + const realGitCore = yield* GitCore; + let refreshFetchAttempts = 0; + let releaseFetch!: () => void; + const waitForReleasePromise = new Promise((resolve) => { + releaseFetch = resolve; + }); + const core = yield* makeIsolatedGitCore((input) => { + if (input.operation === "GitCore.fetchUpstreamRefForStatus") { + refreshFetchAttempts += 1; + return Effect.promise(() => + waitForReleasePromise.then(() => ({ code: 0, stdout: "", stderr: "" })), + ); + } + if (input.operation === "GitCore.statusDetails.status") { + return Effect.succeed({ + code: 0, + stdout: `# branch.head ${initialBranch}\n# branch.upstream origin/${initialBranch}\n# branch.ab +0 -0\n`, + stderr: "", + }); + } + if ( + input.operation === "GitCore.statusDetails.unstagedNumstat" || + input.operation === "GitCore.statusDetails.stagedNumstat" + ) { + return Effect.succeed({ code: 0, stdout: "", stderr: "" }); + } + return realGitCore.execute(input); + }); + + const statusFiber = yield* Effect.forkScoped( + Effect.all([core.statusDetails(source), core.statusDetails(source)], { + concurrency: "unbounded", + }), + ); + + yield* Effect.promise(() => + vi.waitFor(() => { + expect(refreshFetchAttempts).toBe(1); + }), + ); + + releaseFetch(); + const [first, second] = yield* Fiber.join(statusFiber); + expect(first.branch).toBe(initialBranch); + expect(second.branch).toBe(initialBranch); + }), + ); + it.effect("prepares commit context by auto-staging and creates commit", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 8bb5844228..44f5f0126b 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -33,7 +33,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson"; const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); -const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); +const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.minutes(5); const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048; const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const; From 185f83a43846f57c9437a4ab51e0f7e0ba904f4e Mon Sep 17 00:00:00 2001 From: ifBars Date: Wed, 25 Mar 2026 23:57:10 -0700 Subject: [PATCH 2/2] fix(server): keep passive git status refresh off blocking paths --- apps/server/src/git/Layers/GitCore.test.ts | 68 +++++++++++----------- apps/server/src/git/Layers/GitCore.ts | 12 +++- apps/server/src/git/Layers/GitManager.ts | 1 + apps/server/src/git/Services/GitCore.ts | 5 ++ 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index ea8554710a..222c0f9c6e 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1572,7 +1572,7 @@ it.layer(TestLayer)("git integration", (it) => { ); it.effect( - "refreshes upstream before statusDetails so behind count reflects remote updates", + "scheduled status upstream refresh updates behind count without blocking status", () => Effect.gen(function* () { const remote = yield* makeTmpDir(); @@ -1580,10 +1580,7 @@ it.layer(TestLayer)("git integration", (it) => { const clone = yield* makeTmpDir(); yield* git(remote, ["init", "--bare"]); - yield* initRepoWithCommit(source); - const initialBranch = (yield* (yield* GitCore).listBranches({ - cwd: source, - })).branches.find((branch) => branch.current)!.name; + const { initialBranch } = yield* initRepoWithCommit(source); yield* git(source, ["remote", "add", "origin", remote]); yield* git(source, ["push", "-u", "origin", initialBranch]); @@ -1603,14 +1600,22 @@ it.layer(TestLayer)("git integration", (it) => { yield* git(clone, ["push", "origin", initialBranch]); const core = yield* GitCore; - const details = yield* core.statusDetails(source); - expect(details.branch).toBe(initialBranch); - expect(details.aheadCount).toBe(0); - expect(details.behindCount).toBe(1); + const beforeRefresh = yield* core.statusDetails(source); + expect(beforeRefresh.behindCount).toBe(0); + + yield* core.scheduleStatusUpstreamRefresh(source); + yield* Effect.promise(() => + vi.waitFor(async () => { + const details = await Effect.runPromise(core.statusDetails(source)); + expect(details.branch).toBe(initialBranch); + expect(details.aheadCount).toBe(0); + expect(details.behindCount).toBe(1); + }), + ); }), ); - it.effect("uses an extended timeout for status upstream refresh fetches", () => + it.effect("uses an extended timeout for scheduled status upstream refresh fetches", () => Effect.gen(function* () { const remote = yield* makeTmpDir(); const source = yield* makeTmpDir(); @@ -1630,14 +1635,16 @@ it.layer(TestLayer)("git integration", (it) => { return realGitCore.execute(input); }); - const details = yield* core.statusDetails(source); - - expect(details.branch).toBe(initialBranch); - expect(statusRefreshTimeoutMs).toBe(300_000); + yield* core.scheduleStatusUpstreamRefresh(source); + yield* Effect.promise(() => + vi.waitFor(() => { + expect(statusRefreshTimeoutMs).toBe(300_000); + }), + ); }), ); - it.effect("deduplicates in-flight status upstream refresh fetches", () => + it.effect("deduplicates in-flight scheduled status upstream refresh fetches", () => Effect.gen(function* () { const remote = yield* makeTmpDir(); const source = yield* makeTmpDir(); @@ -1660,26 +1667,19 @@ it.layer(TestLayer)("git integration", (it) => { waitForReleasePromise.then(() => ({ code: 0, stdout: "", stderr: "" })), ); } - if (input.operation === "GitCore.statusDetails.status") { - return Effect.succeed({ - code: 0, - stdout: `# branch.head ${initialBranch}\n# branch.upstream origin/${initialBranch}\n# branch.ab +0 -0\n`, - stderr: "", - }); - } - if ( - input.operation === "GitCore.statusDetails.unstagedNumstat" || - input.operation === "GitCore.statusDetails.stagedNumstat" - ) { - return Effect.succeed({ code: 0, stdout: "", stderr: "" }); - } return realGitCore.execute(input); }); - const statusFiber = yield* Effect.forkScoped( - Effect.all([core.statusDetails(source), core.statusDetails(source)], { - concurrency: "unbounded", - }), + const refreshFiber = yield* Effect.forkScoped( + Effect.all( + [ + core.scheduleStatusUpstreamRefresh(source), + core.scheduleStatusUpstreamRefresh(source), + ], + { + concurrency: "unbounded", + }, + ), ); yield* Effect.promise(() => @@ -1689,9 +1689,7 @@ it.layer(TestLayer)("git integration", (it) => { ); releaseFetch(); - const [first, second] = yield* Fiber.join(statusFiber); - expect(first.branch).toBe(initialBranch); - expect(second.branch).toBe(initialBranch); + yield* Fiber.join(refreshFiber); }), ); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 44f5f0126b..e4c6f0ff99 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -497,6 +497,8 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute" const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const { worktreesDir } = yield* ServerConfig; + const statusRefreshScope = yield* Scope.make("sequential"); + yield* Effect.addFinalizer(() => Scope.close(statusRefreshScope, Exit.void)); let execute: GitCoreShape["execute"]; @@ -779,6 +781,13 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute" ); }); + const scheduleStatusUpstreamRefresh: GitCoreShape["scheduleStatusUpstreamRefresh"] = (cwd) => + refreshStatusUpstreamIfStale(cwd).pipe( + Effect.ignoreCause({ log: true }), + Effect.forkIn(statusRefreshScope), + Effect.asVoid, + ); + const refreshCheckedOutBranchUpstream = (cwd: string): Effect.Effect => Effect.gen(function* () { const upstream = yield* resolveCurrentUpstream(cwd); @@ -1025,8 +1034,6 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute" const statusDetails: GitCoreShape["statusDetails"] = (cwd) => Effect.gen(function* () { - yield* refreshStatusUpstreamIfStale(cwd).pipe(Effect.ignoreCause({ log: true })); - const [statusStdout, unstagedNumstatStdout, stagedNumstatStdout] = yield* Effect.all( [ runGitStdout("GitCore.statusDetails.status", cwd, [ @@ -1797,6 +1804,7 @@ export const makeGitCore = (options?: { executeOverride?: GitCoreShape["execute" execute, status, statusDetails, + scheduleStatusUpstreamRefresh, prepareCommitContext, commit, pushCurrentBranch, diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index e2ae56a90c..eb9fa8a5f3 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -923,6 +923,7 @@ export const makeGitManager = Effect.gen(function* () { }); const status: GitManagerShape["status"] = Effect.fnUntraced(function* (input) { + yield* gitCore.scheduleStatusUpstreamRefresh(input.cwd); const details = yield* gitCore.statusDetails(input.cwd); const pr = diff --git a/apps/server/src/git/Services/GitCore.ts b/apps/server/src/git/Services/GitCore.ts index b74c526897..c63afe6629 100644 --- a/apps/server/src/git/Services/GitCore.ts +++ b/apps/server/src/git/Services/GitCore.ts @@ -147,6 +147,11 @@ export interface GitCoreShape { */ readonly statusDetails: (cwd: string) => Effect.Effect; + /** + * Schedule a best-effort upstream refresh for passive status reads without blocking on auth/network. + */ + readonly scheduleStatusUpstreamRefresh: (cwd: string) => Effect.Effect; + /** * Build staged change context for commit generation. */