revert: remove Phase 3 complexity - server handles persistence
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
export const SESSION_DEBUG_PERSIST_FULL_SNAPSHOT =
|
|
||||||
typeof process !== "undefined" && process.env?.EXPO_PUBLIC_SESSION_DEBUG_FULL_SNAPSHOT === "1";
|
|
||||||
|
|
||||||
@@ -33,9 +33,6 @@ import * as FileSystem from 'expo-file-system';
|
|||||||
import { useDaemonConnections } from "./daemon-connections-context";
|
import { useDaemonConnections } from "./daemon-connections-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||||
import { SESSION_DEBUG_PERSIST_FULL_SNAPSHOT } from "@/config/session-debug";
|
|
||||||
import { publishSessionHeavyState, clearSessionHeavyState } from "@/stores/session-heavy-state";
|
|
||||||
import type { SessionHeavyState } from "@/stores/session-heavy-state";
|
|
||||||
|
|
||||||
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
const derivePendingPermissionKey = (agentId: string, request: AgentPermissionRequest) => {
|
||||||
const fallbackId =
|
const fallbackId =
|
||||||
@@ -207,137 +204,42 @@ const pushHistory = (history: string[], path: string): string[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SESSION_SNAPSHOT_STORAGE_PREFIX = "@paseo:session-snapshot:";
|
const SESSION_SNAPSHOT_STORAGE_PREFIX = "@paseo:session-snapshot:";
|
||||||
const SESSION_SNAPSHOT_VERSION = 2;
|
|
||||||
export const MAX_MESSAGES_PER_AGENT = 200;
|
|
||||||
export const MAX_STREAM_ITEMS_PER_AGENT = 200;
|
|
||||||
|
|
||||||
const EMPTY_MESSAGES: MessageEntry[] = [];
|
type PersistedSessionSnapshot = {
|
||||||
const EMPTY_AGENT_STREAM_STATE = new Map<string, StreamItem[]>();
|
|
||||||
const DEFAULT_AGENT_CAPABILITIES: AgentCapabilityFlags = {
|
|
||||||
supportsStreaming: false,
|
|
||||||
supportsSessionPersistence: false,
|
|
||||||
supportsDynamicModes: false,
|
|
||||||
supportsMcpServers: false,
|
|
||||||
supportsReasoningStream: false,
|
|
||||||
supportsToolInvocations: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
type PersistedDirectoryEntry = Omit<AgentDirectoryEntry, "lastActivityAt"> & {
|
|
||||||
lastActivityAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LegacySessionSnapshot = {
|
|
||||||
agents: AgentSnapshotPayload[];
|
agents: AgentSnapshotPayload[];
|
||||||
commands: Command[];
|
commands: Command[];
|
||||||
savedAt: string;
|
savedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LightweightSessionSnapshot = {
|
|
||||||
version: typeof SESSION_SNAPSHOT_VERSION;
|
|
||||||
directory: PersistedDirectoryEntry[];
|
|
||||||
savedAt: string;
|
|
||||||
full?: {
|
|
||||||
agents: AgentSnapshotPayload[];
|
|
||||||
commands: Command[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoadedSessionSnapshot =
|
|
||||||
| {
|
|
||||||
kind: "legacy";
|
|
||||||
agents: AgentSnapshotPayload[];
|
|
||||||
commands: Command[];
|
|
||||||
savedAt: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: "lightweight";
|
|
||||||
directory: AgentDirectoryEntry[];
|
|
||||||
savedAt: string;
|
|
||||||
full?: {
|
|
||||||
agents: AgentSnapshotPayload[];
|
|
||||||
commands: Command[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSessionSnapshotStorageKey = (serverId: string): string => {
|
const getSessionSnapshotStorageKey = (serverId: string): string => {
|
||||||
return `${SESSION_SNAPSHOT_STORAGE_PREFIX}${serverId}`;
|
return `${SESSION_SNAPSHOT_STORAGE_PREFIX}${serverId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isLegacySessionSnapshot(payload: any): payload is LegacySessionSnapshot {
|
async function loadPersistedSessionSnapshot(serverId: string): Promise<PersistedSessionSnapshot | null> {
|
||||||
return Array.isArray(payload?.agents) && Array.isArray(payload?.commands);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLightweightSessionSnapshot(payload: any): payload is LightweightSessionSnapshot {
|
|
||||||
return (
|
|
||||||
payload?.version === SESSION_SNAPSHOT_VERSION &&
|
|
||||||
Array.isArray(payload?.directory)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializePersistedDirectoryEntry(entry: PersistedDirectoryEntry): AgentDirectoryEntry {
|
|
||||||
return {
|
|
||||||
...entry,
|
|
||||||
lastActivityAt: new Date(entry.lastActivityAt),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeDirectoryEntry(entry: AgentDirectoryEntry): PersistedDirectoryEntry {
|
|
||||||
return {
|
|
||||||
...entry,
|
|
||||||
lastActivityAt: entry.lastActivityAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPersistedSessionSnapshot(serverId: string): Promise<LoadedSessionSnapshot | null> {
|
|
||||||
try {
|
try {
|
||||||
const raw = await AsyncStorage.getItem(getSessionSnapshotStorageKey(serverId));
|
const raw = await AsyncStorage.getItem(getSessionSnapshotStorageKey(serverId));
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw) as PersistedSessionSnapshot;
|
||||||
if (isLightweightSessionSnapshot(parsed)) {
|
if (!Array.isArray(parsed?.agents) || !Array.isArray(parsed?.commands)) {
|
||||||
return {
|
|
||||||
kind: "lightweight",
|
|
||||||
directory: parsed.directory.map(deserializePersistedDirectoryEntry),
|
|
||||||
savedAt: parsed.savedAt,
|
|
||||||
full: parsed.full,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!isLegacySessionSnapshot(parsed)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return parsed;
|
||||||
kind: "legacy",
|
|
||||||
agents: parsed.agents,
|
|
||||||
commands: parsed.commands,
|
|
||||||
savedAt: parsed.savedAt,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Session] Failed to load persisted snapshot for ${serverId}`, error);
|
console.error(`[Session] Failed to load persisted snapshot for ${serverId}`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistSessionSnapshot(
|
async function persistSessionSnapshot(serverId: string, snapshot: { agents: AgentSnapshotPayload[]; commands: Command[] }) {
|
||||||
serverId: string,
|
|
||||||
snapshot: { agents: AgentSnapshotPayload[]; commands: Command[]; directory: AgentDirectoryEntry[] }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const payload: LightweightSessionSnapshot = {
|
const payload: PersistedSessionSnapshot = {
|
||||||
version: SESSION_SNAPSHOT_VERSION,
|
agents: snapshot.agents,
|
||||||
directory: snapshot.directory.map(serializeDirectoryEntry),
|
commands: snapshot.commands,
|
||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
...(SESSION_DEBUG_PERSIST_FULL_SNAPSHOT
|
|
||||||
? { full: { agents: snapshot.agents, commands: snapshot.commands } }
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
const persistenceMode = SESSION_DEBUG_PERSIST_FULL_SNAPSHOT ? "full" : "directory";
|
|
||||||
await AsyncStorage.setItem(getSessionSnapshotStorageKey(serverId), JSON.stringify(payload));
|
await AsyncStorage.setItem(getSessionSnapshotStorageKey(serverId), JSON.stringify(payload));
|
||||||
console.log("[Session] Persisted snapshot", {
|
|
||||||
serverId,
|
|
||||||
agentCount: snapshot.directory.length,
|
|
||||||
mode: persistenceMode,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Session] Failed to persist snapshot for ${serverId}`, error);
|
console.error(`[Session] Failed to persist snapshot for ${serverId}`, error);
|
||||||
}
|
}
|
||||||
@@ -410,92 +312,6 @@ function buildAgentDirectoryEntries(serverId: string, agents: Map<string, Agent>
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAgentsFromDirectoryEntries(serverId: string, entries: AgentDirectoryEntry[]) {
|
|
||||||
const agents = new Map<string, Agent>();
|
|
||||||
for (const entry of entries) {
|
|
||||||
const lastActivity = entry.lastActivityAt ?? new Date();
|
|
||||||
agents.set(entry.id, {
|
|
||||||
serverId,
|
|
||||||
id: entry.id,
|
|
||||||
provider: entry.provider,
|
|
||||||
status: entry.status,
|
|
||||||
createdAt: lastActivity,
|
|
||||||
updatedAt: lastActivity,
|
|
||||||
lastUserMessageAt: null,
|
|
||||||
lastActivityAt: lastActivity,
|
|
||||||
sessionId: null,
|
|
||||||
capabilities: DEFAULT_AGENT_CAPABILITIES,
|
|
||||||
currentModeId: null,
|
|
||||||
availableModes: [],
|
|
||||||
pendingPermissions: [],
|
|
||||||
persistence: null,
|
|
||||||
lastUsage: undefined,
|
|
||||||
lastError: null,
|
|
||||||
title: entry.title ?? null,
|
|
||||||
cwd: entry.cwd,
|
|
||||||
model: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return agents;
|
|
||||||
}
|
|
||||||
|
|
||||||
function capMessages(entries: MessageEntry[], serverId: string): MessageEntry[] {
|
|
||||||
if (entries.length <= MAX_MESSAGES_PER_AGENT) {
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
const trimmed = entries.slice(-MAX_MESSAGES_PER_AGENT);
|
|
||||||
console.log("[Session] Trimmed messages", {
|
|
||||||
serverId,
|
|
||||||
before: entries.length,
|
|
||||||
after: trimmed.length,
|
|
||||||
cap: MAX_MESSAGES_PER_AGENT,
|
|
||||||
});
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function capAgentStreamState(map: Map<string, StreamItem[]>, serverId: string): Map<string, StreamItem[]> {
|
|
||||||
let trimmed = false;
|
|
||||||
const trimmedAgents: Array<{ agentId: string; before: number; after: number }> = [];
|
|
||||||
const next = new Map(map);
|
|
||||||
|
|
||||||
for (const [agentId, stream] of map.entries()) {
|
|
||||||
if (stream.length <= MAX_STREAM_ITEMS_PER_AGENT) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
trimmed = true;
|
|
||||||
const capped = stream.slice(-MAX_STREAM_ITEMS_PER_AGENT);
|
|
||||||
next.set(agentId, capped);
|
|
||||||
trimmedAgents.push({
|
|
||||||
agentId,
|
|
||||||
before: stream.length,
|
|
||||||
after: capped.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed) {
|
|
||||||
console.log("[Session] Trimmed stream buffers", {
|
|
||||||
serverId,
|
|
||||||
cap: MAX_STREAM_ITEMS_PER_AGENT,
|
|
||||||
agents: trimmedAgents,
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterSessionStorePayload(payload: Partial<SessionContextValue>): Partial<SessionContextValue> {
|
|
||||||
const { messages: _messages, agentStreamState: _agentStreamState, ...rest } = payload;
|
|
||||||
const next: Partial<SessionContextValue> = { ...rest };
|
|
||||||
if (_messages !== undefined) {
|
|
||||||
next.messages = EMPTY_MESSAGES;
|
|
||||||
}
|
|
||||||
if (_agentStreamState !== undefined) {
|
|
||||||
next.agentStreamState = EMPTY_AGENT_STREAM_STATE;
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface SessionContextValue {
|
export interface SessionContextValue {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -647,15 +463,14 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
|
|
||||||
const syncSessionPartial = useCallback(
|
const syncSessionPartial = useCallback(
|
||||||
(partial: Partial<SessionContextValue>) => {
|
(partial: Partial<SessionContextValue>) => {
|
||||||
const filtered = filterSessionStorePayload(partial);
|
|
||||||
if (!hasRegisteredSessionRef.current) {
|
if (!hasRegisteredSessionRef.current) {
|
||||||
pendingSessionUpdatesRef.current = {
|
pendingSessionUpdatesRef.current = {
|
||||||
...pendingSessionUpdatesRef.current,
|
...pendingSessionUpdatesRef.current,
|
||||||
...filtered,
|
...partial,
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateSession(serverId, filtered);
|
updateSession(serverId, partial);
|
||||||
},
|
},
|
||||||
[serverId, updateSession]
|
[serverId, updateSession]
|
||||||
);
|
);
|
||||||
@@ -717,35 +532,9 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
}, [updateIsPlayingAudio]);
|
}, [updateIsPlayingAudio]);
|
||||||
const [focusedAgentOverride, setFocusedAgentOverride] = useState<string | null>(null);
|
const [focusedAgentOverride, setFocusedAgentOverride] = useState<string | null>(null);
|
||||||
const [orchestratorFocusedAgentId, setOrchestratorFocusedAgentId] = useState<string | null>(null);
|
const [orchestratorFocusedAgentId, setOrchestratorFocusedAgentId] = useState<string | null>(null);
|
||||||
const [messages, setLocalMessages] = useState<MessageEntry[]>([]);
|
const [messages, setMessages] = useSyncedSessionState("messages", () => [], syncSessionField);
|
||||||
const setMessages = useCallback<SessionContextValue["setMessages"]>(
|
|
||||||
(valueOrUpdater) => {
|
|
||||||
setLocalMessages((previous) => {
|
|
||||||
const next =
|
|
||||||
typeof valueOrUpdater === "function"
|
|
||||||
? (valueOrUpdater as (prev: MessageEntry[]) => MessageEntry[])(previous)
|
|
||||||
: valueOrUpdater;
|
|
||||||
return capMessages(next, serverId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[serverId]
|
|
||||||
);
|
|
||||||
const [currentAssistantMessage, setCurrentAssistantMessage] = useSyncedSessionState("currentAssistantMessage", "", syncSessionField);
|
const [currentAssistantMessage, setCurrentAssistantMessage] = useSyncedSessionState("currentAssistantMessage", "", syncSessionField);
|
||||||
const [agentStreamState, setLocalAgentStreamState] = useState<Map<string, StreamItem[]>>(
|
const [agentStreamState, setAgentStreamState] = useSyncedSessionState("agentStreamState", () => new Map<string, StreamItem[]>(), syncSessionField);
|
||||||
() => new Map<string, StreamItem[]>()
|
|
||||||
);
|
|
||||||
const setAgentStreamState = useCallback<SessionContextValue["setAgentStreamState"]>(
|
|
||||||
(valueOrUpdater) => {
|
|
||||||
setLocalAgentStreamState((previous) => {
|
|
||||||
const next =
|
|
||||||
typeof valueOrUpdater === "function"
|
|
||||||
? (valueOrUpdater as (prev: Map<string, StreamItem[]>) => Map<string, StreamItem[]>)(previous)
|
|
||||||
: valueOrUpdater;
|
|
||||||
return capAgentStreamState(next, serverId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[serverId]
|
|
||||||
);
|
|
||||||
const [initializingAgents, setInitializingAgents] = useSyncedSessionState("initializingAgents", () => new Map<string, boolean>(), syncSessionField);
|
const [initializingAgents, setInitializingAgents] = useSyncedSessionState("initializingAgents", () => new Map<string, boolean>(), syncSessionField);
|
||||||
const [hasHydratedAgents, setHasHydratedAgents] = useSyncedSessionState("hasHydratedAgents", false, syncSessionField);
|
const [hasHydratedAgents, setHasHydratedAgents] = useSyncedSessionState("hasHydratedAgents", false, syncSessionField);
|
||||||
|
|
||||||
@@ -774,24 +563,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const [queuedMessages, setQueuedMessages] = useSyncedSessionState("queuedMessages", () => new Map(), syncSessionField);
|
const [queuedMessages, setQueuedMessages] = useSyncedSessionState("queuedMessages", () => new Map(), syncSessionField);
|
||||||
const heavyStateRef = useRef<SessionHeavyState>({
|
|
||||||
messages,
|
|
||||||
agentStreamState,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
heavyStateRef.current = {
|
|
||||||
messages,
|
|
||||||
agentStreamState,
|
|
||||||
};
|
|
||||||
publishSessionHeavyState(serverId, heavyStateRef.current);
|
|
||||||
}, [agentStreamState, messages, serverId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearSessionHeavyState(serverId);
|
|
||||||
};
|
|
||||||
}, [serverId]);
|
|
||||||
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
|
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
|
||||||
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
|
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
|
||||||
const providerModelRequestIdsRef = useRef<Map<AgentProvider, string>>(new Map());
|
const providerModelRequestIdsRef = useRef<Map<AgentProvider, string>>(new Map());
|
||||||
@@ -830,66 +601,33 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFullSnapshot = snapshot.kind === "legacy" || Boolean(snapshot.full);
|
const { agents: hydratedAgents, pendingPermissions: hydratedPermissions } = buildSessionStateFromSnapshots(
|
||||||
const snapshotMode = hasFullSnapshot ? "full" : "directory";
|
|
||||||
const agentCount =
|
|
||||||
snapshot.kind === "legacy"
|
|
||||||
? snapshot.agents.length
|
|
||||||
: snapshot.directory.length;
|
|
||||||
|
|
||||||
console.log("[Session] Hydrating snapshot", {
|
|
||||||
serverId,
|
serverId,
|
||||||
mode: snapshotMode,
|
snapshot.agents
|
||||||
agentCount,
|
);
|
||||||
|
|
||||||
|
let applied = false;
|
||||||
|
setAgents((prev) => {
|
||||||
|
if (prev.size > 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
applied = true;
|
||||||
|
return hydratedAgents;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasFullSnapshot) {
|
if (!applied) {
|
||||||
const fullAgents = snapshot.kind === "legacy" ? snapshot.agents : snapshot.full?.agents ?? [];
|
return;
|
||||||
const fullCommands = snapshot.kind === "legacy" ? snapshot.commands : snapshot.full?.commands ?? [];
|
|
||||||
|
|
||||||
const { agents: hydratedAgents, pendingPermissions: hydratedPermissions } =
|
|
||||||
buildSessionStateFromSnapshots(serverId, fullAgents);
|
|
||||||
|
|
||||||
let applied = false;
|
|
||||||
setAgents((prev) => {
|
|
||||||
if (prev.size > 0) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
applied = true;
|
|
||||||
return hydratedAgents;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!applied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingPermissions(hydratedPermissions);
|
|
||||||
setCommands((prev) => {
|
|
||||||
if (prev.size > 0) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return new Map(fullCommands.map((command) => [command.id, command]));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const stubAgents = buildAgentsFromDirectoryEntries(serverId, snapshot.directory);
|
|
||||||
let applied = false;
|
|
||||||
setAgents((prev) => {
|
|
||||||
if (prev.size > 0) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
applied = true;
|
|
||||||
return stubAgents;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!applied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingPermissions(new Map());
|
|
||||||
setCommands((prev) => (prev.size > 0 ? prev : new Map()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHasHydratedAgents(true);
|
setPendingPermissions(hydratedPermissions);
|
||||||
|
const commandEntries = snapshot.commands ?? [];
|
||||||
|
setCommands((prev) => {
|
||||||
|
if (prev.size > 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return new Map(commandEntries.map((command) => [command.id, command]));
|
||||||
|
});
|
||||||
|
setHasHydratedAgents(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
void hydrateFromSnapshot();
|
void hydrateFromSnapshot();
|
||||||
@@ -1094,7 +832,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
serverId,
|
serverId,
|
||||||
agentsList
|
agentsList
|
||||||
);
|
);
|
||||||
const directoryEntries = buildAgentDirectoryEntries(serverId, hydratedAgents);
|
|
||||||
const normalizedCommands = commandsList.map((command) => command as Command);
|
const normalizedCommands = commandsList.map((command) => command as Command);
|
||||||
|
|
||||||
setAgents(hydratedAgents);
|
setAgents(hydratedAgents);
|
||||||
@@ -1136,7 +873,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
|
|
||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
void persistSessionSnapshot(serverId, { agents: agentsList, commands: normalizedCommands, directory: directoryEntries });
|
void persistSessionSnapshot(serverId, { agents: agentsList, commands: normalizedCommands });
|
||||||
setHasHydratedAgents(true);
|
setHasHydratedAgents(true);
|
||||||
updateConnectionStatus(serverId, { status: "online", lastOnlineAt: new Date().toISOString(), sessionReady: true });
|
updateConnectionStatus(serverId, { status: "online", lastOnlineAt: new Date().toISOString(), sessionReady: true });
|
||||||
});
|
});
|
||||||
@@ -2306,10 +2043,10 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialSnapshot = initialSessionValueRef.current as SessionContextValue;
|
const initialSnapshot = initialSessionValueRef.current as SessionContextValue;
|
||||||
const initialPayload = filterSessionStorePayload({
|
const initialPayload = {
|
||||||
...initialSnapshot,
|
...initialSnapshot,
|
||||||
...pendingSessionUpdatesRef.current,
|
...pendingSessionUpdatesRef.current,
|
||||||
}) as SessionContextValue;
|
};
|
||||||
pendingSessionUpdatesRef.current = {};
|
pendingSessionUpdatesRef.current = {};
|
||||||
setSession(serverId, initialPayload);
|
setSession(serverId, initialPayload);
|
||||||
hasRegisteredSessionRef.current = true;
|
hasRegisteredSessionRef.current = true;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import type { SessionContextValue } from "@/contexts/session-context";
|
import type { SessionContextValue } from "@/contexts/session-context";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useSessionHeavyState } from "@/stores/session-heavy-state";
|
|
||||||
|
|
||||||
export class DaemonSessionUnavailableError extends Error {
|
export class DaemonSessionUnavailableError extends Error {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -38,21 +37,7 @@ export function useDaemonSession(serverId?: string | null, options?: UseDaemonSe
|
|||||||
},
|
},
|
||||||
[serverId]
|
[serverId]
|
||||||
);
|
);
|
||||||
const baseSession = useSessionStore(selectSession);
|
const session = useSessionStore(selectSession);
|
||||||
const heavyState = useSessionHeavyState(serverId ?? null);
|
|
||||||
const session = useMemo(() => {
|
|
||||||
if (!baseSession) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!serverId || !heavyState) {
|
|
||||||
return baseSession;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...baseSession,
|
|
||||||
messages: heavyState.messages,
|
|
||||||
agentStreamState: heavyState.agentStreamState,
|
|
||||||
} as SessionContextValue;
|
|
||||||
}, [baseSession, heavyState, serverId]);
|
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const alertedDaemonsRef = useRef<Set<string>>(new Set());
|
const alertedDaemonsRef = useRef<Set<string>>(new Set());
|
||||||
const loggedDaemonsRef = useRef<Set<string>>(new Set());
|
const loggedDaemonsRef = useRef<Set<string>>(new Set());
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||||
import { useSessionStore, type SessionData } from "@/stores/session-store";
|
import { useSessionStore, type SessionData } from "@/stores/session-store";
|
||||||
import { useSessionHeavyDirectory } from "@/stores/session-heavy-state";
|
|
||||||
|
|
||||||
export function useSessionDirectory(): Map<string, SessionData> {
|
export function useSessionDirectory(): Map<string, SessionData> {
|
||||||
const sessions = useSessionStore((state) => state.sessions);
|
const sessions = useSessionStore((state) => state.sessions);
|
||||||
const heavyDirectory = useSessionHeavyDirectory();
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const merged = new Map<string, SessionData>();
|
return new Map<string, SessionData>(Object.entries(sessions));
|
||||||
for (const [serverId, session] of Object.entries(sessions)) {
|
}, [sessions]);
|
||||||
const heavy = heavyDirectory.get(serverId);
|
|
||||||
if (heavy) {
|
|
||||||
merged.set(serverId, {
|
|
||||||
...session,
|
|
||||||
messages: heavy.messages,
|
|
||||||
agentStreamState: heavy.agentStreamState,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
merged.set(serverId, session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}, [heavyDirectory, sessions]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionSelector<T> = (session: SessionData | null) => T;
|
type SessionSelector<T> = (session: SessionData | null) => T;
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
|
||||||
import type { MessageEntry } from "@/contexts/session-context";
|
|
||||||
import type { StreamItem } from "@/types/stream";
|
|
||||||
|
|
||||||
export type SessionHeavyState = {
|
|
||||||
messages: MessageEntry[];
|
|
||||||
agentStreamState: Map<string, StreamItem[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_MESSAGES: MessageEntry[] = [];
|
|
||||||
const EMPTY_AGENT_STREAM_STATE = new Map<string, StreamItem[]>();
|
|
||||||
const EMPTY_HEAVY_STATE: SessionHeavyState = {
|
|
||||||
messages: EMPTY_MESSAGES,
|
|
||||||
agentStreamState: EMPTY_AGENT_STREAM_STATE,
|
|
||||||
};
|
|
||||||
|
|
||||||
const heavyStateRegistry = new Map<string, SessionHeavyState>();
|
|
||||||
const heavyStateListeners = new Map<string, Set<() => void>>();
|
|
||||||
const directoryListeners = new Set<() => void>();
|
|
||||||
|
|
||||||
function getServerListeners(serverId: string): Set<() => void> {
|
|
||||||
let listeners = heavyStateListeners.get(serverId);
|
|
||||||
if (!listeners) {
|
|
||||||
listeners = new Set();
|
|
||||||
heavyStateListeners.set(serverId, listeners);
|
|
||||||
}
|
|
||||||
return listeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyServer(serverId: string) {
|
|
||||||
const listeners = heavyStateListeners.get(serverId);
|
|
||||||
if (listeners) {
|
|
||||||
for (const listener of listeners) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const listener of directoryListeners) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function publishSessionHeavyState(serverId: string, state: SessionHeavyState) {
|
|
||||||
heavyStateRegistry.set(serverId, state);
|
|
||||||
notifyServer(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSessionHeavyState(serverId: string) {
|
|
||||||
if (heavyStateRegistry.delete(serverId)) {
|
|
||||||
notifyServer(serverId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSessionHeavyState(serverId: string): SessionHeavyState | null {
|
|
||||||
return heavyStateRegistry.get(serverId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToServer(serverId: string | null, listener: () => void) {
|
|
||||||
if (!serverId) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
const listeners = getServerListeners(serverId);
|
|
||||||
listeners.add(listener);
|
|
||||||
return () => {
|
|
||||||
listeners.delete(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToDirectory(listener: () => void) {
|
|
||||||
directoryListeners.add(listener);
|
|
||||||
return () => {
|
|
||||||
directoryListeners.delete(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerSnapshot(serverId: string | null): SessionHeavyState {
|
|
||||||
if (!serverId) {
|
|
||||||
return EMPTY_HEAVY_STATE;
|
|
||||||
}
|
|
||||||
return heavyStateRegistry.get(serverId) ?? EMPTY_HEAVY_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDirectorySnapshot(): Map<string, SessionHeavyState> {
|
|
||||||
return new Map(heavyStateRegistry.entries());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSessionHeavyState(serverId: string | null): SessionHeavyState | null {
|
|
||||||
const snapshot = useSyncExternalStore(
|
|
||||||
(listener) => subscribeToServer(serverId, listener),
|
|
||||||
() => getServerSnapshot(serverId),
|
|
||||||
() => getServerSnapshot(serverId)
|
|
||||||
);
|
|
||||||
if (!serverId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSessionHeavyDirectory(): Map<string, SessionHeavyState> {
|
|
||||||
return useSyncExternalStore(subscribeToDirectory, getDirectorySnapshot, getDirectorySnapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user