Keep legacy daemon agents attached to workspaces

Old daemons can report agents by cwd without workspace IDs. Build path-backed workspace rows for that compatibility path and reapply ownership to legacy snapshots so tab reconciliation keeps those agents visible.
This commit is contained in:
Mohamed Boudra
2026-06-18 22:41:48 +07:00
parent ef87b09146
commit 3d5059ab05
7 changed files with 852 additions and 86 deletions
+93 -50
View File
@@ -72,6 +72,10 @@ import { isNative } from "@/constants/platform";
import { useToast } from "@/contexts/toast-context";
import { toErrorMessage } from "@/utils/error-messages";
import { applyCheckoutStatusUpdateFromEvent } from "@/git/checkout-status-cache";
import {
applyLegacyDaemonWorkspaceOwnership,
backfillLegacyDaemonWorkspaceDirectoryIfEmpty,
} from "@/workspace/legacy-daemon-workspaces";
// Re-export types from session-store and draft-store for backward compatibility
export type { DraftInput } from "@/stores/draft-store";
@@ -114,6 +118,56 @@ interface BufferedAudioChunk {
id: string;
}
interface WorkspaceHydrationSnapshot {
workspaces: Map<string, WorkspaceDescriptor>;
emptyProjects: Map<string, EmptyProjectDescriptor>;
}
async function fetchWorkspaceHydrationSnapshot(input: {
client: DaemonClient;
serverId: string;
subscribe: boolean;
isCancelled?: () => boolean;
}): Promise<WorkspaceHydrationSnapshot | null> {
const workspaces = new Map<string, WorkspaceDescriptor>();
const emptyProjects = new Map<string, EmptyProjectDescriptor>();
let cursor: string | null = null;
let includeSubscribe = input.subscribe;
while (true) {
const payload = await input.client.fetchWorkspaces({
sort: [{ key: "activity_at", direction: "desc" }],
...(includeSubscribe ? { subscribe: {} } : {}),
page: cursor ? { limit: 200, cursor } : { limit: 200 },
});
if (input.isCancelled?.()) {
return null;
}
for (const entry of payload.entries) {
const workspace = normalizeWorkspaceDescriptor(entry);
if (shouldSuppressWorkspaceForLocalArchive({ serverId: input.serverId, workspace })) {
continue;
}
workspaces.set(workspace.id, workspace);
}
// Empty project parents only ride on the first page.
for (const project of payload.emptyProjects ?? []) {
const descriptor = normalizeEmptyProjectDescriptor(project);
emptyProjects.set(descriptor.projectId, descriptor);
}
if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {
break;
}
cursor = payload.pageInfo.nextCursor;
includeSubscribe = false;
}
return { workspaces, emptyProjects };
}
function decodeBase64Chunk(base64: string): Uint8Array {
return Buffer.from(base64, "base64");
}
@@ -543,48 +597,29 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
return;
}
const workspaces = new Map<string, WorkspaceDescriptor>();
const emptyProjects = new Map<string, EmptyProjectDescriptor>();
let cursor: string | null = null;
let includeSubscribe = options?.subscribe ?? false;
while (true) {
const payload = await client.fetchWorkspaces({
sort: [{ key: "activity_at", direction: "desc" }],
...(includeSubscribe ? { subscribe: {} } : {}),
page: cursor ? { limit: 200, cursor } : { limit: 200 },
});
if (options?.isCancelled?.()) {
return;
}
for (const entry of payload.entries) {
const workspace = normalizeWorkspaceDescriptor(entry);
if (shouldSuppressWorkspaceForLocalArchive({ serverId, workspace })) {
continue;
}
workspaces.set(workspace.id, workspace);
}
// Empty project parents only ride on the first page.
for (const project of payload.emptyProjects ?? []) {
const descriptor = normalizeEmptyProjectDescriptor(project);
emptyProjects.set(descriptor.projectId, descriptor);
}
if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {
break;
}
cursor = payload.pageInfo.nextCursor;
includeSubscribe = false;
}
if (options?.isCancelled?.()) {
const snapshot = await fetchWorkspaceHydrationSnapshot({
client,
serverId,
subscribe: options?.subscribe ?? false,
isCancelled: options?.isCancelled,
});
if (!snapshot || options?.isCancelled?.()) {
return;
}
setWorkspaces(serverId, workspaces);
setEmptyProjects(serverId, emptyProjects.values());
const didBackfillLegacy = await backfillLegacyDaemonWorkspaceDirectoryIfEmpty({
client,
serverId,
workspaces: snapshot.workspaces,
emptyProjects: snapshot.emptyProjects,
isCancelled: options?.isCancelled,
});
if (didBackfillLegacy) {
return;
}
setWorkspaces(serverId, snapshot.workspaces);
setEmptyProjects(serverId, snapshot.emptyProjects.values());
setHasHydratedWorkspaces(serverId, true);
},
[client, isConnected, serverId, setEmptyProjects, setHasHydratedWorkspaces, setWorkspaces],
@@ -1044,13 +1079,16 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
const normalized = normalizeAgentSnapshot(update.agent, serverId);
const agent = {
...normalized,
projectPlacement: resolveProjectPlacement({
projectPlacement: update.project,
cwd: normalized.cwd,
}),
};
const agent = applyLegacyDaemonWorkspaceOwnership({
serverId,
agent: {
...normalized,
projectPlacement: resolveProjectPlacement({
projectPlacement: update.project,
cwd: normalized.cwd,
}),
},
});
applyAuthoritativeAgentSnapshot(agent);
},
@@ -1110,10 +1148,15 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
if (payload.agent) {
const normalized = normalizeAgentSnapshot(payload.agent, serverId);
applyAuthoritativeAgentSnapshot({
...normalized,
projectPlacement: session?.agents.get(agentId)?.projectPlacement ?? null,
});
applyAuthoritativeAgentSnapshot(
applyLegacyDaemonWorkspaceOwnership({
serverId,
agent: {
...normalized,
projectPlacement: session?.agents.get(agentId)?.projectPlacement ?? null,
},
}),
);
}
// Call pure reducer