Unify app connection state around host runtime

This commit is contained in:
Mohamed Boudra
2026-02-20 10:37:59 +07:00
parent 6b23509b96
commit d128a32d2f
22 changed files with 173 additions and 191 deletions
@@ -18,7 +18,7 @@ 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; agentListReady?: boolean }
| { 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 };
@@ -30,6 +30,7 @@ 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);
@@ -69,7 +70,8 @@ function resolveNextConnectionState(
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
case "online":
const currentAgentListReady = update.agentListReady ?? (existing.status === "online" ? existing.agentListReady : false);
const currentAgentListReady =
existing.status === "online" ? existing.agentListReady : false;
return {
status: "online",
@@ -173,10 +175,37 @@ export function DaemonConnectionsProvider({ children }: { children: ReactNode })
[]
);
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 value: DaemonConnectionsContextValue = {
connectionStates,
isLoading: registryLoading,
updateConnectionStatus,
markAgentListReady,
};
return (
+11 -18
View File
@@ -33,7 +33,6 @@ import {
useSessionStore,
type Agent,
type SessionState,
type DaemonConnectionSnapshot,
} from "@/stores/session-store";
import { useDraftStore } from "@/stores/draft-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory";
@@ -74,6 +73,12 @@ const derivePendingPermissionKey = (
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];
@@ -87,7 +92,7 @@ const findLatestAssistantMessageText = (items: StreamItem[]): string | null => {
const mapConnectionState = (
state: ConnectionState,
lastError: string | null
): DaemonConnectionSnapshot => ({
): SessionConnectionSnapshot => ({
isConnected: state.status === "connected",
isConnecting: state.status === "connecting",
lastError: state.status === "disconnected" ? state.reason ?? lastError : null,
@@ -264,10 +269,10 @@ function SessionProviderInternal({
}: SessionProviderClientProps) {
const queryClient = useQueryClient();
const [connectionSnapshot, setConnectionSnapshot] =
useState<DaemonConnectionSnapshot>(() =>
useState<SessionConnectionSnapshot>(() =>
mapConnectionState(client.getConnectionState(), client.lastError)
);
const { updateConnectionStatus } = useDaemonConnections();
const { markAgentListReady } = useDaemonConnections();
// Zustand store actions
const initializeSession = useSessionStore((state) => state.initializeSession);
@@ -312,9 +317,6 @@ function SessionProviderInternal({
const clearDraftInput = useDraftStore((state) => state.clearDraftInput);
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
const updateSessionClient = useSessionStore((state) => state.updateSessionClient);
const updateSessionConnection = useSessionStore(
(state) => state.updateSessionConnection
);
const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo);
// Track focused agent for heartbeat
@@ -470,10 +472,6 @@ function SessionProviderInternal({
wasConnectedRef.current = connectionSnapshot.isConnected;
}, [serverId, connectionSnapshot.isConnected, bumpHistorySyncGeneration]);
useEffect(() => {
updateSessionConnection(serverId, connectionSnapshot);
}, [serverId, connectionSnapshot, updateSessionConnection]);
// Initialize session in store
useEffect(() => {
initializeSession(serverId, client, audioPlayer);
@@ -855,11 +853,7 @@ function SessionProviderInternal({
setPendingPermissions(serverId, nextPendingPermissions);
setInitializingAgents(serverId, new Map());
setHasHydratedAgents(serverId, true);
updateConnectionStatus(serverId, {
status: "online",
lastOnlineAt: new Date().toISOString(),
agentListReady: true,
});
markAgentListReady(serverId, true);
} catch (err) {
if (cancelled) {
return;
@@ -885,7 +879,7 @@ function SessionProviderInternal({
setHasHydratedAgents,
setInitializingAgents,
setPendingPermissions,
updateConnectionStatus,
markAgentListReady,
]);
// Daemon message handlers - directly update Zustand store
@@ -1420,7 +1414,6 @@ function SessionProviderInternal({
setPendingPermissions,
setFileExplorer,
setHasHydratedAgents,
updateConnectionStatus,
clearDraftInput,
notifyAgentAttention,
applyAgentUpdatePayload,
+7 -3
View File
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, ReactNode, useCallback, useEffect,
import { useSpeechmaticsAudio } from "@/hooks/use-speechmatics-audio";
import type { SessionState } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import { useHostRuntimeSession } from "@/runtime/host-runtime";
import { resolveVoiceUnavailableMessage } from "@/utils/server-info-capabilities";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { REALTIME_VOICE_VAD_CONFIG } from "@/voice/realtime-voice-config";
@@ -45,6 +46,9 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
const getSession = useSessionStore((state) => state.getSession);
const [activeServerId, setActiveServerId] = useState<string | null>(null);
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
const { client: runtimeClient, isConnected: activeRuntimeConnected } = useHostRuntimeSession(
activeServerId ?? ""
);
const activeSession = useSessionStore(
useCallback(
(state: ReturnType<typeof useSessionStore.getState>) => {
@@ -274,8 +278,8 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
}, [isVoiceMode, realtimeAudio.isDetecting, realtimeAudio.isSpeaking]);
useEffect(() => {
const connected = activeSession?.connection.isConnected ?? false;
const client = activeSession?.client ?? null;
const connected = activeRuntimeConnected;
const client = runtimeClient;
if (!connected) {
voiceTransportReadyRef.current = false;
}
@@ -313,7 +317,7 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
}
wasVoiceSocketConnectedRef.current = connected;
}, [activeAgentId, activeServerId, activeSession?.client, activeSession?.connection.isConnected, isVoiceMode]);
}, [activeAgentId, activeRuntimeConnected, activeServerId, isVoiceMode, runtimeClient]);
const isPlayingAudio = activeSession?.isPlayingAudio ?? false;