feat: broadcast pilot agent traces via WebSocket for live status on reconnect
Agent trace logs are now stored in-memory (pilotRunStore) and broadcast through the existing /ws/crm-updates WebSocket channel. When a client reconnects, it receives a pilot.catchup with all accumulated logs so the user sees agent progress even after page reload. Three new WS event types: pilot.trace, pilot.finished, pilot.catchup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "../../utils/prisma";
|
||||
import { getActivePilotRun } from "../../utils/pilotRunStore";
|
||||
|
||||
function mapChannel(channel: string) {
|
||||
if (channel === "TELEGRAM") return "Telegram";
|
||||
@@ -19,6 +20,10 @@ const peerTeamById = new Map<string, string>();
|
||||
const lastSignatureByTeam = new Map<string, string>();
|
||||
const lastMsgCreatedAtByTeam = new Map<string, Date>();
|
||||
|
||||
// Conversation-level tracking for pilot events
|
||||
const peersByConversation = new Map<string, Set<any>>();
|
||||
const peerConversationById = new Map<string, string>();
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function parseCookies(raw: string | null) {
|
||||
@@ -42,19 +47,41 @@ function attachPeerToTeam(peer: any, teamId: string) {
|
||||
peerTeamById.set(String(peer.id), teamId);
|
||||
}
|
||||
|
||||
function attachPeerToConversation(peer: any, conversationId: string) {
|
||||
if (!peersByConversation.has(conversationId)) peersByConversation.set(conversationId, new Set());
|
||||
peersByConversation.get(conversationId)?.add(peer);
|
||||
peerConversationById.set(String(peer.id), conversationId);
|
||||
}
|
||||
|
||||
function detachPeer(peer: any) {
|
||||
const key = String(peer.id);
|
||||
const teamId = peerTeamById.get(key);
|
||||
if (!teamId) return;
|
||||
peerTeamById.delete(key);
|
||||
|
||||
const peers = peersByTeam.get(teamId);
|
||||
if (!peers) return;
|
||||
peers.delete(peer);
|
||||
if (peers.size === 0) {
|
||||
peersByTeam.delete(teamId);
|
||||
lastSignatureByTeam.delete(teamId);
|
||||
lastMsgCreatedAtByTeam.delete(teamId);
|
||||
// Detach from team
|
||||
const teamId = peerTeamById.get(key);
|
||||
if (teamId) {
|
||||
peerTeamById.delete(key);
|
||||
const peers = peersByTeam.get(teamId);
|
||||
if (peers) {
|
||||
peers.delete(peer);
|
||||
if (peers.size === 0) {
|
||||
peersByTeam.delete(teamId);
|
||||
lastSignatureByTeam.delete(teamId);
|
||||
lastMsgCreatedAtByTeam.delete(teamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detach from conversation
|
||||
const convId = peerConversationById.get(key);
|
||||
if (convId) {
|
||||
peerConversationById.delete(key);
|
||||
const convPeers = peersByConversation.get(convId);
|
||||
if (convPeers) {
|
||||
convPeers.delete(peer);
|
||||
if (convPeers.size === 0) {
|
||||
peersByConversation.delete(convId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +110,7 @@ async function validateSessionFromPeer(peer: any) {
|
||||
]);
|
||||
|
||||
if (!user || !team || !conv) return null;
|
||||
return { teamId };
|
||||
return { teamId, conversationId };
|
||||
}
|
||||
|
||||
async function computeTeamSignature(teamId: string) {
|
||||
@@ -209,6 +236,17 @@ function ensurePoll() {
|
||||
}, TEAM_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public: broadcast to all peers connected for a given conversation
|
||||
// ---------------------------------------------------------------------------
|
||||
export function broadcastToConversation(conversationId: string, payload: Record<string, unknown>) {
|
||||
const peers = peersByConversation.get(conversationId);
|
||||
if (!peers) return;
|
||||
for (const peer of peers) {
|
||||
sendJson(peer, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
const session = await validateSessionFromPeer(peer);
|
||||
@@ -218,9 +256,20 @@ export default defineWebSocketHandler({
|
||||
}
|
||||
|
||||
attachPeerToTeam(peer, session.teamId);
|
||||
attachPeerToConversation(peer, session.conversationId);
|
||||
ensurePoll();
|
||||
sendJson(peer, { type: "realtime.connected", at: new Date().toISOString() });
|
||||
void pollAndBroadcast();
|
||||
|
||||
// Send catch-up for active pilot run
|
||||
const activeRun = getActivePilotRun(session.conversationId);
|
||||
if (activeRun) {
|
||||
sendJson(peer, {
|
||||
type: "pilot.catchup",
|
||||
logs: activeRun.logs,
|
||||
at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
close(peer) {
|
||||
|
||||
Reference in New Issue
Block a user