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:
@@ -1264,6 +1264,83 @@ describe("HostRuntimeStore", () => {
|
||||
store.syncHosts([]);
|
||||
});
|
||||
|
||||
it("bootstraps legacy daemons from unscoped agents and creates path-backed workspaces", async () => {
|
||||
const host = makeHost({
|
||||
serverId: "srv_legacy_workspace_daemon",
|
||||
connections: [
|
||||
{
|
||||
id: "direct:lan:6767",
|
||||
type: "directTcp",
|
||||
endpoint: "lan:6767",
|
||||
},
|
||||
],
|
||||
});
|
||||
const fakeClient = new FakeDaemonClient();
|
||||
fakeClient.setConnectionState({ status: "connected" });
|
||||
fakeClient.fetchAgentsResponses.push(
|
||||
makeFetchAgentsPayload({
|
||||
entries: [
|
||||
makeFetchAgentsEntry({
|
||||
id: "agent-legacy",
|
||||
cwd: "/repo/legacy-app",
|
||||
updatedAt: "2026-06-18T12:00:00.000Z",
|
||||
title: "Legacy daemon agent",
|
||||
}),
|
||||
],
|
||||
subscriptionId: "app:srv_legacy_workspace_daemon",
|
||||
}),
|
||||
);
|
||||
const store = new HostRuntimeStore({
|
||||
deps: {
|
||||
createClient: () => fakeClient as unknown as DaemonClient,
|
||||
connectToDaemon: async ({ host: hostProfile }) => ({
|
||||
client: fakeClient as unknown as DaemonClient,
|
||||
serverId: hostProfile.serverId,
|
||||
hostname: hostProfile.label ?? null,
|
||||
}),
|
||||
getClientId: async () => "cid_test_runtime",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionStore = useSessionStore.getState();
|
||||
sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient);
|
||||
sessionStore.updateSessionServerInfo(host.serverId, {
|
||||
serverId: host.serverId,
|
||||
hostname: null,
|
||||
version: "0.1.96",
|
||||
});
|
||||
store.syncHosts([host]);
|
||||
|
||||
const timeoutAt = Date.now() + 300;
|
||||
while (
|
||||
(fakeClient.fetchAgentsCalls.length === 0 ||
|
||||
!useSessionStore.getState().sessions[host.serverId]?.workspaces.has("/repo/legacy-app")) &&
|
||||
Date.now() < timeoutAt
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
expect(fakeClient.fetchAgentsCalls).toEqual([
|
||||
{
|
||||
sort: [{ key: "updated_at", direction: "desc" }],
|
||||
subscribe: { subscriptionId: "app:srv_legacy_workspace_daemon" },
|
||||
page: { limit: 200 },
|
||||
},
|
||||
]);
|
||||
const session = useSessionStore.getState().sessions[host.serverId];
|
||||
expect(session?.agents.get("agent-legacy")?.workspaceId).toBe("/repo/legacy-app");
|
||||
expect(Array.from(session?.workspaces.values() ?? [])).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "/repo/legacy-app",
|
||||
workspaceDirectory: "/repo/legacy-app",
|
||||
name: "legacy-app",
|
||||
}),
|
||||
]);
|
||||
|
||||
store.syncHosts([]);
|
||||
useSessionStore.getState().clearSession(host.serverId);
|
||||
});
|
||||
|
||||
it("fetches all pages during bootstrap within the active agent scope", async () => {
|
||||
const host = makeHost({
|
||||
serverId: "srv_paged",
|
||||
|
||||
@@ -38,6 +38,10 @@ import {
|
||||
} from "@/desktop/daemon/desktop-daemon-transport";
|
||||
import { replaceFetchedAgentDirectory } from "@/utils/agent-directory-sync";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
fetchLegacyDaemonWorkspaceDirectory,
|
||||
shouldUseLegacyDaemonWorkspaceDirectory,
|
||||
} from "@/workspace/legacy-daemon-workspaces";
|
||||
import { invalidateCheckoutGitQueriesForServer } from "@/git/query-keys";
|
||||
import { queryClient } from "@/query/query-client";
|
||||
|
||||
@@ -187,6 +191,54 @@ function readFetchAgentsNextCursor(
|
||||
return null;
|
||||
}
|
||||
|
||||
interface AgentDirectoryFetchInput {
|
||||
client: DaemonClient;
|
||||
filter?: FetchAgentsOptions["filter"];
|
||||
subscribe?: FetchAgentsOptions["subscribe"];
|
||||
page?: FetchAgentsOptions["page"];
|
||||
}
|
||||
|
||||
interface AgentDirectoryFetchResult {
|
||||
entries: FetchAgentsEntry[];
|
||||
subscriptionId: string | null;
|
||||
}
|
||||
|
||||
async function fetchCurrentAgentDirectory(
|
||||
input: AgentDirectoryFetchInput,
|
||||
): Promise<AgentDirectoryFetchResult> {
|
||||
const pageLimit = input.page?.limit ?? DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT;
|
||||
let cursor = input.page?.cursor ?? null;
|
||||
let includeSubscribe = true;
|
||||
let subscriptionId: string | null = null;
|
||||
const entries: FetchAgentsEntry[] = [];
|
||||
|
||||
while (true) {
|
||||
const payload = await input.client.fetchAgents({
|
||||
scope: input.filter ? undefined : "active",
|
||||
...(input.filter ? { filter: input.filter } : {}),
|
||||
sort: DEFAULT_AGENT_DIRECTORY_SORT,
|
||||
...(includeSubscribe && input.subscribe ? { subscribe: input.subscribe } : {}),
|
||||
page: cursor ? { limit: pageLimit, cursor } : { limit: pageLimit },
|
||||
});
|
||||
|
||||
entries.push(...payload.entries);
|
||||
subscriptionId = subscriptionId ?? payload.subscriptionId ?? null;
|
||||
includeSubscribe = false;
|
||||
|
||||
if (!readFetchAgentsHasMore(payload.pageInfo)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextCursor = readFetchAgentsNextCursor(payload.pageInfo);
|
||||
if (!nextCursor) {
|
||||
break;
|
||||
}
|
||||
cursor = nextCursor;
|
||||
}
|
||||
|
||||
return { entries, subscriptionId };
|
||||
}
|
||||
|
||||
function toActiveConnection(connection: HostConnection): ActiveConnection {
|
||||
if (connection.type === "directSocket") {
|
||||
return {
|
||||
@@ -1912,46 +1964,37 @@ export class HostRuntimeStore {
|
||||
|
||||
controller.markAgentDirectorySyncLoading();
|
||||
try {
|
||||
const pageLimit = input.page?.limit ?? DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT;
|
||||
let cursor = input.page?.cursor ?? null;
|
||||
let includeSubscribe = true;
|
||||
let subscriptionId: string | null = null;
|
||||
const allEntries: FetchAgentsEntry[] = [];
|
||||
|
||||
while (true) {
|
||||
const payload = await client.fetchAgents({
|
||||
scope: input.filter ? undefined : "active",
|
||||
...(input.filter ? { filter: input.filter } : {}),
|
||||
sort: DEFAULT_AGENT_DIRECTORY_SORT,
|
||||
...(includeSubscribe && input.subscribe ? { subscribe: input.subscribe } : {}),
|
||||
page: cursor ? { limit: pageLimit, cursor } : { limit: pageLimit },
|
||||
const session = useSessionStore.getState().sessions[input.serverId];
|
||||
if (!input.filter && shouldUseLegacyDaemonWorkspaceDirectory(session?.serverInfo)) {
|
||||
const result = await fetchLegacyDaemonWorkspaceDirectory({
|
||||
client,
|
||||
serverId: input.serverId,
|
||||
subscribe: input.subscribe,
|
||||
page: input.page,
|
||||
});
|
||||
|
||||
allEntries.push(...payload.entries);
|
||||
|
||||
subscriptionId = subscriptionId ?? payload.subscriptionId ?? null;
|
||||
includeSubscribe = false;
|
||||
|
||||
if (!readFetchAgentsHasMore(payload.pageInfo)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextCursor = readFetchAgentsNextCursor(payload.pageInfo);
|
||||
if (!nextCursor) {
|
||||
break;
|
||||
}
|
||||
cursor = nextCursor;
|
||||
controller.markAgentDirectorySyncReady();
|
||||
return {
|
||||
agents: result.agents,
|
||||
subscriptionId: result.subscriptionId,
|
||||
};
|
||||
}
|
||||
|
||||
const directory = await fetchCurrentAgentDirectory({
|
||||
client,
|
||||
filter: input.filter,
|
||||
subscribe: input.subscribe,
|
||||
page: input.page,
|
||||
});
|
||||
|
||||
const { agents } = replaceFetchedAgentDirectory({
|
||||
serverId: input.serverId,
|
||||
entries: allEntries,
|
||||
entries: directory.entries,
|
||||
});
|
||||
|
||||
controller.markAgentDirectorySyncReady();
|
||||
return {
|
||||
agents,
|
||||
subscriptionId,
|
||||
subscriptionId: directory.subscriptionId,
|
||||
};
|
||||
} catch (error) {
|
||||
controller.markAgentDirectorySyncError(toErrorMessage(error));
|
||||
|
||||
Reference in New Issue
Block a user