feat: implement daemon connectivity updates and relay protocol versioning
This commit is contained in:
@@ -1,212 +1,93 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useMemo, useSyncExternalStore } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
getHostRuntimeStore,
|
||||
type HostRuntimeAgentDirectoryStatus,
|
||||
type HostRuntimeConnectionStatus,
|
||||
} from "@/runtime/host-runtime";
|
||||
import { useDaemonRegistry, type HostProfile } from "./daemon-registry-context";
|
||||
|
||||
export type ActiveConnection =
|
||||
| { type: "direct"; endpoint: string; display: string }
|
||||
| { type: "relay"; endpoint: string; display: "relay" };
|
||||
|
||||
export type ConnectionState =
|
||||
| { status: "idle"; activeConnection: ActiveConnection | null; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: false }
|
||||
| { status: "connecting"; activeConnection: ActiveConnection | null; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
|
||||
| { status: "online"; activeConnection: ActiveConnection | null; lastError: null; lastOnlineAt: string; agentListReady: boolean; hasEverReceivedAgentList: boolean }
|
||||
| { status: "offline"; activeConnection: ActiveConnection | null; lastError: string | null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
|
||||
| { status: "error"; activeConnection: ActiveConnection | null; lastError: string; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean };
|
||||
|
||||
export type ConnectionStatus = ConnectionState["status"];
|
||||
|
||||
export type ConnectionStateUpdate =
|
||||
| { status: "idle" }
|
||||
| { status: "connecting"; activeConnection?: ActiveConnection | null; lastOnlineAt?: string | null }
|
||||
| { status: "online"; activeConnection?: ActiveConnection | null; lastOnlineAt: string }
|
||||
| { status: "offline"; activeConnection?: ActiveConnection | null; lastError?: string | null; lastOnlineAt?: string | null }
|
||||
| { status: "error"; activeConnection?: ActiveConnection | null; lastError: string; lastOnlineAt?: string | null };
|
||||
export type ConnectionStatus = HostRuntimeConnectionStatus;
|
||||
|
||||
export type DaemonConnectionRecord = {
|
||||
daemon: HostProfile;
|
||||
} & ConnectionState;
|
||||
status: HostRuntimeConnectionStatus;
|
||||
activeConnection: ActiveConnection | null;
|
||||
lastError: string | null;
|
||||
lastOnlineAt: string | null;
|
||||
agentDirectoryStatus: HostRuntimeAgentDirectoryStatus;
|
||||
agentDirectoryError: string | null;
|
||||
hasEverLoadedAgentDirectory: boolean;
|
||||
};
|
||||
|
||||
interface DaemonConnectionsContextValue {
|
||||
connectionStates: Map<string, DaemonConnectionRecord>;
|
||||
isLoading: boolean;
|
||||
updateConnectionStatus: (serverId: string, update: ConnectionStateUpdate) => void;
|
||||
markAgentListReady: (serverId: string, ready: boolean) => void;
|
||||
}
|
||||
|
||||
const DaemonConnectionsContext = createContext<DaemonConnectionsContextValue | null>(null);
|
||||
|
||||
function createDefaultConnectionState(): ConnectionState {
|
||||
return {
|
||||
status: "idle",
|
||||
activeConnection: null,
|
||||
lastError: null,
|
||||
lastOnlineAt: null,
|
||||
agentListReady: false,
|
||||
hasEverReceivedAgentList: false,
|
||||
};
|
||||
}
|
||||
function buildConnectionStates(input: {
|
||||
daemons: HostProfile[];
|
||||
runtime: ReturnType<typeof getHostRuntimeStore>;
|
||||
}): Map<string, DaemonConnectionRecord> {
|
||||
const { daemons, runtime } = input;
|
||||
const next = new Map<string, DaemonConnectionRecord>();
|
||||
|
||||
function resolveNextConnectionState(
|
||||
existing: ConnectionState,
|
||||
update: ConnectionStateUpdate
|
||||
): ConnectionState {
|
||||
switch (update.status) {
|
||||
case "idle":
|
||||
return {
|
||||
status: "idle",
|
||||
activeConnection: existing.activeConnection ?? null,
|
||||
lastError: null,
|
||||
lastOnlineAt: existing.lastOnlineAt,
|
||||
agentListReady: false,
|
||||
hasEverReceivedAgentList: false,
|
||||
};
|
||||
case "connecting":
|
||||
return {
|
||||
status: "connecting",
|
||||
activeConnection: update.activeConnection ?? existing.activeConnection ?? null,
|
||||
lastError: null,
|
||||
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||
agentListReady: false,
|
||||
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
|
||||
};
|
||||
case "online":
|
||||
const currentAgentListReady =
|
||||
existing.status === "online" ? existing.agentListReady : false;
|
||||
|
||||
return {
|
||||
status: "online",
|
||||
activeConnection: update.activeConnection ?? existing.activeConnection ?? null,
|
||||
lastError: null,
|
||||
lastOnlineAt: update.lastOnlineAt,
|
||||
agentListReady: currentAgentListReady,
|
||||
hasEverReceivedAgentList:
|
||||
currentAgentListReady || existing.hasEverReceivedAgentList || false,
|
||||
};
|
||||
case "offline":
|
||||
return {
|
||||
status: "offline",
|
||||
activeConnection: update.activeConnection ?? existing.activeConnection ?? null,
|
||||
lastError: update.lastError ?? null,
|
||||
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||
agentListReady: false,
|
||||
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
status: "error",
|
||||
activeConnection: update.activeConnection ?? existing.activeConnection ?? null,
|
||||
lastError: update.lastError,
|
||||
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
|
||||
agentListReady: false,
|
||||
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
|
||||
};
|
||||
for (const daemon of daemons) {
|
||||
const snapshot = runtime.getSnapshot(daemon.serverId);
|
||||
next.set(daemon.serverId, {
|
||||
daemon,
|
||||
status: snapshot?.connectionStatus ?? "connecting",
|
||||
activeConnection: snapshot?.activeConnection ?? null,
|
||||
lastError: snapshot?.lastError ?? null,
|
||||
lastOnlineAt: snapshot?.lastOnlineAt ?? null,
|
||||
agentDirectoryStatus: snapshot?.agentDirectoryStatus ?? "initial_loading",
|
||||
agentDirectoryError: snapshot?.agentDirectoryError ?? null,
|
||||
hasEverLoadedAgentDirectory:
|
||||
snapshot?.hasEverLoadedAgentDirectory ?? false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function logConnectionLifecycle(daemon: HostProfile, previous: ConnectionState, next: ConnectionState) {
|
||||
const severity: "info" | "warn" = next.status === "error" ? "warn" : "info";
|
||||
const logger = severity === "warn" ? console.warn : console.info;
|
||||
|
||||
const logPayload = {
|
||||
event: "daemon_connection_state",
|
||||
serverId: daemon.serverId,
|
||||
label: daemon.label,
|
||||
from: previous.status,
|
||||
to: next.status,
|
||||
lastError: next.lastError ?? null,
|
||||
lastOnlineAt: next.lastOnlineAt ?? null,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity,
|
||||
};
|
||||
|
||||
logger("[DaemonConnection]", logPayload);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function useDaemonConnections(): DaemonConnectionsContextValue {
|
||||
const ctx = useContext(DaemonConnectionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useDaemonConnections must be used within DaemonConnectionsProvider");
|
||||
throw new Error(
|
||||
"useDaemonConnections must be used within DaemonConnectionsProvider"
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function DaemonConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
||||
const [connectionStates, setConnectionStates] = useState<Map<string, DaemonConnectionRecord>>(new Map());
|
||||
const runtime = getHostRuntimeStore();
|
||||
|
||||
// Ensure connection states stay in sync with registry entries
|
||||
useEffect(() => {
|
||||
setConnectionStates((prev) => {
|
||||
const next = new Map<string, DaemonConnectionRecord>();
|
||||
for (const daemon of daemons) {
|
||||
const existing = prev.get(daemon.serverId);
|
||||
next.set(daemon.serverId, {
|
||||
daemon,
|
||||
...(existing ?? createDefaultConnectionState()),
|
||||
});
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [daemons]);
|
||||
|
||||
|
||||
const updateConnectionStatus = useCallback(
|
||||
(serverId: string, update: ConnectionStateUpdate) => {
|
||||
setConnectionStates((prev) => {
|
||||
const existing = prev.get(serverId);
|
||||
if (!existing) {
|
||||
return prev;
|
||||
}
|
||||
const nextState = resolveNextConnectionState(existing, update);
|
||||
const hasChanged =
|
||||
existing.status !== nextState.status ||
|
||||
existing.lastError !== nextState.lastError ||
|
||||
existing.lastOnlineAt !== nextState.lastOnlineAt;
|
||||
|
||||
if (hasChanged) {
|
||||
logConnectionLifecycle(existing.daemon, existing, nextState);
|
||||
}
|
||||
|
||||
const next = new Map(prev);
|
||||
next.set(serverId, { daemon: existing.daemon, ...nextState });
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
const runtimeVersion = useSyncExternalStore(
|
||||
(onStoreChange) => runtime.subscribeAll(onStoreChange),
|
||||
() => runtime.getVersion(),
|
||||
() => runtime.getVersion()
|
||||
);
|
||||
|
||||
const markAgentListReady = useCallback((serverId: string, ready: boolean) => {
|
||||
setConnectionStates((prev) => {
|
||||
const existing = prev.get(serverId);
|
||||
if (!existing) {
|
||||
return prev;
|
||||
}
|
||||
if (existing.status !== "online") {
|
||||
return prev;
|
||||
}
|
||||
if (
|
||||
existing.agentListReady === ready &&
|
||||
(ready ? existing.hasEverReceivedAgentList : true)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(serverId, {
|
||||
...existing,
|
||||
agentListReady: ready,
|
||||
hasEverReceivedAgentList:
|
||||
ready || existing.hasEverReceivedAgentList,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const connectionStates = useMemo(
|
||||
() => buildConnectionStates({ daemons, runtime }),
|
||||
[daemons, runtime, runtimeVersion]
|
||||
);
|
||||
|
||||
const value: DaemonConnectionsContextValue = {
|
||||
connectionStates,
|
||||
isLoading: registryLoading,
|
||||
updateConnectionStatus,
|
||||
markAgentListReady,
|
||||
};
|
||||
const value = useMemo<DaemonConnectionsContextValue>(
|
||||
() => ({
|
||||
connectionStates,
|
||||
isLoading: registryLoading,
|
||||
}),
|
||||
[connectionStates, registryLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<DaemonConnectionsContext.Provider value={value}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useRef, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRef, ReactNode, useCallback, useEffect, useMemo } from "react";
|
||||
import { AppState, Platform } from "react-native";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useDaemonClient } from "@/hooks/use-daemon-client";
|
||||
import { useAudioPlayer } from "@/hooks/use-audio-player";
|
||||
import { useClientActivity } from "@/hooks/use-client-activity";
|
||||
import { usePushTokenRegistration } from "@/hooks/use-push-token-registration";
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
} from "@/types/stream";
|
||||
import type {
|
||||
ActivityLogPayload,
|
||||
AgentSnapshotPayload,
|
||||
AgentStreamEventPayload,
|
||||
SessionOutboundMessage,
|
||||
} from "@server/shared/messages";
|
||||
@@ -26,9 +24,9 @@ import {
|
||||
type NotificationPermissionRequest,
|
||||
} from "@server/shared/agent-attention-notification";
|
||||
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
|
||||
import type { DaemonClient, ConnectionState } from "@server/client/daemon-client";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { File } from "expo-file-system";
|
||||
import { useDaemonConnections } from "./daemon-connections-context";
|
||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||
import {
|
||||
useSessionStore,
|
||||
type Agent,
|
||||
@@ -39,6 +37,11 @@ import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||
import { sendOsNotification } from "@/utils/os-notifications";
|
||||
import { getInitKey, getInitDeferred, resolveInitDeferred, rejectInitDeferred, createInitDeferred } from "@/utils/agent-initialization";
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
import {
|
||||
derivePendingPermissionKey,
|
||||
normalizeAgentSnapshot,
|
||||
} from "@/utils/agent-snapshots";
|
||||
import { resolveProjectPlacement } from "@/utils/project-placement";
|
||||
|
||||
// Re-export types from session-store and draft-store for backward compatibility
|
||||
export type { DraftInput } from "@/stores/draft-store";
|
||||
@@ -53,32 +56,8 @@ export type {
|
||||
AgentFileExplorerState,
|
||||
} from "@/stores/session-store";
|
||||
|
||||
const derivePendingPermissionKey = (
|
||||
agentId: string,
|
||||
request: NotificationPermissionRequest
|
||||
) => {
|
||||
const fallbackId =
|
||||
request.id ||
|
||||
(typeof request.metadata?.id === "string"
|
||||
? request.metadata.id
|
||||
: undefined) ||
|
||||
request.name ||
|
||||
request.title ||
|
||||
`${request.kind}:${JSON.stringify(
|
||||
request.input ?? request.metadata ?? {}
|
||||
)}`;
|
||||
|
||||
return `${agentId}:${fallbackId}`;
|
||||
};
|
||||
|
||||
const HISTORY_STALE_AFTER_MS = 60_000;
|
||||
|
||||
type SessionConnectionSnapshot = {
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
|
||||
for (let i = items.length - 1; i >= 0; i -= 1) {
|
||||
const item = items[i];
|
||||
@@ -89,15 +68,6 @@ const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const mapConnectionState = (
|
||||
state: ConnectionState,
|
||||
lastError: string | null
|
||||
): SessionConnectionSnapshot => ({
|
||||
isConnected: state.status === "connected",
|
||||
isConnecting: state.status === "connecting",
|
||||
lastError: state.status === "disconnected" ? state.reason ?? lastError : null,
|
||||
});
|
||||
|
||||
const getLatestPermissionRequest = (
|
||||
session: SessionState | undefined,
|
||||
agentId: string
|
||||
@@ -142,51 +112,6 @@ type AgentUpdatePayload = Extract<
|
||||
const getAgentIdFromUpdate = (update: AgentUpdatePayload): string =>
|
||||
update.kind === "remove" ? update.agentId : update.agent.id;
|
||||
|
||||
function normalizeAgentSnapshot(
|
||||
snapshot: AgentSnapshotPayload,
|
||||
serverId: string
|
||||
) {
|
||||
const createdAt = new Date(snapshot.createdAt);
|
||||
const updatedAt = new Date(snapshot.updatedAt);
|
||||
const lastUserMessageAt = snapshot.lastUserMessageAt
|
||||
? new Date(snapshot.lastUserMessageAt)
|
||||
: null;
|
||||
const attentionTimestamp = snapshot.attentionTimestamp
|
||||
? new Date(snapshot.attentionTimestamp)
|
||||
: null;
|
||||
const archivedAt = snapshot.archivedAt
|
||||
? new Date(snapshot.archivedAt)
|
||||
: null;
|
||||
return {
|
||||
serverId,
|
||||
id: snapshot.id,
|
||||
provider: snapshot.provider,
|
||||
status: snapshot.status as AgentLifecycleStatus,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastUserMessageAt,
|
||||
lastActivityAt: updatedAt,
|
||||
capabilities: snapshot.capabilities,
|
||||
currentModeId: snapshot.currentModeId,
|
||||
availableModes: snapshot.availableModes ?? [],
|
||||
pendingPermissions: snapshot.pendingPermissions ?? [],
|
||||
persistence: snapshot.persistence ?? null,
|
||||
runtimeInfo: snapshot.runtimeInfo,
|
||||
lastUsage: snapshot.lastUsage,
|
||||
lastError: snapshot.lastError ?? null,
|
||||
title: snapshot.title ?? null,
|
||||
cwd: snapshot.cwd,
|
||||
model: snapshot.model ?? null,
|
||||
thinkingOptionId: snapshot.thinkingOptionId ?? null,
|
||||
requiresAttention: snapshot.requiresAttention ?? false,
|
||||
attentionReason: snapshot.attentionReason ?? null,
|
||||
attentionTimestamp,
|
||||
archivedAt,
|
||||
labels: snapshot.labels,
|
||||
projectPlacement: null,
|
||||
};
|
||||
}
|
||||
|
||||
const createExplorerState = () => ({
|
||||
directories: new Map(),
|
||||
files: new Map(),
|
||||
@@ -213,32 +138,11 @@ interface SessionProviderSharedProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
interface SessionProviderUrlProps extends SessionProviderSharedProps {
|
||||
serverUrl: string;
|
||||
daemonPublicKeyB64?: string;
|
||||
}
|
||||
|
||||
interface SessionProviderClientProps extends SessionProviderSharedProps {
|
||||
client: DaemonClient;
|
||||
}
|
||||
|
||||
export type SessionProviderProps = SessionProviderUrlProps | SessionProviderClientProps;
|
||||
|
||||
function SessionProviderWithUrl({
|
||||
children,
|
||||
serverId,
|
||||
serverUrl,
|
||||
daemonPublicKeyB64,
|
||||
}: SessionProviderUrlProps) {
|
||||
const client = useDaemonClient(serverUrl, { daemonPublicKeyB64 });
|
||||
return (
|
||||
<SessionProviderInternal
|
||||
children={children}
|
||||
serverId={serverId}
|
||||
client={client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export type SessionProviderProps = SessionProviderClientProps;
|
||||
|
||||
function SessionProviderWithClient({
|
||||
children,
|
||||
@@ -256,10 +160,7 @@ function SessionProviderWithClient({
|
||||
|
||||
// SessionProvider: Daemon client message handler that updates Zustand store
|
||||
export function SessionProvider(props: SessionProviderProps) {
|
||||
if ("client" in props) {
|
||||
return <SessionProviderWithClient {...props} />;
|
||||
}
|
||||
return <SessionProviderWithUrl {...props} />;
|
||||
return <SessionProviderWithClient {...props} />;
|
||||
}
|
||||
|
||||
function SessionProviderInternal({
|
||||
@@ -268,11 +169,7 @@ function SessionProviderInternal({
|
||||
client,
|
||||
}: SessionProviderClientProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [connectionSnapshot, setConnectionSnapshot] =
|
||||
useState<SessionConnectionSnapshot>(() =>
|
||||
mapConnectionState(client.getConnectionState(), client.lastError)
|
||||
);
|
||||
const { markAgentListReady } = useDaemonConnections();
|
||||
const { isConnected } = useHostRuntimeSession(serverId);
|
||||
|
||||
// Zustand store actions
|
||||
const initializeSession = useSessionStore((state) => state.initializeSession);
|
||||
@@ -323,6 +220,9 @@ function SessionProviderInternal({
|
||||
const focusedAgentId = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.focusedAgentId ?? null
|
||||
);
|
||||
const sessionAgents = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.agents
|
||||
);
|
||||
|
||||
const handleAppResumed = useCallback(
|
||||
(awayMs: number) => {
|
||||
@@ -359,8 +259,6 @@ function SessionProviderInternal({
|
||||
) => Promise<void>)
|
||||
| null
|
||||
>(null);
|
||||
const hasBootstrappedAgentUpdatesRef = useRef(false);
|
||||
const agentUpdatesSubscriptionIdRef = useRef<string | null>(null);
|
||||
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
@@ -380,6 +278,18 @@ function SessionProviderInternal({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionAgents) {
|
||||
previousAgentStatusRef.current.clear();
|
||||
return;
|
||||
}
|
||||
const nextStatuses = new Map<string, AgentLifecycleStatus>();
|
||||
for (const nextAgent of sessionAgents.values()) {
|
||||
nextStatuses.set(nextAgent.id, nextAgent.status);
|
||||
}
|
||||
previousAgentStatusRef.current = nextStatuses;
|
||||
}, [sessionAgents]);
|
||||
|
||||
const notifyAgentAttention = useCallback(
|
||||
(params: {
|
||||
agentId: string;
|
||||
@@ -456,22 +366,6 @@ function SessionProviderInternal({
|
||||
}
|
||||
const audioChunkBuffersRef = useRef<Map<string, AudioChunk[]>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = client.subscribeConnectionStatus((state) => {
|
||||
setConnectionSnapshot(mapConnectionState(state, client.lastError));
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [client]);
|
||||
|
||||
const wasConnectedRef = useRef(client.isConnected);
|
||||
useEffect(() => {
|
||||
const wasConnected = wasConnectedRef.current;
|
||||
if (!wasConnected && connectionSnapshot.isConnected) {
|
||||
bumpHistorySyncGeneration(serverId);
|
||||
}
|
||||
wasConnectedRef.current = connectionSnapshot.isConnected;
|
||||
}, [serverId, connectionSnapshot.isConnected, bumpHistorySyncGeneration]);
|
||||
|
||||
// Initialize session in store
|
||||
useEffect(() => {
|
||||
initializeSession(serverId, client, audioPlayer);
|
||||
@@ -483,11 +377,11 @@ function SessionProviderInternal({
|
||||
|
||||
// If the client drops mid-initialization, clear pending flags
|
||||
useEffect(() => {
|
||||
if (!connectionSnapshot.isConnected) {
|
||||
if (!isConnected) {
|
||||
pendingAgentUpdatesRef.current.clear();
|
||||
setInitializingAgents(serverId, new Map());
|
||||
}
|
||||
}, [serverId, connectionSnapshot.isConnected, setInitializingAgents]);
|
||||
}, [serverId, isConnected, setInitializingAgents]);
|
||||
|
||||
const applyAgentUpdatePayload = useCallback(
|
||||
(update: AgentUpdatePayload) => {
|
||||
@@ -542,9 +436,13 @@ function SessionProviderInternal({
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeAgentSnapshot(update.agent, serverId);
|
||||
const agent = {
|
||||
...normalizeAgentSnapshot(update.agent, serverId),
|
||||
projectPlacement: update.project,
|
||||
...normalized,
|
||||
projectPlacement: resolveProjectPlacement({
|
||||
projectPlacement: update.project,
|
||||
cwd: normalized.cwd,
|
||||
}),
|
||||
};
|
||||
|
||||
console.log("[Session] Agent update:", agent.id, agent.status);
|
||||
@@ -796,91 +694,11 @@ function SessionProviderInternal({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectionSnapshot.isConnected) {
|
||||
hasBootstrappedAgentUpdatesRef.current = false;
|
||||
pendingAgentUpdatesRef.current.clear();
|
||||
agentUpdatesSubscriptionIdRef.current = null;
|
||||
if (isConnected) {
|
||||
return;
|
||||
}
|
||||
if (hasBootstrappedAgentUpdatesRef.current) {
|
||||
return;
|
||||
}
|
||||
hasBootstrappedAgentUpdatesRef.current = true;
|
||||
|
||||
let cancelled = false;
|
||||
const requestedSubscriptionId = `app:${serverId}`;
|
||||
|
||||
const bootstrapAgentDirectory = async () => {
|
||||
try {
|
||||
const payload = await client.fetchAgents({
|
||||
filter: { labels: { ui: "true" } },
|
||||
subscribe: { subscriptionId: requestedSubscriptionId },
|
||||
});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
agentUpdatesSubscriptionIdRef.current =
|
||||
payload.subscriptionId ?? requestedSubscriptionId;
|
||||
|
||||
const nextAgents = new Map<string, Agent>();
|
||||
const nextPendingPermissions = new Map<
|
||||
string,
|
||||
{ key: string; agentId: string; request: NotificationPermissionRequest }
|
||||
>();
|
||||
const nextStatuses = new Map<string, AgentLifecycleStatus>();
|
||||
|
||||
for (const entry of payload.entries) {
|
||||
const agent = {
|
||||
...normalizeAgentSnapshot(entry.agent, serverId),
|
||||
projectPlacement: entry.project,
|
||||
};
|
||||
nextAgents.set(agent.id, agent);
|
||||
nextStatuses.set(agent.id, agent.status);
|
||||
|
||||
for (const request of agent.pendingPermissions) {
|
||||
const key = derivePendingPermissionKey(agent.id, request);
|
||||
nextPendingPermissions.set(key, { key, agentId: agent.id, request });
|
||||
}
|
||||
}
|
||||
|
||||
previousAgentStatusRef.current = nextStatuses;
|
||||
pendingAgentUpdatesRef.current.clear();
|
||||
setAgents(serverId, nextAgents);
|
||||
for (const agent of nextAgents.values()) {
|
||||
setAgentLastActivity(agent.id, agent.lastActivityAt);
|
||||
}
|
||||
setPendingPermissions(serverId, nextPendingPermissions);
|
||||
setInitializingAgents(serverId, new Map());
|
||||
setHasHydratedAgents(serverId, true);
|
||||
markAgentListReady(serverId, true);
|
||||
} catch (err) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
hasBootstrappedAgentUpdatesRef.current = false;
|
||||
pendingAgentUpdatesRef.current.clear();
|
||||
agentUpdatesSubscriptionIdRef.current = null;
|
||||
console.error("[Session] fetchAgents bootstrap failed", { serverId, err });
|
||||
}
|
||||
};
|
||||
|
||||
void bootstrapAgentDirectory();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
connectionSnapshot.isConnected,
|
||||
client,
|
||||
serverId,
|
||||
setAgentLastActivity,
|
||||
setAgents,
|
||||
setHasHydratedAgents,
|
||||
setInitializingAgents,
|
||||
setPendingPermissions,
|
||||
markAgentListReady,
|
||||
]);
|
||||
pendingAgentUpdatesRef.current.clear();
|
||||
}, [isConnected]);
|
||||
|
||||
// Daemon message handlers - directly update Zustand store
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user