Reduce workspace, agent, and chat sync traffic (#2028)
* feat(sync): keep live data scoped and current Only viewed chats receive live timeline rows, while directory state now uses one subscribed bootstrap followed by ordered deltas. Legacy clients and daemons retain their existing behavior through centralized compatibility gates. * fix(sync): preserve selective delivery boundaries Keep selective timeline capability app-owned, union viewed sets across shared sockets, and reconcile directory side effects from accepted state. Visibility and archive suppression now follow their existing authoritative boundaries. * test(app): align browser fixtures with runtime contracts * fix(sync): preserve mixed-client delivery guarantees * fix(sync): finish paged history before advancing * fix(sync): replay live deltas after refresh failures * test(server): model socket capability lookup * test(app): use platform shortcut for split-pane coverage * fix(app): keep hidden timelines dormant * test(app): normalize split-pane shortcut setup * fix(app): preserve sync state across retries * fix(app): preserve background sync state across races * fix directory bootstrap reconciliation edge cases * fix selective timeline compatibility races * fix superseded directory sync races * fix(sync): centralize directory replica ordering Own agent and workspace refresh transactions in HostRuntime so reconnect epochs, buffered updates, and lifecycle invalidation share one ordering boundary. Route timeline metadata and mixed-capability stream delivery through their authoritative runtime sources. * fix(app): keep timeline requests runtime-scoped * fix(app): close directory bootstrap races
This commit is contained in:
@@ -4,7 +4,6 @@ import equal from "fast-deep-equal/es6";
|
||||
import {
|
||||
DaemonClient,
|
||||
type ConnectionState,
|
||||
type FetchAgentsEntry,
|
||||
type FetchAgentsOptions,
|
||||
} from "@getpaseo/client/internal/daemon-client";
|
||||
import {
|
||||
@@ -40,12 +39,7 @@ import {
|
||||
import { getDesktopHost } from "@/desktop/host";
|
||||
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
|
||||
import { BROWSER_AUTOMATION_COMMAND_NAMES } from "@getpaseo/protocol/browser-automation/rpc-schemas";
|
||||
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 "@/data/query-client";
|
||||
import {
|
||||
@@ -54,6 +48,13 @@ import {
|
||||
} from "@/data/push-router";
|
||||
import { mountBrowserAutomationDaemonClientHandler } from "@/browser-automation/handler";
|
||||
import { schedulesQueryBaseKey } from "@/schedules/aggregated-schedules";
|
||||
import { sendQueuedComposerMessageNow } from "@/composer/actions";
|
||||
import {
|
||||
resolveComposerAttachmentSubmitFormat,
|
||||
splitComposerAttachmentsForSubmit,
|
||||
} from "@/composer/attachments/submit";
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync";
|
||||
|
||||
export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error";
|
||||
export type HostRegistryStatus = "loading" | "ready";
|
||||
@@ -85,6 +86,7 @@ export interface HostRuntimeSnapshot {
|
||||
hasEverLoadedAgentDirectory: boolean;
|
||||
probeByConnectionId: Map<string, ConnectionProbeState>;
|
||||
clientGeneration: number;
|
||||
connectionEpoch: number;
|
||||
}
|
||||
|
||||
type HostRuntimeSnapshotPatch = Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>;
|
||||
@@ -176,90 +178,6 @@ const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
|
||||
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
||||
const CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS = 1_000;
|
||||
|
||||
const DEFAULT_AGENT_DIRECTORY_SORT: NonNullable<FetchAgentsOptions["sort"]> = [
|
||||
{ key: "updated_at", direction: "desc" },
|
||||
];
|
||||
|
||||
function readFetchAgentsHasMore(
|
||||
pageInfo: Awaited<ReturnType<DaemonClient["fetchAgents"]>>["pageInfo"],
|
||||
): boolean {
|
||||
const page = pageInfo as {
|
||||
hasMore?: boolean;
|
||||
hasMoreAfter?: boolean;
|
||||
};
|
||||
if (typeof page.hasMore === "boolean") {
|
||||
return page.hasMore;
|
||||
}
|
||||
if (typeof page.hasMoreAfter === "boolean") {
|
||||
return page.hasMoreAfter;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function readFetchAgentsNextCursor(
|
||||
pageInfo: Awaited<ReturnType<DaemonClient["fetchAgents"]>>["pageInfo"],
|
||||
): string | null {
|
||||
const page = pageInfo as {
|
||||
nextCursor?: string | null;
|
||||
afterCursor?: string | null;
|
||||
};
|
||||
if (typeof page.nextCursor === "string" && page.nextCursor.length > 0) {
|
||||
return page.nextCursor;
|
||||
}
|
||||
if (typeof page.afterCursor === "string" && page.afterCursor.length > 0) {
|
||||
return page.afterCursor;
|
||||
}
|
||||
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 {
|
||||
@@ -445,9 +363,15 @@ function nextConnectionMachineState(input: {
|
||||
|
||||
function toSnapshotConnectionPatch(
|
||||
state: HostRuntimeConnectionMachineState,
|
||||
connectionEpoch: number,
|
||||
): Pick<
|
||||
HostRuntimeSnapshot,
|
||||
"activeConnectionId" | "activeConnection" | "connectionStatus" | "lastError" | "lastOnlineAt"
|
||||
| "activeConnectionId"
|
||||
| "activeConnection"
|
||||
| "connectionStatus"
|
||||
| "lastError"
|
||||
| "lastOnlineAt"
|
||||
| "connectionEpoch"
|
||||
> {
|
||||
if (state.tag === "booting") {
|
||||
return {
|
||||
@@ -456,6 +380,7 @@ function toSnapshotConnectionPatch(
|
||||
connectionStatus: "connecting",
|
||||
lastError: null,
|
||||
lastOnlineAt: null,
|
||||
connectionEpoch,
|
||||
};
|
||||
}
|
||||
if (state.tag === "connecting") {
|
||||
@@ -465,6 +390,7 @@ function toSnapshotConnectionPatch(
|
||||
connectionStatus: "connecting",
|
||||
lastError: null,
|
||||
lastOnlineAt: null,
|
||||
connectionEpoch,
|
||||
};
|
||||
}
|
||||
if (state.tag === "online") {
|
||||
@@ -474,6 +400,7 @@ function toSnapshotConnectionPatch(
|
||||
connectionStatus: "online",
|
||||
lastError: null,
|
||||
lastOnlineAt: state.lastOnlineAt,
|
||||
connectionEpoch,
|
||||
};
|
||||
}
|
||||
if (state.tag === "offline") {
|
||||
@@ -483,6 +410,7 @@ function toSnapshotConnectionPatch(
|
||||
connectionStatus: "offline",
|
||||
lastError: null,
|
||||
lastOnlineAt: null,
|
||||
connectionEpoch,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -491,6 +419,7 @@ function toSnapshotConnectionPatch(
|
||||
connectionStatus: "error",
|
||||
lastError: state.message,
|
||||
lastOnlineAt: null,
|
||||
connectionEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -538,6 +467,10 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const appCapabilities = {
|
||||
[CLIENT_CAPS.selectiveAgentTimeline]: true,
|
||||
...browserAutomationCapabilities,
|
||||
};
|
||||
|
||||
return {
|
||||
createClient: ({ host, connection, clientId, runtimeGeneration }) => {
|
||||
@@ -548,7 +481,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
clientType: "mobile" as const,
|
||||
appVersion: resolveAppVersion() ?? undefined,
|
||||
runtimeGeneration,
|
||||
...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}),
|
||||
capabilities: appCapabilities,
|
||||
};
|
||||
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
||||
return new DaemonClient({
|
||||
@@ -586,7 +519,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
connectToDaemon(connection, {
|
||||
...(host.serverId ? { serverId: host.serverId } : {}),
|
||||
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
||||
...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}),
|
||||
capabilities: appCapabilities,
|
||||
}),
|
||||
getClientId: () => getOrCreateClientId(),
|
||||
mountClientHandlers: ({ client, host }) => {
|
||||
@@ -614,6 +547,7 @@ export class HostRuntimeController {
|
||||
private deps: HostRuntimeControllerDeps;
|
||||
private onReconcileServerId: ((oldId: string, newId: string) => void) | null;
|
||||
private connectionMachineState: HostRuntimeConnectionMachineState;
|
||||
private connectionEpoch = 0;
|
||||
private snapshot: HostRuntimeSnapshot;
|
||||
private listeners = new Set<() => void>();
|
||||
private activeClient: DaemonClient | null = null;
|
||||
@@ -644,7 +578,7 @@ export class HostRuntimeController {
|
||||
};
|
||||
this.snapshot = {
|
||||
serverId: this.host.serverId,
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
client: null,
|
||||
agentDirectoryStatus: "idle",
|
||||
agentDirectoryError: null,
|
||||
@@ -712,7 +646,7 @@ export class HostRuntimeController {
|
||||
}
|
||||
this.applyConnectionEvent({ type: "stopped" });
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
client: null,
|
||||
});
|
||||
}
|
||||
@@ -776,7 +710,7 @@ export class HostRuntimeController {
|
||||
markStartupError(message: string): void {
|
||||
this.applyConnectionEvent({ type: "connect_failed", message });
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -809,7 +743,7 @@ export class HostRuntimeController {
|
||||
}
|
||||
this.applyConnectionEvent({ type: "no_connections" });
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
probeByConnectionId: new Map(),
|
||||
});
|
||||
return;
|
||||
@@ -1095,6 +1029,9 @@ export class HostRuntimeController {
|
||||
state: previousState,
|
||||
event,
|
||||
});
|
||||
if (previousState.tag !== "online" && nextState.tag === "online") {
|
||||
this.connectionEpoch += 1;
|
||||
}
|
||||
this.connectionMachineState = nextState;
|
||||
this.logConnectionTransition({
|
||||
from: previousState.tag,
|
||||
@@ -1171,7 +1108,7 @@ export class HostRuntimeController {
|
||||
message: `Failed to resolve client id: ${message}`,
|
||||
});
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -1269,7 +1206,7 @@ export class HostRuntimeController {
|
||||
this.snapshot = {
|
||||
...this.snapshot,
|
||||
serverId: this.host.serverId,
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
client,
|
||||
clientGeneration: nextGeneration,
|
||||
};
|
||||
@@ -1287,7 +1224,7 @@ export class HostRuntimeController {
|
||||
lastError: client.lastError,
|
||||
});
|
||||
const patch: HostRuntimeSnapshotPatch = {
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
...this.buildAgentDirectoryStatusPatch(),
|
||||
};
|
||||
this.updateSnapshot(patch);
|
||||
@@ -1307,7 +1244,7 @@ export class HostRuntimeController {
|
||||
message,
|
||||
});
|
||||
this.updateSnapshot({
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState),
|
||||
...toSnapshotConnectionPatch(this.connectionMachineState, this.connectionEpoch),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1393,6 +1330,13 @@ function rekeyMap<V>(map: Map<string, V>, oldKey: string, newKey: string): void
|
||||
map.set(newKey, value);
|
||||
}
|
||||
|
||||
interface AgentDirectoryRefreshInput {
|
||||
serverId: string;
|
||||
filter?: FetchAgentsOptions["filter"];
|
||||
subscribe?: FetchAgentsOptions["subscribe"];
|
||||
page?: FetchAgentsOptions["page"];
|
||||
}
|
||||
|
||||
export class HostRuntimeStore {
|
||||
private controllers = new Map<string, HostRuntimeController>();
|
||||
private serverListeners = new Map<string, Set<() => void>>();
|
||||
@@ -1406,6 +1350,8 @@ export class HostRuntimeStore {
|
||||
private deps: HostRuntimeControllerDeps;
|
||||
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
|
||||
private agentDirectoryBootstrapInFlight = new Map<string, Promise<void>>();
|
||||
private queuedAgentDrainInFlight = new Set<string>();
|
||||
private directorySyncByServer = new Map<string, DirectorySync>();
|
||||
private configuredOverrideBootstrapInFlight: Promise<void> | null = null;
|
||||
private bootStarted = false;
|
||||
private storage: HostRuntimeStorage;
|
||||
@@ -1636,6 +1582,27 @@ export class HostRuntimeStore {
|
||||
|
||||
rekeyMap(this.lastConnectionStatusByServer, oldServerId, newServerId);
|
||||
rekeyMap(this.agentDirectoryBootstrapInFlight, 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();
|
||||
},
|
||||
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
|
||||
});
|
||||
this.directorySyncByServer.set(newServerId, directory);
|
||||
const snapshot = controller.getSnapshot();
|
||||
directory.connectionChanged({
|
||||
client: snapshot.client,
|
||||
status: snapshot.connectionStatus === "online" ? "online" : "offline",
|
||||
source: {
|
||||
clientGeneration: snapshot.clientGeneration,
|
||||
connectionEpoch: snapshot.connectionEpoch,
|
||||
},
|
||||
});
|
||||
|
||||
const listeners = this.serverListeners.get(oldServerId);
|
||||
if (listeners) {
|
||||
@@ -1926,6 +1893,8 @@ export class HostRuntimeStore {
|
||||
this.controllers.delete(serverId);
|
||||
this.lastConnectionStatusByServer.delete(serverId);
|
||||
this.agentDirectoryBootstrapInFlight.delete(serverId);
|
||||
this.directorySyncByServer.get(serverId)?.dispose();
|
||||
this.directorySyncByServer.delete(serverId);
|
||||
void controller.stop();
|
||||
this.emit(serverId);
|
||||
}
|
||||
@@ -1948,6 +1917,19 @@ export class HostRuntimeStore {
|
||||
onReconcileServerId: (oldId, newId) => this.reconcileServerId(oldId, newId),
|
||||
});
|
||||
this.controllers.set(host.serverId, controller);
|
||||
this.directorySyncByServer.set(
|
||||
host.serverId,
|
||||
new DirectorySync(host.serverId, {
|
||||
drainQueuedAgentMessage: (agentId) =>
|
||||
this.drainQueuedAgentMessage(host.serverId, agentId),
|
||||
markAgentLoading: () => controller.markAgentDirectorySyncLoading(),
|
||||
markAgentReady: () => {
|
||||
this.agentDirectoryBootstrapInFlight.delete(host.serverId);
|
||||
controller.markAgentDirectorySyncReady();
|
||||
},
|
||||
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
|
||||
}),
|
||||
);
|
||||
this.lastConnectionStatusByServer.set(
|
||||
host.serverId,
|
||||
controller.getSnapshot().connectionStatus,
|
||||
@@ -1980,6 +1962,15 @@ export class HostRuntimeStore {
|
||||
return;
|
||||
}
|
||||
const snapshot = controller.getSnapshot();
|
||||
const directorySourceChanged =
|
||||
this.directorySyncByServer.get(serverId)?.connectionChanged({
|
||||
client: snapshot.client,
|
||||
status: snapshot.connectionStatus === "online" ? "online" : "offline",
|
||||
source: {
|
||||
clientGeneration: snapshot.clientGeneration,
|
||||
connectionEpoch: snapshot.connectionEpoch,
|
||||
},
|
||||
}) ?? false;
|
||||
const previousStatus = this.lastConnectionStatusByServer.get(serverId);
|
||||
this.lastConnectionStatusByServer.set(serverId, snapshot.connectionStatus);
|
||||
const didTransitionOnline =
|
||||
@@ -2002,17 +1993,20 @@ export class HostRuntimeStore {
|
||||
if (!didTransitionOnline && snapshot.hasEverLoadedAgentDirectory) {
|
||||
return;
|
||||
}
|
||||
if (this.agentDirectoryBootstrapInFlight.has(serverId)) {
|
||||
if (this.agentDirectoryBootstrapInFlight.has(serverId) && !directorySourceChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bootstrap = Promise.resolve()
|
||||
.then(() =>
|
||||
this.refreshAgentDirectory({
|
||||
serverId,
|
||||
subscribe: { subscriptionId: `app:${serverId}` },
|
||||
page: { limit: DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT },
|
||||
}),
|
||||
Promise.all([
|
||||
this.refreshAgentDirectory({
|
||||
serverId,
|
||||
subscribe: { subscriptionId: `app:${serverId}` },
|
||||
page: { limit: DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT },
|
||||
}),
|
||||
this.refreshWorkspaceDirectory({ serverId, subscribe: true }),
|
||||
]),
|
||||
)
|
||||
.then(() => undefined)
|
||||
.catch((error) => {
|
||||
@@ -2031,6 +2025,55 @@ export class HostRuntimeStore {
|
||||
this.agentDirectoryBootstrapInFlight.set(serverId, bootstrap);
|
||||
}
|
||||
|
||||
drainQueuedAgentMessage(serverId: string, agentId: string): void {
|
||||
const drainKey = `${serverId}:${agentId}`;
|
||||
if (this.queuedAgentDrainInFlight.has(drainKey)) return;
|
||||
const store = useSessionStore.getState();
|
||||
const session = store.sessions[serverId];
|
||||
const queue = session?.queuedMessages.get(agentId);
|
||||
const client = session?.client;
|
||||
if (!client || !queue?.length || session.initializingAgents.get(agentId) === true) {
|
||||
return;
|
||||
}
|
||||
this.queuedAgentDrainInFlight.add(drainKey);
|
||||
const next = queue[0];
|
||||
void sendQueuedComposerMessageNow({
|
||||
agentId,
|
||||
messageId: next.id,
|
||||
queue: {
|
||||
read: (queuedAgentId) =>
|
||||
useSessionStore.getState().sessions[serverId]?.queuedMessages.get(queuedAgentId) ?? [],
|
||||
write: (update) => useSessionStore.getState().setQueuedMessages(serverId, update),
|
||||
},
|
||||
submitMessage: async ({ text, attachments }) => {
|
||||
const supportsForgeAttachments =
|
||||
useSessionStore.getState().sessions[serverId]?.serverInfo?.features?.forgeSearch === true;
|
||||
const wirePayload = splitComposerAttachmentsForSubmit(attachments, {
|
||||
format: resolveComposerAttachmentSubmitFormat({ supportsForgeAttachments }),
|
||||
});
|
||||
const images = await encodeImages(wirePayload.images);
|
||||
await client.sendAgentMessage(agentId, text, {
|
||||
messageId: next.id,
|
||||
...(images && images.length > 0 ? { images } : {}),
|
||||
attachments: wirePayload.attachments,
|
||||
});
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.status === "failed") {
|
||||
console.error("[HostRuntime] failed to drain queued agent message", {
|
||||
serverId,
|
||||
agentId,
|
||||
error: result.errorMessage,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.finally(() => {
|
||||
this.queuedAgentDrainInFlight.delete(drainKey);
|
||||
});
|
||||
}
|
||||
|
||||
getSnapshot(serverId: string): HostRuntimeSnapshot | null {
|
||||
return this.controllers.get(serverId)?.getSnapshot() ?? null;
|
||||
}
|
||||
@@ -2095,63 +2138,34 @@ export class HostRuntimeStore {
|
||||
).then(() => undefined);
|
||||
}
|
||||
|
||||
async refreshAgentDirectory(input: {
|
||||
serverId: string;
|
||||
filter?: FetchAgentsOptions["filter"];
|
||||
subscribe?: FetchAgentsOptions["subscribe"];
|
||||
page?: FetchAgentsOptions["page"];
|
||||
}): Promise<{
|
||||
agents: ReturnType<typeof replaceFetchedAgentDirectory>["agents"];
|
||||
subscriptionId: string | null;
|
||||
}> {
|
||||
const controller = this.controllers.get(input.serverId);
|
||||
if (!controller) {
|
||||
throw new Error(`Unknown host runtime for serverId ${input.serverId}`);
|
||||
}
|
||||
const snapshot = controller.getSnapshot();
|
||||
const client = controller.getClient();
|
||||
if (!client || snapshot.connectionStatus !== "online") {
|
||||
throw new Error(`Host ${input.serverId} is not connected`);
|
||||
}
|
||||
async refreshAgentDirectory(
|
||||
input: AgentDirectoryRefreshInput,
|
||||
): Promise<RefreshAgentDirectoryResult> {
|
||||
const directory = this.directorySyncByServer.get(input.serverId);
|
||||
if (!directory) throw new Error(`Unknown host runtime for serverId ${input.serverId}`);
|
||||
return directory.refreshAgents(input);
|
||||
}
|
||||
|
||||
controller.markAgentDirectorySyncLoading();
|
||||
try {
|
||||
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,
|
||||
});
|
||||
controller.markAgentDirectorySyncReady();
|
||||
return {
|
||||
agents: result.agents,
|
||||
subscriptionId: result.subscriptionId,
|
||||
};
|
||||
}
|
||||
async refreshWorkspaceDirectory(input: { serverId: string; subscribe?: boolean }): Promise<void> {
|
||||
const directory = this.directorySyncByServer.get(input.serverId);
|
||||
if (!directory) throw new Error(`Unknown host runtime for serverId ${input.serverId}`);
|
||||
await directory.refreshWorkspaces({ subscribe: input.subscribe });
|
||||
}
|
||||
|
||||
const directory = await fetchCurrentAgentDirectory({
|
||||
client,
|
||||
filter: input.filter,
|
||||
subscribe: input.subscribe,
|
||||
page: input.page,
|
||||
});
|
||||
async refreshDirectories(serverId: string): Promise<void> {
|
||||
const directory = this.directorySyncByServer.get(serverId);
|
||||
if (!directory) throw new Error(`Unknown host runtime for serverId ${serverId}`);
|
||||
await directory.refreshAll();
|
||||
}
|
||||
|
||||
const { agents } = replaceFetchedAgentDirectory({
|
||||
serverId: input.serverId,
|
||||
entries: directory.entries,
|
||||
});
|
||||
|
||||
controller.markAgentDirectorySyncReady();
|
||||
return {
|
||||
agents,
|
||||
subscriptionId: directory.subscriptionId,
|
||||
};
|
||||
} catch (error) {
|
||||
controller.markAgentDirectorySyncError(toErrorMessage(error));
|
||||
throw error;
|
||||
}
|
||||
fetchAgentTimeline(
|
||||
serverId: string,
|
||||
agentId: string,
|
||||
request: Parameters<DaemonClient["fetchAgentTimeline"]>[1],
|
||||
): Promise<Awaited<ReturnType<DaemonClient["fetchAgentTimeline"]>>> {
|
||||
const directory = this.directorySyncByServer.get(serverId);
|
||||
if (!directory) throw new Error(`Unknown host runtime for serverId ${serverId}`);
|
||||
return directory.fetchTimeline(agentId, request);
|
||||
}
|
||||
|
||||
refreshAllAgentDirectories(input?: { serverIds?: string[] }): void {
|
||||
|
||||
Reference in New Issue
Block a user