Unify app connection state around host runtime
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user