refactor: rename session_state to agent_list and add subscriptions

This commit is contained in:
Mohamed Boudra
2026-01-30 13:39:40 +07:00
parent 5b32d0a56c
commit 0618dc6b0d
45 changed files with 2163 additions and 257 deletions
@@ -3,18 +3,18 @@ import type { ReactNode } from "react";
import { useDaemonRegistry, type DaemonProfile } from "./daemon-registry-context";
export type ConnectionState =
| { status: "idle"; lastError: null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: false }
| { status: "connecting"; lastError: null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean }
| { status: "online"; lastError: null; lastOnlineAt: string; sessionReady: boolean; hasEverReceivedSessionState: boolean }
| { status: "offline"; lastError: string | null; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean }
| { status: "error"; lastError: string; lastOnlineAt: string | null; sessionReady: false; hasEverReceivedSessionState: boolean };
| { status: "idle"; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: false }
| { status: "connecting"; lastError: null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
| { status: "online"; lastError: null; lastOnlineAt: string; agentListReady: boolean; hasEverReceivedAgentList: boolean }
| { status: "offline"; lastError: string | null; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean }
| { status: "error"; lastError: string; lastOnlineAt: string | null; agentListReady: false; hasEverReceivedAgentList: boolean };
export type ConnectionStatus = ConnectionState["status"];
type ConnectionStateUpdate =
| { status: "idle" }
| { status: "connecting"; lastOnlineAt?: string | null }
| { status: "online"; lastOnlineAt: string; sessionReady?: boolean }
| { status: "online"; lastOnlineAt: string; agentListReady?: boolean }
| { status: "offline"; lastError?: string | null; lastOnlineAt?: string | null }
| { status: "error"; lastError: string; lastOnlineAt?: string | null };
@@ -35,8 +35,8 @@ function createDefaultConnectionState(): ConnectionState {
status: "idle",
lastError: null,
lastOnlineAt: null,
sessionReady: false,
hasEverReceivedSessionState: false,
agentListReady: false,
hasEverReceivedAgentList: false,
};
}
@@ -50,43 +50,43 @@ function resolveNextConnectionState(
status: "idle",
lastError: null,
lastOnlineAt: existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: false,
agentListReady: false,
hasEverReceivedAgentList: false,
};
case "connecting":
return {
status: "connecting",
lastError: null,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
case "online":
const currentSessionReady = update.sessionReady ?? (existing.status === "online" ? existing.sessionReady : false);
const currentAgentListReady = update.agentListReady ?? (existing.status === "online" ? existing.agentListReady : false);
return {
status: "online",
lastError: null,
lastOnlineAt: update.lastOnlineAt,
sessionReady: currentSessionReady,
hasEverReceivedSessionState:
currentSessionReady || existing.hasEverReceivedSessionState || false,
agentListReady: currentAgentListReady,
hasEverReceivedAgentList:
currentAgentListReady || existing.hasEverReceivedAgentList || false,
};
case "offline":
return {
status: "offline",
lastError: update.lastError ?? null,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
case "error":
return {
status: "error",
lastError: update.lastError,
lastOnlineAt: update.lastOnlineAt ?? existing.lastOnlineAt,
sessionReady: false,
hasEverReceivedSessionState: existing.hasEverReceivedSessionState ?? false,
agentListReady: false,
hasEverReceivedAgentList: existing.hasEverReceivedAgentList ?? false,
};
}
}
+83 -24
View File
@@ -247,6 +247,7 @@ function normalizeAgentSnapshot(
attentionReason: snapshot.attentionReason ?? null,
attentionTimestamp,
archivedAt,
labels: snapshot.labels,
};
}
@@ -411,6 +412,7 @@ export function SessionProvider({
| null
>(null);
const hasRequestedInitialSnapshotRef = useRef(false);
const agentUpdatesSubscriptionIdRef = useRef<string | null>(null);
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null
);
@@ -674,6 +676,15 @@ export function SessionProvider({
useEffect(() => {
if (!connectionSnapshot.isConnected) {
hasRequestedInitialSnapshotRef.current = false;
const subscriptionId = agentUpdatesSubscriptionIdRef.current;
if (subscriptionId && client) {
try {
client.unsubscribeAgentUpdates(subscriptionId);
} catch {
// no-op
}
}
agentUpdatesSubscriptionIdRef.current = null;
return;
}
if (hasRequestedInitialSnapshotRef.current) {
@@ -687,15 +698,21 @@ export function SessionProvider({
let retryCount = 0;
const requestSessionState = () => {
const requestAgentList = () => {
console.log(
`[Session] Requesting session_state (attempt ${retryCount + 1}/${
`[Session] Requesting agent_list (attempt ${retryCount + 1}/${
MAX_RETRIES + 1
})`,
{ serverId }
);
void client
.requestSessionState();
.requestAgentList({ filter: { labels: { ui: "true" } } });
if (!agentUpdatesSubscriptionIdRef.current) {
agentUpdatesSubscriptionIdRef.current = client.subscribeAgentUpdates({
subscriptionId: `app:${serverId}`,
filter: { labels: { ui: "true" } },
});
}
if (sessionStateTimeoutRef.current) {
clearTimeout(sessionStateTimeoutRef.current);
@@ -705,7 +722,7 @@ export function SessionProvider({
if (retryCount < MAX_RETRIES) {
retryCount++;
console.warn(
`[Session] session_state timeout, retrying in ${RETRY_DELAY_MS}ms`,
`[Session] agent_list timeout, retrying in ${RETRY_DELAY_MS}ms`,
{
serverId,
attempt: retryCount,
@@ -714,11 +731,11 @@ export function SessionProvider({
);
setTimeout(() => {
requestSessionState();
requestAgentList();
}, RETRY_DELAY_MS);
} else {
console.error(
`[Session] session_state failed after ${MAX_RETRIES} retries`,
`[Session] agent_list failed after ${MAX_RETRIES} retries`,
{ serverId }
);
@@ -726,13 +743,13 @@ export function SessionProvider({
updateConnectionStatus(serverId, {
status: "online",
lastOnlineAt: new Date().toISOString(),
sessionReady: true,
agentListReady: true,
});
}
}, TIMEOUT_MS);
};
requestSessionState();
requestAgentList();
return () => {
if (sessionStateTimeoutRef.current) {
@@ -744,10 +761,10 @@ export function SessionProvider({
// Daemon message handlers - directly update Zustand store
useEffect(() => {
console.log("[Session] Setting up session_state listener for", serverId);
console.log("[Session] Setting up agent_list listener for", serverId);
const unsubSessionState = client.on("session_state", (message) => {
if (message.type !== "session_state") return;
const unsubAgentList = client.on("agent_list", (message) => {
if (message.type !== "agent_list") return;
if (sessionStateTimeoutRef.current) {
clearTimeout(sessionStateTimeoutRef.current);
@@ -757,7 +774,7 @@ export function SessionProvider({
const { agents: agentsList } = message.payload;
console.log(
"[Session] ✅ Received session_state:",
"[Session] ✅ Received agent_list:",
agentsList.length,
"agents"
);
@@ -849,16 +866,57 @@ export function SessionProvider({
updateConnectionStatus(serverId, {
status: "online",
lastOnlineAt: new Date().toISOString(),
sessionReady: true,
agentListReady: true,
});
});
const unsubAgentState = client.on("agent_state", (message) => {
if (message.type !== "agent_state") return;
const snapshot = message.payload;
const agent = normalizeAgentSnapshot(snapshot, serverId);
const unsubAgentUpdate = client.on("agent_update", (message) => {
if (message.type !== "agent_update") return;
const update = message.payload;
console.log("[Session] Agent state update:", agent.id, agent.status);
if (update.kind === "remove") {
const agentId = update.agentId;
previousAgentStatusRef.current.delete(agentId);
setAgents(serverId, (prev) => {
if (!prev.has(agentId)) {
return prev;
}
const next = new Map(prev);
next.delete(agentId);
return next;
});
setPendingPermissions(serverId, (prev) => {
if (prev.size === 0) {
return prev;
}
let changed = false;
const next = new Map(prev);
for (const [key, pending] of Array.from(next.entries())) {
if (pending.agentId === agentId) {
next.delete(key);
changed = true;
}
}
return changed ? next : prev;
});
setQueuedMessages(serverId, (prev) => {
if (!prev.has(agentId)) {
return prev;
}
const next = new Map(prev);
next.delete(agentId);
return next;
});
return;
}
const agent = normalizeAgentSnapshot(update.agent, serverId);
console.log("[Session] Agent update:", agent.id, agent.status);
setAgents(serverId, (prev) => {
const next = new Map(prev);
@@ -967,7 +1025,7 @@ export function SessionProvider({
});
// NOTE: We don't update lastActivityAt on every stream event to prevent
// cascading rerenders. The agent_state handler updates agent.lastActivityAt
// cascading rerenders. The agent_update handler updates agent.lastActivityAt
// on status changes, which is sufficient for sorting and display purposes.
});
@@ -1462,8 +1520,8 @@ export function SessionProvider({
});
return () => {
unsubSessionState();
unsubAgentState();
unsubAgentList();
unsubAgentUpdate();
unsubAgentStream();
unsubAgentStreamSnapshot();
unsubStatus();
@@ -1834,7 +1892,7 @@ export function SessionProvider({
[encodeImages, serverId, client, setAgentStreamTail]
);
// Keep the ref updated so the agent_state handler can call it
// Keep the ref updated so the agent_update handler can call it
sendAgentMessageRef.current = sendAgentMessage;
const cancelAgentRun = useCallback(
@@ -1934,6 +1992,7 @@ export function SessionProvider({
}
return client.createAgent({
config,
labels: { ui: "true" },
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
...(git ? { git } : {}),
@@ -2166,9 +2225,9 @@ export function SessionProvider({
return;
}
try {
client.requestSessionState();
client.requestAgentList({ filter: { labels: { ui: "true" } } });
} catch (error: any) {
console.error("[Session] Failed to refresh session:", error);
console.error("[Session] Failed to refresh agent list:", error);
}
}, [client, serverId]);