fix(sync): show complete workspace and agent history (#2263)

Cached focused state was leaking into the sidebar before authoritative workspace hydration, while catch-up limits counted raw events instead of projected entries. Keep the focused cache fast, gate directory presentation on hydration, and preserve canonical cursors while paging projected history.
This commit is contained in:
Mohamed Boudra
2026-07-20 23:24:45 +08:00
committed by GitHub
parent 0c68b26a8b
commit 07d988488e
9 changed files with 331 additions and 43 deletions
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo } from "react";
import { shallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
import { useHydratedWorkspaceServerIds } from "@/stores/session-store-hooks";
import { useHostProjects } from "@/projects/host-projects";
import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching";
import { getHostRuntimeStore, useHostRegistryLoaded, useHosts } from "@/runtime/host-runtime";
@@ -89,13 +88,9 @@ export function useSidebarWorkspacesList(options?: {
const persistedProjectOrder = useSidebarOrderStore((state) => state.projectOrder ?? EMPTY_ORDER);
const hydratedServerIds = useStoreWithEqualityFn(
useSessionStore,
(state) => serverIds.filter((id) => state.sessions[id]?.hasHydratedWorkspaces ?? false),
shallow,
);
const hydratedServerIds = useHydratedWorkspaceServerIds(serverIds);
const hostProjects = useHostProjects(serverIds);
const hostProjects = useHostProjects(hydratedServerIds);
const sidebarModel = useMemo(
() =>
+24 -27
View File
@@ -1350,7 +1350,7 @@ export class HostRuntimeStore {
private hostRegistryStatus: HostRegistryStatus = "loading";
private deps: HostRuntimeControllerDeps;
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
private agentDirectoryBootstrapInFlight = new Map<string, Promise<void>>();
private directoryBootstrapInFlight = new Map<string, Promise<void>>();
private queuedAgentDrainInFlight = new Set<string>();
private directorySyncByServer = new Map<string, DirectorySync>();
private configuredOverrideBootstrapInFlight: Promise<void> | null = null;
@@ -1586,17 +1586,14 @@ export class HostRuntimeStore {
controller.adoptReconciledServerId(newServerId);
rekeyMap(this.lastConnectionStatusByServer, oldServerId, newServerId);
rekeyMap(this.agentDirectoryBootstrapInFlight, oldServerId, newServerId);
rekeyMap(this.directoryBootstrapInFlight, oldServerId, newServerId);
this.replicaCache.reconcileServerId(oldServerId, newServerId);
this.directorySyncByServer.get(oldServerId)?.dispose();
this.directorySyncByServer.delete(oldServerId);
const directory = new DirectorySync(newServerId, {
drainQueuedAgentMessage: (agentId) => this.drainQueuedAgentMessage(newServerId, agentId),
markAgentLoading: () => controller.markAgentDirectorySyncLoading(),
markAgentReady: () => {
this.agentDirectoryBootstrapInFlight.delete(newServerId);
controller.markAgentDirectorySyncReady();
},
markAgentReady: () => controller.markAgentDirectorySyncReady(),
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
});
this.directorySyncByServer.set(newServerId, directory);
@@ -1899,7 +1896,7 @@ export class HostRuntimeStore {
}
this.controllers.delete(serverId);
this.lastConnectionStatusByServer.delete(serverId);
this.agentDirectoryBootstrapInFlight.delete(serverId);
this.directoryBootstrapInFlight.delete(serverId);
this.directorySyncByServer.get(serverId)?.dispose();
this.directorySyncByServer.delete(serverId);
void controller.stop();
@@ -1930,10 +1927,7 @@ export class HostRuntimeStore {
drainQueuedAgentMessage: (agentId) =>
this.drainQueuedAgentMessage(host.serverId, agentId),
markAgentLoading: () => controller.markAgentDirectorySyncLoading(),
markAgentReady: () => {
this.agentDirectoryBootstrapInFlight.delete(host.serverId);
controller.markAgentDirectorySyncReady();
},
markAgentReady: () => controller.markAgentDirectorySyncReady(),
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
}),
);
@@ -1942,7 +1936,7 @@ export class HostRuntimeStore {
controller.getSnapshot().connectionStatus,
);
controller.subscribe(() => {
this.maybeAutoBootstrapAgentDirectory(host.serverId);
this.maybeAutoBootstrapDirectories(host.serverId);
this.emit(host.serverId);
});
void controller
@@ -1961,11 +1955,11 @@ export class HostRuntimeStore {
}
}
private maybeAutoBootstrapAgentDirectory(serverId: string): void {
private maybeAutoBootstrapDirectories(serverId: string): void {
const controller = this.controllers.get(serverId);
if (!controller) {
this.lastConnectionStatusByServer.delete(serverId);
this.agentDirectoryBootstrapInFlight.delete(serverId);
this.directoryBootstrapInFlight.delete(serverId);
return;
}
const snapshot = controller.getSnapshot();
@@ -2000,7 +1994,7 @@ export class HostRuntimeStore {
if (!didTransitionOnline && snapshot.hasEverLoadedAgentDirectory) {
return;
}
if (this.agentDirectoryBootstrapInFlight.has(serverId) && !directorySourceChanged) {
if (this.directoryBootstrapInFlight.has(serverId) && !directorySourceChanged) {
return;
}
@@ -2011,25 +2005,28 @@ export class HostRuntimeStore {
serverId,
subscribe: { subscriptionId: `app:${serverId}` },
page: { limit: DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT },
}).catch((error) => {
console.error("[HostRuntime] agent directory bootstrap failed", {
serverId,
error: toErrorMessage(error),
});
}),
this.refreshWorkspaceDirectory({ serverId, subscribe: true }),
]),
this.refreshWorkspaceDirectory({ serverId, subscribe: true }).catch((error) => {
console.error("[HostRuntime] workspace directory bootstrap failed", {
serverId,
error: toErrorMessage(error),
});
}),
]).then(() => undefined),
)
.then(() => undefined)
.catch((error) => {
console.error("[HostRuntime] agent directory bootstrap failed", {
serverId,
error: toErrorMessage(error),
});
})
.finally(() => {
const inFlight = this.agentDirectoryBootstrapInFlight.get(serverId);
const inFlight = this.directoryBootstrapInFlight.get(serverId);
if (inFlight === bootstrap) {
this.agentDirectoryBootstrapInFlight.delete(serverId);
this.directoryBootstrapInFlight.delete(serverId);
}
});
this.agentDirectoryBootstrapInFlight.set(serverId, bootstrap);
this.directoryBootstrapInFlight.set(serverId, bootstrap);
}
drainQueuedAgentMessage(serverId: string, agentId: string): void {
@@ -4,6 +4,7 @@ import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import {
composeWorkspaceStructure,
selectHasHydratedWorkspaces,
selectHydratedWorkspaceServerIds,
selectHasWorkspaces,
selectProjectOrder,
selectRecommendedProjectPaths,
@@ -70,6 +71,14 @@ export function useHasHydratedWorkspaces(serverId: string | null): boolean {
);
}
export function useHydratedWorkspaceServerIds(serverIds: string[]): string[] {
return useStoreWithEqualityFn(
useSessionStore,
(state) => selectHydratedWorkspaceServerIds(state, serverIds),
workspaceEqualityFns.deep,
);
}
export function useWorkspaceDirectory(
serverId: string | null,
workspaceId: string | null,
@@ -4,6 +4,7 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import {
composeWorkspaceStructure,
selectHasWorkspaces,
selectHydratedWorkspaceServerIds,
selectProjectOrder,
selectRecommendedProjectPaths,
selectWorkspace,
@@ -101,6 +102,75 @@ afterEach(() => {
useSessionStore.getState().clearSession(SERVER_ID);
});
describe("workspace replica authority", () => {
it("keeps a cached workspace addressable without publishing it as an authoritative directory", () => {
const cachedWorkspace = createWorkspace({ id: "cached-workspace" });
initializeWorkspaces([cachedWorkspace]);
const cachedServerIds = selectHydratedWorkspaceServerIds(useSessionStore.getState(), [
SERVER_ID,
]);
expect(selectWorkspace(useSessionStore.getState(), SERVER_ID, cachedWorkspace.id)).toBe(
cachedWorkspace,
);
expect(selectWorkspaceStructureProjects(useSessionStore.getState(), cachedServerIds)).toEqual(
[],
);
const authoritativeWorkspace = createWorkspace({
id: "authoritative-workspace",
projectId: "authoritative-project",
});
useSessionStore
.getState()
.setWorkspaces(SERVER_ID, new Map([[authoritativeWorkspace.id, authoritativeWorkspace]]));
useSessionStore.getState().setHasHydratedWorkspaces(SERVER_ID, true);
const hydratedServerIds = selectHydratedWorkspaceServerIds(useSessionStore.getState(), [
SERVER_ID,
]);
expect(
selectWorkspaceStructureProjects(useSessionStore.getState(), hydratedServerIds).map(
(project) => project.workspaceKeys,
),
).toEqual([[`${SERVER_ID}:${authoritativeWorkspace.id}`]]);
});
it("publishes each host to the workspace directory independently", () => {
const loadingServerId = "loading-server";
const hydratedServerId = "hydrated-server";
const loadingWorkspace = createWorkspace({ id: "loading-workspace" });
const hydratedWorkspace = createWorkspace({
id: "hydrated-workspace",
projectId: "hydrated-project",
});
const state = {
sessions: {
[loadingServerId]: {
hasHydratedWorkspaces: false,
workspaces: new Map([[loadingWorkspace.id, loadingWorkspace]]),
},
[hydratedServerId]: {
hasHydratedWorkspaces: true,
workspaces: new Map([[hydratedWorkspace.id, hydratedWorkspace]]),
},
},
};
const directoryServerIds = selectHydratedWorkspaceServerIds(state, [
loadingServerId,
hydratedServerId,
]);
const directoryProjects = selectWorkspaceStructureProjects(state, directoryServerIds);
expect(directoryServerIds).toEqual([hydratedServerId]);
expect(directoryProjects.map((project) => project.workspaceKeys)).toEqual([
[`${hydratedServerId}:${hydratedWorkspace.id}`],
]);
});
});
describe("selectWorkspace", () => {
it("resolves a descriptor when the route id matches workspace identity but not the map key", () => {
const workspace = createWorkspace({ id: "workspace-a" });
@@ -131,6 +131,13 @@ export function selectHasHydratedWorkspaces(
return serverId ? (state.sessions[serverId]?.hasHydratedWorkspaces ?? false) : false;
}
export function selectHydratedWorkspaceServerIds(
state: SessionsSnapshot,
serverIds: readonly string[],
): string[] {
return serverIds.filter((serverId) => state.sessions[serverId]?.hasHydratedWorkspaces === true);
}
export function selectWorkspaceStructureProjects(
state: SessionsSnapshot,
serverIds: readonly string[],