fix: use cached git service snapshots for fetchWorkspaces instead of shelling out

describeWorkspaceRecordWithGitData was calling buildProjectPlacement → getCheckoutStatusLite
which spawned 3-4 sequential git child processes per workspace. With 8 active workspaces
and event loop pressure from running agents, this caused fetchWorkspaces to take 35-45s.

Now uses workspaceGitService.getSnapshot() which returns instantly from cache (warm path)
or awaits the git service's own concurrency-managed refresh (cold path).
This commit is contained in:
Mohamed Boudra
2026-04-14 03:54:32 +07:00
parent 7ad4482096
commit be3d5ed78d
3 changed files with 55 additions and 25 deletions
+7 -15
View File
@@ -114,6 +114,7 @@ import type {
import { AgentStorage, type StoredAgentRecord } from "./agent/agent-storage.js"; import { AgentStorage, type StoredAgentRecord } from "./agent/agent-storage.js";
import { import {
buildProjectPlacementForCwd, buildProjectPlacementForCwd,
checkoutLiteFromGitSnapshot,
detectStaleWorkspaces, detectStaleWorkspaces,
deriveProjectKind, deriveProjectKind,
deriveProjectRootPath, deriveProjectRootPath,
@@ -5406,26 +5407,17 @@ export class Session {
projectRecord?: PersistedProjectRecord | null, projectRecord?: PersistedProjectRecord | null,
): Promise<WorkspaceDescriptorPayload> { ): Promise<WorkspaceDescriptorPayload> {
const base = await this.describeWorkspaceRecord(workspace, projectRecord); const base = await this.describeWorkspaceRecord(workspace, projectRecord);
let displayName = workspace.displayName; const snapshot = await this.workspaceGitService.getSnapshot(workspace.cwd);
try {
const placement = await this.buildProjectPlacement(workspace.cwd);
displayName = deriveWorkspaceDisplayName({
cwd: workspace.cwd,
checkout: placement.checkout,
});
} catch {
// Fall back to the persisted label if checkout metadata is unavailable.
}
let snapshot: WorkspaceGitRuntimeSnapshot | null = null; const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git);
snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd); const displayName = deriveWorkspaceDisplayName({ cwd: workspace.cwd, checkout });
return { return {
...base, ...base,
name: displayName, name: displayName,
diffStat: snapshot?.git.diffStat ?? null, diffStat: snapshot.git.diffStat ?? null,
gitRuntime: snapshot ? this.buildWorkspaceGitRuntimePayload(snapshot) : undefined, gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot),
githubRuntime: snapshot ? this.buildWorkspaceGitHubRuntimePayload(snapshot) : undefined, githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
}; };
} }
@@ -1845,9 +1845,7 @@ describe("workspace aggregation", () => {
}); });
const workspaceGitService = createNoopWorkspaceGitService(); const workspaceGitService = createNoopWorkspaceGitService();
workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot); workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot);
workspaceGitService.getSnapshot = vi.fn(async () => { workspaceGitService.getSnapshot = vi.fn(async () => runtimeSnapshot);
throw new Error("fetch_workspaces should not trigger per-workspace refreshes");
});
workspaceGitService.subscribe = vi.fn(async () => ({ workspaceGitService.subscribe = vi.fn(async () => ({
initial: runtimeSnapshot, initial: runtimeSnapshot,
unsubscribe: () => {}, unsubscribe: () => {},
@@ -1901,8 +1899,7 @@ describe("workspace aggregation", () => {
| { type: "fetch_workspaces_response"; payload: any } | { type: "fetch_workspaces_response"; payload: any }
| undefined; | undefined;
expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo"); expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo");
expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
expect(response?.payload.entries).toEqual([ expect(response?.payload.entries).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "/tmp/repo", id: "/tmp/repo",
@@ -1953,9 +1950,7 @@ describe("workspace aggregation", () => {
}); });
const workspaceGitService = createNoopWorkspaceGitService(); const workspaceGitService = createNoopWorkspaceGitService();
workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot); workspaceGitService.peekSnapshot = vi.fn(() => runtimeSnapshot);
workspaceGitService.getSnapshot = vi.fn(async () => { workspaceGitService.getSnapshot = vi.fn(async () => runtimeSnapshot);
throw new Error("workspace updates should use passive workspace git snapshots");
});
const session = createSessionForWorkspaceTests({ const session = createSessionForWorkspaceTests({
workspaceGitService, workspaceGitService,
@@ -2008,8 +2003,7 @@ describe("workspace aggregation", () => {
skipReconcile: true, skipReconcile: true,
}); });
expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo"); expect(workspaceGitService.getSnapshot).toHaveBeenCalledWith("/tmp/repo");
expect(workspaceGitService.getSnapshot).not.toHaveBeenCalled();
expect(emitted).toContainEqual({ expect(emitted).toContainEqual({
type: "workspace_update", type: "workspace_update",
payload: { payload: {
@@ -137,6 +137,50 @@ export function deriveWorkspaceKind(checkout: ProjectCheckoutLitePayload): Persi
return checkout.isPaseoOwnedWorktree ? "worktree" : "local_checkout"; return checkout.isPaseoOwnedWorktree ? "worktree" : "local_checkout";
} }
export function checkoutLiteFromGitSnapshot(
cwd: string,
git: {
isGit: boolean;
currentBranch: string | null;
remoteUrl: string | null;
repoRoot: string | null;
isPaseoOwnedWorktree: boolean;
mainRepoRoot: string | null;
},
): ProjectCheckoutLitePayload {
if (!git.isGit) {
return {
cwd,
isGit: false,
currentBranch: null,
remoteUrl: null,
worktreeRoot: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
};
}
if (git.isPaseoOwnedWorktree && git.mainRepoRoot) {
return {
cwd,
isGit: true,
currentBranch: git.currentBranch,
remoteUrl: git.remoteUrl,
worktreeRoot: git.repoRoot ?? cwd,
isPaseoOwnedWorktree: true,
mainRepoRoot: git.mainRepoRoot,
};
}
return {
cwd,
isGit: true,
currentBranch: git.currentBranch,
remoteUrl: git.remoteUrl,
worktreeRoot: git.repoRoot ?? cwd,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
};
}
export async function detectStaleWorkspaces( export async function detectStaleWorkspaces(
input: DetectStaleWorkspacesInput, input: DetectStaleWorkspacesInput,
): Promise<Set<string>> { ): Promise<Set<string>> {