Fix agent history catch-up + avoid mobile background stream deltas (#18)
* Update files * Fix agent history sync + mobile stream gating
This commit is contained in:
@@ -84,6 +84,11 @@ export interface AgentStreamViewProps {
|
|||||||
agent: Agent;
|
agent: Agent;
|
||||||
streamItems: StreamItem[];
|
streamItems: StreamItem[];
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
|
/**
|
||||||
|
* True when we expect a history snapshot to arrive shortly (e.g. after opening an agent
|
||||||
|
* or resuming after connectivity changes) and should avoid showing the "empty" state.
|
||||||
|
*/
|
||||||
|
isSyncingHistory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentStreamView({
|
export function AgentStreamView({
|
||||||
@@ -92,6 +97,7 @@ export function AgentStreamView({
|
|||||||
agent,
|
agent,
|
||||||
streamItems,
|
streamItems,
|
||||||
pendingPermissions,
|
pendingPermissions,
|
||||||
|
isSyncingHistory = false,
|
||||||
}: AgentStreamViewProps) {
|
}: AgentStreamViewProps) {
|
||||||
const flatListRef = useRef<FlatList<StreamItem>>(null);
|
const flatListRef = useRef<FlatList<StreamItem>>(null);
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -590,6 +596,43 @@ export function AgentStreamView({
|
|||||||
return { marginBottom: tightGap };
|
return { marginBottom: tightGap };
|
||||||
}, [listHeaderComponent, tightGap]);
|
}, [listHeaderComponent, tightGap]);
|
||||||
|
|
||||||
|
const listEmptyComponent = useMemo(() => {
|
||||||
|
const hasPermissions = pendingPermissionItems.length > 0;
|
||||||
|
const hasHeadItems = (streamHead?.length ?? 0) > 0;
|
||||||
|
if (hasPermissions || hasHeadItems) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowSyncing =
|
||||||
|
isSyncingHistory || agent.status === "running";
|
||||||
|
|
||||||
|
if (shouldShowSyncing) {
|
||||||
|
return (
|
||||||
|
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
||||||
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
|
<Text style={stylesheet.emptyStateText}>Catching up…</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
||||||
|
<Text style={stylesheet.emptyStateText}>
|
||||||
|
Start chatting with this agent...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
agent.status,
|
||||||
|
isSyncingHistory,
|
||||||
|
pendingPermissionItems.length,
|
||||||
|
streamHead,
|
||||||
|
theme.colors.foregroundMuted,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolCallSheetProvider>
|
<ToolCallSheetProvider>
|
||||||
<View style={stylesheet.container}>
|
<View style={stylesheet.container}>
|
||||||
@@ -607,13 +650,7 @@ export function AgentStreamView({
|
|||||||
style={stylesheet.list}
|
style={stylesheet.list}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={listEmptyComponent}
|
||||||
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
|
||||||
<Text style={stylesheet.emptyStateText}>
|
|
||||||
Start chatting with this agent...
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
ListHeaderComponent={listHeaderComponent}
|
ListHeaderComponent={listHeaderComponent}
|
||||||
extraData={flatListExtraData}
|
extraData={flatListExtraData}
|
||||||
maintainVisibleContentPosition={
|
maintainVisibleContentPosition={
|
||||||
|
|||||||
@@ -915,16 +915,6 @@ export function SessionProvider({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
|
||||||
const currentState = prev.get(agentId);
|
|
||||||
if (currentState === false) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, false);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: We don't update lastActivityAt on every stream event to prevent
|
// NOTE: We don't update lastActivityAt on every stream event to prevent
|
||||||
// cascading rerenders. The agent_update handler updates agent.lastActivityAt
|
// cascading rerenders. The agent_update handler updates agent.lastActivityAt
|
||||||
// on status changes, which is sufficient for sorting and display purposes.
|
// on status changes, which is sufficient for sorting and display purposes.
|
||||||
@@ -956,7 +946,7 @@ export function SessionProvider({
|
|||||||
clearAgentStreamHead(serverId, agentId);
|
clearAgentStreamHead(serverId, agentId);
|
||||||
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
if (!prev.has(agentId)) {
|
if (prev.get(agentId) !== true) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
@@ -990,16 +980,6 @@ export function SessionProvider({
|
|||||||
requestId: (message.payload as any).requestId,
|
requestId: (message.payload as any).requestId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (status === "agent_initialized" && "agentId" in message.payload) {
|
|
||||||
const agentId = (message.payload as any).agentId as string | undefined;
|
|
||||||
if (agentId) {
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, false);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubPermissionRequest = client.on(
|
const unsubPermissionRequest = client.on(
|
||||||
@@ -1443,12 +1423,6 @@ export function SessionProvider({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
setAgentStreamTail(serverId, (prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, []);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
clearAgentStreamHead(serverId, agentId);
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn("[Session] initializeAgent skipped: daemon unavailable");
|
console.warn("[Session] initializeAgent skipped: daemon unavailable");
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
@@ -1470,24 +1444,17 @@ export function SessionProvider({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[serverId, client, setAgentStreamTail, setInitializingAgents, clearAgentStreamHead]
|
[serverId, client, setInitializingAgents]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshAgent = useCallback(
|
const refreshAgent = useCallback(
|
||||||
({ agentId, requestId }: { agentId: string; requestId?: string }) => {
|
({ agentId }: { agentId: string }) => {
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
next.set(agentId, true);
|
next.set(agentId, true);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
setAgentStreamTail(serverId, (prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, []);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
clearAgentStreamHead(serverId, agentId);
|
|
||||||
|
|
||||||
refreshAgentMutation
|
refreshAgentMutation
|
||||||
.mutateAsync({ agentId })
|
.mutateAsync({ agentId })
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -1499,13 +1466,7 @@ export function SessionProvider({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[serverId, refreshAgentMutation, setInitializingAgents]
|
||||||
serverId,
|
|
||||||
refreshAgentMutation,
|
|
||||||
setAgentStreamTail,
|
|
||||||
setInitializingAgents,
|
|
||||||
clearAgentStreamHead,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendAgentMessage = useCallback(
|
const sendAgentMessage = useCallback(
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import {
|
import {
|
||||||
|
attachInitTimeout,
|
||||||
createInitDeferred,
|
createInitDeferred,
|
||||||
getInitDeferred,
|
getInitDeferred,
|
||||||
getInitKey,
|
getInitKey,
|
||||||
rejectInitDeferred,
|
rejectInitDeferred,
|
||||||
} from "@/utils/agent-initialization";
|
} from "@/utils/agent-initialization";
|
||||||
|
|
||||||
const INIT_TIMEOUT_MS = 10000;
|
const INIT_TIMEOUT_MS = 5 * 60_000;
|
||||||
|
|
||||||
export function useAgentInitialization(serverId: string) {
|
export function useAgentInitialization(serverId: string) {
|
||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
|
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
|
||||||
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
|
||||||
const clearAgentStreamHead = useSessionStore((state) => state.clearAgentStreamHead);
|
|
||||||
|
|
||||||
const ensureAgentIsInitialized = useCallback(
|
const ensureAgentIsInitialized = useCallback(
|
||||||
(agentId: string): Promise<void> => {
|
(agentId: string): Promise<void> => {
|
||||||
@@ -24,10 +23,23 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deferred = createInitDeferred(key);
|
const deferred = createInitDeferred(key);
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
rejectInitDeferred(key, new Error(`Agent initialization timed out after ${INIT_TIMEOUT_MS}ms`));
|
setInitializingAgents(serverId, (prev) => {
|
||||||
|
if (prev.get(agentId) !== true) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(agentId, false);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
rejectInitDeferred(
|
||||||
|
key,
|
||||||
|
new Error(
|
||||||
|
`History sync timed out after ${Math.round(INIT_TIMEOUT_MS / 1000)}s`
|
||||||
|
)
|
||||||
|
);
|
||||||
}, INIT_TIMEOUT_MS);
|
}, INIT_TIMEOUT_MS);
|
||||||
|
attachInitTimeout(key, timeoutId);
|
||||||
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
@@ -35,15 +47,7 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
setAgentStreamTail(serverId, (prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, []);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
clearAgentStreamHead(serverId, agentId);
|
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
next.set(agentId, false);
|
next.set(agentId, false);
|
||||||
@@ -56,10 +60,10 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
client
|
client
|
||||||
.initializeAgent(agentId)
|
.initializeAgent(agentId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
clearTimeout(timeoutId);
|
// No-op: the actual "timeline hydrated" signal is the `agent_stream_snapshot`
|
||||||
|
// message, handled in SessionContext.
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
setInitializingAgents(serverId, (prev) => {
|
setInitializingAgents(serverId, (prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
next.set(agentId, false);
|
next.set(agentId, false);
|
||||||
@@ -73,7 +77,7 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
[clearAgentStreamHead, client, serverId, setAgentStreamTail, setInitializingAgents]
|
[client, serverId, setInitializingAgents]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshAgent = useCallback(
|
const refreshAgent = useCallback(
|
||||||
@@ -87,13 +91,6 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
setAgentStreamTail(serverId, (prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(agentId, []);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
clearAgentStreamHead(serverId, agentId);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.refreshAgent(agentId);
|
await client.refreshAgent(agentId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -105,9 +102,8 @@ export function useAgentInitialization(serverId: string) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clearAgentStreamHead, client, serverId, setAgentStreamTail, setInitializingAgents]
|
[client, serverId, setInitializingAgents]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ensureAgentIsInitialized, refreshAgent };
|
return { ensureAgentIsInitialized, refreshAgent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ export function useClientActivity({ client, focusedAgentId }: ClientActivityOpti
|
|||||||
appVisibleRef.current = nextState === "active";
|
appVisibleRef.current = nextState === "active";
|
||||||
if (nextState === "active") {
|
if (nextState === "active") {
|
||||||
recordUserActivity();
|
recordUserActivity();
|
||||||
maybeSendImmediateHeartbeat();
|
|
||||||
}
|
}
|
||||||
|
// Send immediately on visibility changes so the server can adapt streaming behavior.
|
||||||
|
sendHeartbeat();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => subscription.remove();
|
return () => subscription.remove();
|
||||||
}, [maybeSendImmediateHeartbeat, recordUserActivity]);
|
}, [recordUserActivity, sendHeartbeat]);
|
||||||
|
|
||||||
// Track user activity on web for accurate staleness.
|
// Track user activity on web for accurate staleness.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
|
AppState,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
@@ -503,6 +504,46 @@ function AgentScreenContent({
|
|||||||
});
|
});
|
||||||
}, [resolvedAgentId, ensureAgentIsInitialized, isConnected]);
|
}, [resolvedAgentId, ensureAgentIsInitialized, isConnected]);
|
||||||
|
|
||||||
|
// When the app comes back to the foreground, re-sync history for the focused agent.
|
||||||
|
// This covers cases where the OS/backgrounding caused us to miss stream events.
|
||||||
|
const lastAppStateRef = useRef(AppState.currentState);
|
||||||
|
const lastResumeSyncAtRef = useRef(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resolvedAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = AppState.addEventListener("change", (nextState) => {
|
||||||
|
const prev = lastAppStateRef.current;
|
||||||
|
lastAppStateRef.current = nextState;
|
||||||
|
|
||||||
|
if (nextState !== "active" || prev === "active") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
// Avoid accidental double-syncs on rapid transitions.
|
||||||
|
if (now - lastResumeSyncAtRef.current < 2000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastResumeSyncAtRef.current = now;
|
||||||
|
|
||||||
|
ensureAgentIsInitialized(resolvedAgentId).catch((error) => {
|
||||||
|
console.warn("[AgentScreen] Agent initialization failed on resume", {
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.remove();
|
||||||
|
};
|
||||||
|
}, [ensureAgentIsInitialized, isConnected, resolvedAgentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web") {
|
if (Platform.OS !== "web") {
|
||||||
return;
|
return;
|
||||||
@@ -723,28 +764,21 @@ function AgentScreenContent({
|
|||||||
<ReanimatedAnimated.View
|
<ReanimatedAnimated.View
|
||||||
style={[styles.content, animatedKeyboardStyle]}
|
style={[styles.content, animatedKeyboardStyle]}
|
||||||
>
|
>
|
||||||
{isInitializing && !shouldUseOptimisticStream ? (
|
<AgentStreamView
|
||||||
<View style={styles.loadingContainer}>
|
agentId={effectiveAgent.id}
|
||||||
<ActivityIndicator
|
serverId={serverId}
|
||||||
size="large"
|
agent={effectiveAgent}
|
||||||
color={theme.colors.primary}
|
streamItems={
|
||||||
/>
|
shouldUseOptimisticStream ? mergedStreamItems : streamItems
|
||||||
<Text style={styles.loadingText}>Loading agent...</Text>
|
}
|
||||||
</View>
|
pendingPermissions={pendingPermissions}
|
||||||
) : (
|
isSyncingHistory={isInitializing && !shouldUseOptimisticStream}
|
||||||
<AgentStreamView
|
/>
|
||||||
agentId={effectiveAgent.id}
|
|
||||||
serverId={serverId}
|
|
||||||
agent={effectiveAgent}
|
|
||||||
streamItems={shouldUseOptimisticStream ? mergedStreamItems : streamItems}
|
|
||||||
pendingPermissions={pendingPermissions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ReanimatedAnimated.View>
|
</ReanimatedAnimated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Agent Input Area */}
|
{/* Agent Input Area */}
|
||||||
{!isInitializing && agent && resolvedAgentId && (
|
{agent && resolvedAgentId && (
|
||||||
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} autoFocus onAddImages={handleAddImagesCallback} />
|
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} autoFocus onAddImages={handleAddImagesCallback} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -46,25 +46,11 @@ function createUniqueTimelineId(
|
|||||||
timestamp: Date
|
timestamp: Date
|
||||||
): string {
|
): string {
|
||||||
const base = createTimelineId(prefix, text, timestamp);
|
const base = createTimelineId(prefix, text, timestamp);
|
||||||
let suffixSeed = state.length;
|
// We only ever append new timeline items, and we incorporate the current
|
||||||
let candidate = `${base}_${suffixSeed.toString(36)}`;
|
// length as a monotonic suffix, so uniqueness is guaranteed without an O(n)
|
||||||
|
// collision scan (important for large hydration snapshots).
|
||||||
// Fast path when the generated id hasn't been used yet
|
const suffixSeed = state.length;
|
||||||
const hasCollision = state.some((entry) => entry.id === candidate);
|
return `${base}_${suffixSeed.toString(36)}`;
|
||||||
if (!hasCollision) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the id already exists (e.g. prior items were pruned/replaced),
|
|
||||||
// spin until we find an unused suffix. Building the lookup Set lazily keeps
|
|
||||||
// the common case cheap while still guaranteeing uniqueness.
|
|
||||||
const usedIds = new Set(state.map((entry) => entry.id));
|
|
||||||
while (usedIds.has(candidate)) {
|
|
||||||
suffixSeed += 1;
|
|
||||||
candidate = `${base}_${suffixSeed.toString(36)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StreamItem =
|
export type StreamItem =
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface DeferredInit {
|
|||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
resolve: () => void;
|
resolve: () => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
|
timeoutId: ReturnType<typeof setTimeout> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPromises = new Map<string, DeferredInit>();
|
const initPromises = new Map<string, DeferredInit>();
|
||||||
@@ -23,14 +24,30 @@ export function createInitDeferred(key: string): DeferredInit {
|
|||||||
reject = rej;
|
reject = rej;
|
||||||
});
|
});
|
||||||
|
|
||||||
const deferred: DeferredInit = { promise, resolve, reject };
|
const deferred: DeferredInit = { promise, resolve, reject, timeoutId: null };
|
||||||
initPromises.set(key, deferred);
|
initPromises.set(key, deferred);
|
||||||
return deferred;
|
return deferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function attachInitTimeout(key: string, timeoutId: ReturnType<typeof setTimeout>): void {
|
||||||
|
const deferred = initPromises.get(key);
|
||||||
|
if (!deferred) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deferred.timeoutId = timeoutId;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveInitDeferred(key: string): void {
|
export function resolveInitDeferred(key: string): void {
|
||||||
const deferred = initPromises.get(key);
|
const deferred = initPromises.get(key);
|
||||||
deferred?.resolve();
|
if (!deferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deferred.timeoutId) {
|
||||||
|
clearTimeout(deferred.timeoutId);
|
||||||
|
}
|
||||||
|
initPromises.delete(key);
|
||||||
|
deferred.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rejectInitDeferred(key: string, error: Error): void {
|
export function rejectInitDeferred(key: string, error: Error): void {
|
||||||
@@ -38,7 +55,9 @@ export function rejectInitDeferred(key: string, error: Error): void {
|
|||||||
if (!deferred) {
|
if (!deferred) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (deferred.timeoutId) {
|
||||||
|
clearTimeout(deferred.timeoutId);
|
||||||
|
}
|
||||||
initPromises.delete(key);
|
initPromises.delete(key);
|
||||||
deferred.reject(error);
|
deferred.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -578,6 +578,24 @@ export class Session {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
|
||||||
|
// while the app is visible and the user is focused on that agent.
|
||||||
|
//
|
||||||
|
// History catch-up is handled via explicit `initialize_agent_request` which emits a
|
||||||
|
// batched `agent_stream_snapshot`.
|
||||||
|
const activity = this.clientActivity;
|
||||||
|
if (activity?.deviceType === "mobile") {
|
||||||
|
if (!activity.appVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!activity.focusedAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activity.focusedAgentId !== event.agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
agentId: event.agentId,
|
agentId: event.agentId,
|
||||||
event: serializeAgentStreamEvent(event.event),
|
event: serializeAgentStreamEvent(event.event),
|
||||||
|
|||||||
Reference in New Issue
Block a user