From cdbaa8d29c0844c05a4e9eae19cc971793e730e9 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 6 Apr 2026 21:35:07 -0500 Subject: [PATCH] fix(server): serve workspace list instantly on fetch, reconcile in background (#204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(server): serve workspace list instantly on fetch, reconcile in background Previously fetch_workspaces_request awaited reconcileActiveWorkspaceRecords() before responding — this ran git operations (getCheckoutStatusLite) on every active workspace, causing projects to appear empty for several seconds after daemon restart or reconnect. Now the registry snapshot is returned immediately and reconciliation runs in the background. Any changed workspaces (e.g. stale worktrees being archived) are pushed as workspace_update events through the existing subscription. * fix(server): make workspace snapshot instant by removing per-workspace git calls describeWorkspaceRecord was running two git operations per workspace: - buildProjectPlacement (to refresh display name) - getCheckoutShortstat (for diff indicator) With 13 workspaces this caused fetch_workspaces_request to take 7-15s (observed as ws_slow_request in logs). Split into two methods: - describeWorkspaceRecord: uses only persisted data, returns instantly - describeWorkspaceRecordWithGitData: runs git ops, used for open_project and create_worktree responses where fresh data is needed immediately The snapshot path (initial load + background reconciliation) now uses the fast variant. diffStat is null on initial load; it will be pushed later via workspace_update once a git-aware path triggers. --- packages/server/src/server/session.ts | 64 ++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index ac0da00a8..916650f10 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -5307,6 +5307,26 @@ export class Session { ): Promise { const resolvedProjectRecord = projectRecord ?? (await this.projectRegistry.get(workspace.projectId)); + + return { + id: workspace.workspaceId, + projectId: workspace.projectId, + projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId, + projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd, + projectKind: resolvedProjectRecord?.kind ?? "non_git", + workspaceKind: workspace.kind, + name: workspace.displayName, + status: "done", + activityAt: null, + diffStat: null, + }; + } + + private async describeWorkspaceRecordWithGitData( + workspace: PersistedWorkspaceRecord, + projectRecord?: PersistedProjectRecord | null, + ): Promise { + const base = await this.describeWorkspaceRecord(workspace, projectRecord); let displayName = workspace.displayName; try { const placement = await this.buildProjectPlacement(workspace.cwd); @@ -5325,18 +5345,7 @@ export class Session { // Non-critical — leave null on failure. } - return { - id: workspace.workspaceId, - projectId: workspace.projectId, - projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId, - projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd, - projectKind: resolvedProjectRecord?.kind ?? "non_git", - workspaceKind: workspace.kind, - name: displayName, - status: "done", - activityAt: null, - diffStat, - }; + return { ...base, name: displayName, diffStat }; } private async listWorkspaceDescriptorsSnapshot(): Promise { @@ -5412,7 +5421,6 @@ export class Session { } private async listWorkspaceDescriptors(): Promise { - await this.reconcileActiveWorkspaceRecords(); return this.listWorkspaceDescriptorsSnapshot(); } @@ -5742,6 +5750,31 @@ export class Session { } } + private async reconcileAndEmitWorkspaceUpdates(): Promise { + const subscription = this.workspaceUpdatesSubscription; + if (!subscription) { + return; + } + try { + const changedWorkspaceIds = await this.reconcileActiveWorkspaceRecords(); + if (changedWorkspaceIds.size === 0) { + return; + } + const all = await this.listWorkspaceDescriptorsSnapshot(); + const descriptorsByWorkspaceId = new Map(all.map((entry) => [entry.id, entry] as const)); + 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) { + this.sessionLogger.error({ err: error }, "Background workspace reconciliation failed"); + } + } + private async emitWorkspaceUpdateForCwd( cwd: string, options?: { dedupeGitState?: boolean }, @@ -5942,6 +5975,7 @@ export class Session { if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) { this.flushBootstrappedWorkspaceUpdates({ snapshotLatestActivityByWorkspaceId }); + void this.reconcileAndEmitWorkspaceUpdates(); } } catch (error) { if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) { @@ -5968,7 +6002,7 @@ export class Session { try { const workspace = await this.ensureWorkspaceRegistered(request.cwd); await this.emitWorkspaceUpdateForCwd(workspace.cwd); - const descriptor = await this.describeWorkspaceRecord(workspace); + const descriptor = await this.describeWorkspaceRecordWithGitData(workspace); this.emit({ type: "open_project_response", payload: { @@ -6068,7 +6102,7 @@ export class Session { return handleCreateWorktreeRequest( { paseoHome: this.paseoHome, - describeWorkspaceRecord: (workspace) => this.describeWorkspaceRecord(workspace), + describeWorkspaceRecord: (workspace) => this.describeWorkspaceRecordWithGitData(workspace), emit: (message) => this.emit(message), registerPendingWorktreeWorkspace: (options) => this.registerPendingWorktreeWorkspace(options),