Defer workspace diff stats until post-bootstrap updates

This commit is contained in:
Mohamed Boudra
2026-04-07 16:09:47 +07:00
parent 682fc54778
commit 21c7761403
2 changed files with 346 additions and 120 deletions
+109 -70
View File
@@ -5367,7 +5367,21 @@ export class Session {
return { ...base, name: displayName, diffStat }; return { ...base, name: displayName, diffStat };
} }
private async listWorkspaceDescriptorsSnapshot(): Promise<WorkspaceDescriptorPayload[]> { private async buildWorkspaceDescriptor(input: {
workspace: PersistedWorkspaceRecord;
projectRecord?: PersistedProjectRecord | null;
includeGitData: boolean;
}): Promise<WorkspaceDescriptorPayload> {
if (input.includeGitData && input.projectRecord?.kind === "git") {
return this.describeWorkspaceRecordWithGitData(input.workspace, input.projectRecord);
}
return this.describeWorkspaceRecord(input.workspace, input.projectRecord);
}
private async buildWorkspaceDescriptorMap(options: {
includeGitData: boolean;
workspaceIds?: Iterable<string>;
}): Promise<Map<string, WorkspaceDescriptorPayload>> {
const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([ const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
this.listAgentPayloads(), this.listAgentPayloads(),
this.workspaceRegistry.list(), this.workspaceRegistry.list(),
@@ -5381,14 +5395,26 @@ export class Session {
.map((project) => [project.projectId, project] as const), .map((project) => [project.projectId, project] as const),
); );
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>(); const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
const workspaceIds = options.workspaceIds
? new Set(
Array.from(options.workspaceIds, (workspaceId) =>
normalizePersistedWorkspaceId(workspaceId),
),
)
: null;
for (const workspace of activeRecords) { for (const workspace of activeRecords) {
if (workspaceIds && !workspaceIds.has(workspace.workspaceId)) {
continue;
}
const projectRecord = activeProjects.get(workspace.projectId) ?? null;
descriptorsByWorkspaceId.set( descriptorsByWorkspaceId.set(
workspace.workspaceId, workspace.workspaceId,
await this.describeWorkspaceRecord( await this.buildWorkspaceDescriptor({
workspace, workspace,
activeProjects.get(workspace.projectId) ?? null, projectRecord,
), includeGitData: options.includeGitData,
}),
); );
} }
@@ -5410,7 +5436,15 @@ export class Session {
existing.activityAt = this.accumulateLatestActivityAt(existing.activityAt, agent); existing.activityAt = this.accumulateLatestActivityAt(existing.activityAt, agent);
} }
return Array.from(descriptorsByWorkspaceId.values()); return descriptorsByWorkspaceId;
}
private async listWorkspaceDescriptorsSnapshot(): Promise<WorkspaceDescriptorPayload[]> {
return Array.from(
(await this.buildWorkspaceDescriptorMap({
includeGitData: false,
})).values(),
);
} }
private resolveRegisteredWorkspaceIdForCwd( private resolveRegisteredWorkspaceIdForCwd(
@@ -5770,8 +5804,7 @@ export class Session {
} }
private async reconcileAndEmitWorkspaceUpdates(): Promise<void> { private async reconcileAndEmitWorkspaceUpdates(): Promise<void> {
const subscription = this.workspaceUpdatesSubscription; if (!this.workspaceUpdatesSubscription) {
if (!subscription) {
return; return;
} }
try { try {
@@ -5779,82 +5812,34 @@ export class Session {
if (changedWorkspaceIds.size === 0) { if (changedWorkspaceIds.size === 0) {
return; return;
} }
const all = await this.listWorkspaceDescriptorsSnapshot(); await this.emitWorkspaceUpdatesForWorkspaceIds(changedWorkspaceIds, {
const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const)); skipReconcile: true,
for (const workspaceId of changedWorkspaceIds) { });
const workspace = descriptorsByWorkspaceId.get(workspaceId) ?? null;
if (workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })) {
this.bufferOrEmitWorkspaceUpdate(subscription, { kind: "upsert", workspace });
} else {
this.bufferOrEmitWorkspaceUpdate(subscription, { kind: "remove", id: workspaceId });
}
}
} catch (error) { } catch (error) {
this.sessionLogger.error({ err: error }, "Background workspace reconciliation failed"); this.sessionLogger.error({ err: error }, "Background workspace reconciliation failed");
} }
} }
private async emitWorkspaceUpdateForCwd( private async emitWorkspaceUpdatesForWorkspaceIds(
cwd: string, workspaceIds: Iterable<string>,
options?: { dedupeGitState?: boolean }, options?: { dedupeGitState?: boolean; skipReconcile?: boolean },
): Promise<void> { ): Promise<void> {
const subscription = this.workspaceUpdatesSubscription; const subscription = this.workspaceUpdatesSubscription;
if (!subscription) { if (!subscription) {
return; return;
} }
const activeWorkspaces = (await this.workspaceRegistry.list()).filter( const uniqueWorkspaceIds = new Set(
(workspace) => !workspace.archivedAt, Array.from(workspaceIds, (workspaceId) => normalizePersistedWorkspaceId(workspaceId)),
); );
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces); if (uniqueWorkspaceIds.size === 0) {
const all = await this.listWorkspaceDescriptorsSnapshot();
const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const));
const workspace = descriptorsByWorkspaceId.get(workspaceId);
const nextWorkspace =
workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
? workspace
: null;
if (
options?.dedupeGitState &&
this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)
) {
void this.reconcileAndEmitWorkspaceUpdates();
return;
}
this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace);
if (!nextWorkspace) {
this.bufferOrEmitWorkspaceUpdate(subscription, {
kind: "remove",
id: workspaceId,
});
} else {
this.bufferOrEmitWorkspaceUpdate(subscription, {
kind: "upsert",
workspace: nextWorkspace,
});
}
void this.reconcileAndEmitWorkspaceUpdates();
}
private async emitWorkspaceUpdatesForCwds(cwds: Iterable<string>): Promise<void> {
const subscription = this.workspaceUpdatesSubscription;
if (!subscription) {
return; return;
} }
const activeWorkspaces = (await this.workspaceRegistry.list()).filter( const descriptorsByWorkspaceId = await this.buildWorkspaceDescriptorMap({
(workspace) => !workspace.archivedAt, workspaceIds: uniqueWorkspaceIds,
); includeGitData: true,
const uniqueWorkspaceIds = new Set<string>(); });
for (const cwd of cwds) {
uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces));
}
const all = await this.listWorkspaceDescriptorsSnapshot();
const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const));
for (const workspaceId of uniqueWorkspaceIds) { for (const workspaceId of uniqueWorkspaceIds) {
const workspace = descriptorsByWorkspaceId.get(workspaceId); const workspace = descriptorsByWorkspaceId.get(workspaceId);
@@ -5862,6 +5847,12 @@ export class Session {
workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter }) workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
? workspace ? workspace
: null; : null;
if (
options?.dedupeGitState &&
this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)
) {
continue;
}
this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace); this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace);
if (!nextWorkspace) { if (!nextWorkspace) {
@@ -5878,7 +5869,52 @@ export class Session {
}); });
} }
void this.reconcileAndEmitWorkspaceUpdates(); if (!options?.skipReconcile) {
void this.reconcileAndEmitWorkspaceUpdates();
}
}
private scheduleWorkspaceGitBootstrapUpdates(options: {
subscriptionId: string;
workspaces: Iterable<WorkspaceDescriptorPayload>;
}): void {
const gitWorkspaceIds = Array.from(options.workspaces, (workspace) => workspace)
.filter((workspace) => workspace.projectKind === "git")
.map((workspace) => workspace.id);
if (gitWorkspaceIds.length === 0) {
return;
}
queueMicrotask(() => {
if (this.workspaceUpdatesSubscription?.subscriptionId !== options.subscriptionId) {
return;
}
void this.emitWorkspaceUpdatesForWorkspaceIds(gitWorkspaceIds, {
skipReconcile: true,
});
});
}
private async emitWorkspaceUpdateForCwd(
cwd: string,
options?: { dedupeGitState?: boolean },
): Promise<void> {
const activeWorkspaces = (await this.workspaceRegistry.list()).filter(
(workspace) => !workspace.archivedAt,
);
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces);
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], options);
}
private async emitWorkspaceUpdatesForCwds(cwds: Iterable<string>): Promise<void> {
const activeWorkspaces = (await this.workspaceRegistry.list()).filter(
(workspace) => !workspace.archivedAt,
);
const uniqueWorkspaceIds = new Set<string>();
for (const cwd of cwds) {
uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces));
}
await this.emitWorkspaceUpdatesForWorkspaceIds(uniqueWorkspaceIds);
} }
private async handleFetchAgents( private async handleFetchAgents(
@@ -5991,6 +6027,10 @@ export class Session {
if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) { if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
this.flushBootstrappedWorkspaceUpdates({ snapshotLatestActivityByWorkspaceId }); this.flushBootstrappedWorkspaceUpdates({ snapshotLatestActivityByWorkspaceId });
void this.reconcileAndEmitWorkspaceUpdates(); void this.reconcileAndEmitWorkspaceUpdates();
this.scheduleWorkspaceGitBootstrapUpdates({
subscriptionId,
workspaces: payload.entries,
});
} }
} catch (error) { } catch (error) {
if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) { if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
@@ -6558,7 +6598,6 @@ export class Session {
: result.status === "error" : result.status === "error"
? "error" ? "error"
: "idle"; : "idle";
this.emit({ this.emit({
type: "wait_for_finish_response", type: "wait_for_finish_response",
payload: { requestId, status, final, error: null, lastMessage: result.lastMessage }, payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
@@ -964,34 +964,42 @@ describe("workspace aggregation", () => {
}; };
session.reconcileActiveWorkspaceRecords = async () => new Set(); session.reconcileActiveWorkspaceRecords = async () => new Set();
session.listWorkspaceDescriptorsSnapshot = async () => [ session.buildWorkspaceDescriptorMap = async () =>
{ new Map([
id: "/tmp/repo", [
projectId: "/tmp/repo", "/tmp/repo",
projectDisplayName: "repo", {
projectRootPath: "/tmp/repo", id: "/tmp/repo",
projectKind: "non_git", projectId: "/tmp/repo",
workspaceKind: "directory", projectDisplayName: "repo",
name: "repo", projectRootPath: "/tmp/repo",
status: "running", projectKind: "non_git",
activityAt: "2026-03-01T12:00:00.000Z", workspaceKind: "directory",
}, name: "repo",
]; status: "running",
activityAt: "2026-03-01T12:00:00.000Z",
},
],
]);
await session.emitWorkspaceUpdateForCwd("/tmp/repo"); await session.emitWorkspaceUpdateForCwd("/tmp/repo");
session.listWorkspaceDescriptorsSnapshot = async () => [ session.buildWorkspaceDescriptorMap = async () =>
{ new Map([
id: "/tmp/repo", [
projectId: "/tmp/repo", "/tmp/repo",
projectDisplayName: "repo", {
projectRootPath: "/tmp/repo", id: "/tmp/repo",
projectKind: "non_git", projectId: "/tmp/repo",
workspaceKind: "directory", projectDisplayName: "repo",
name: "repo", projectRootPath: "/tmp/repo",
status: "done", projectKind: "non_git",
activityAt: null, workspaceKind: "directory",
}, name: "repo",
]; status: "done",
activityAt: null,
},
],
]);
await session.emitWorkspaceUpdateForCwd("/tmp/repo"); await session.emitWorkspaceUpdateForCwd("/tmp/repo");
const workspaceUpdates = emitted.filter((message) => message.type === "workspace_update"); const workspaceUpdates = emitted.filter((message) => message.type === "workspace_update");
@@ -1083,30 +1091,37 @@ describe("workspace aggregation", () => {
}; };
session.reconcileActiveWorkspaceRecords = async () => session.reconcileActiveWorkspaceRecords = async () =>
new Set(["/tmp/repo", "/tmp/repo/worktree"]); new Set(["/tmp/repo", "/tmp/repo/worktree"]);
session.listWorkspaceDescriptorsSnapshot = async () => [ session.buildWorkspaceDescriptorMap = async () =>
{ new Map([
id: "/tmp/repo", [
projectId: "/tmp/repo", "/tmp/repo",
projectDisplayName: "repo", {
projectRootPath: "/tmp/repo", id: "/tmp/repo",
projectKind: "git", projectId: "/tmp/repo",
workspaceKind: "local_checkout", projectDisplayName: "repo",
name: "main", projectRootPath: "/tmp/repo",
status: "done", projectKind: "git",
activityAt: null, workspaceKind: "local_checkout",
}, name: "main",
{ status: "done",
id: "/tmp/repo/worktree", activityAt: null,
projectId: "/tmp/repo", },
projectDisplayName: "repo", ],
projectRootPath: "/tmp/repo", [
projectKind: "git", "/tmp/repo/worktree",
workspaceKind: "worktree", {
name: "feature", id: "/tmp/repo/worktree",
status: "running", projectId: "/tmp/repo",
activityAt: "2026-03-01T12:00:00.000Z", projectDisplayName: "repo",
}, projectRootPath: "/tmp/repo",
]; projectKind: "git",
workspaceKind: "worktree",
name: "feature",
status: "running",
activityAt: "2026-03-01T12:00:00.000Z",
},
],
]);
session.onMessage = (message: { type: string; payload: unknown }) => { session.onMessage = (message: { type: string; payload: unknown }) => {
emitted.push(message); emitted.push(message);
}; };
@@ -1605,4 +1620,176 @@ describe("workspace aggregation", () => {
rmSync(tempDir, { recursive: true, force: true }); rmSync(tempDir, { recursive: true, force: true });
} }
}); });
test("listWorkspaceDescriptorsSnapshot keeps git workspaces on the baseline descriptor path", async () => {
const session = createSessionForWorkspaceTests() as any;
const project = createPersistedProjectRecord({
projectId: "/tmp/repo",
rootPath: "/tmp/repo",
kind: "git",
displayName: "repo",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const workspace = createPersistedWorkspaceRecord({
workspaceId: "/tmp/repo",
projectId: project.projectId,
cwd: "/tmp/repo",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
session.listAgentPayloads = async () => [];
session.projectRegistry.list = async () => [project];
session.workspaceRegistry.list = async () => [workspace];
const baselineDescriptor = {
id: workspace.workspaceId,
projectId: project.projectId,
projectDisplayName: project.displayName,
projectRootPath: project.rootPath,
projectKind: project.kind,
workspaceKind: workspace.kind,
name: "main",
status: "done",
activityAt: null,
diffStat: null,
} as const;
const gitDescriptor = {
...baselineDescriptor,
diffStat: { additions: 3, deletions: 1 },
} as const;
session.describeWorkspaceRecord = vi.fn(async () => baselineDescriptor);
session.describeWorkspaceRecordWithGitData = vi.fn(async () => gitDescriptor);
const descriptors = await session.listWorkspaceDescriptorsSnapshot();
expect(session.describeWorkspaceRecord).toHaveBeenCalledWith(workspace, project);
expect(session.describeWorkspaceRecordWithGitData).not.toHaveBeenCalled();
expect(descriptors).toEqual([baselineDescriptor]);
});
test("subscribed fetch_workspaces emits git enrichment updates after the baseline snapshot", async () => {
const emitted: Array<{ type: string; payload: any }> = [];
const session = createSessionForWorkspaceTests() as any;
const gitProject = createPersistedProjectRecord({
projectId: "/tmp/repo",
rootPath: "/tmp/repo",
kind: "git",
displayName: "repo",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const directoryProject = createPersistedProjectRecord({
projectId: "/tmp/docs",
rootPath: "/tmp/docs",
kind: "non_git",
displayName: "docs",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const gitWorkspace = createPersistedWorkspaceRecord({
workspaceId: "/tmp/repo",
projectId: gitProject.projectId,
cwd: "/tmp/repo",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const directoryWorkspace = createPersistedWorkspaceRecord({
workspaceId: "/tmp/docs",
projectId: directoryProject.projectId,
cwd: "/tmp/docs",
kind: "directory",
displayName: "docs",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
});
const baselineGitDescriptor = {
id: gitWorkspace.workspaceId,
projectId: gitProject.projectId,
projectDisplayName: gitProject.displayName,
projectRootPath: gitProject.rootPath,
projectKind: gitProject.kind,
workspaceKind: gitWorkspace.kind,
name: "main",
status: "done",
activityAt: null,
diffStat: null,
} as const;
const enrichedGitDescriptor = {
...baselineGitDescriptor,
diffStat: { additions: 3, deletions: 1 },
} as const;
const directoryDescriptor = {
id: directoryWorkspace.workspaceId,
projectId: directoryProject.projectId,
projectDisplayName: directoryProject.displayName,
projectRootPath: directoryProject.rootPath,
projectKind: directoryProject.kind,
workspaceKind: directoryWorkspace.kind,
name: "docs",
status: "done",
activityAt: null,
diffStat: null,
} as const;
session.emit = (message: any) => emitted.push(message);
session.listAgentPayloads = async () => [];
session.projectRegistry.list = async () => [gitProject, directoryProject];
session.workspaceRegistry.list = async () => [gitWorkspace, directoryWorkspace];
session.reconcileAndEmitWorkspaceUpdates = vi.fn(async () => {});
session.describeWorkspaceRecord = vi.fn(
async (workspace: typeof gitWorkspace | typeof directoryWorkspace, project: any) => {
if (workspace.workspaceId === gitWorkspace.workspaceId) {
expect(project).toEqual(gitProject);
return baselineGitDescriptor;
}
expect(project).toEqual(directoryProject);
return directoryDescriptor;
},
);
session.describeWorkspaceRecordWithGitData = vi.fn(async () => enrichedGitDescriptor);
await session.handleMessage({
type: "fetch_workspaces_request",
requestId: "req-fetch-workspaces",
subscribe: {},
});
await new Promise((resolve) => setTimeout(resolve, 0));
const response = emitted.find(
(message) => message.type === "fetch_workspaces_response",
) as { type: "fetch_workspaces_response"; payload: any } | undefined;
expect(
response?.payload.entries.map((entry: typeof baselineGitDescriptor | typeof directoryDescriptor) => [
entry.id,
entry.diffStat,
]),
).toEqual([
[directoryDescriptor.id, directoryDescriptor.diffStat],
[baselineGitDescriptor.id, baselineGitDescriptor.diffStat],
]);
const workspaceUpdates = emitted.filter(
(message) => message.type === "workspace_update",
) as Array<{ type: "workspace_update"; payload: any }>;
expect(workspaceUpdates).toEqual([
{
type: "workspace_update",
payload: {
kind: "upsert",
workspace: enrichedGitDescriptor,
},
},
]);
expect(session.describeWorkspaceRecordWithGitData).toHaveBeenCalledWith(
gitWorkspace,
gitProject,
);
});
}); });