agentgui 1.0.154 → 1.0.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/database.js CHANGED
@@ -430,6 +430,17 @@ export const queries = {
430
430
  });
431
431
  },
432
432
 
433
+ getLastUserMessage(conversationId) {
434
+ const stmt = prep(
435
+ "SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
436
+ );
437
+ const msg = stmt.get(conversationId);
438
+ if (msg && typeof msg.content === 'string') {
439
+ try { msg.content = JSON.parse(msg.content); } catch (_) {}
440
+ }
441
+ return msg || null;
442
+ },
443
+
433
444
  getPaginatedMessages(conversationId, limit = 50, offset = 0) {
434
445
  const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
435
446
  const total = countStmt.get(conversationId).count;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.154",
3
+ "version": "1.0.156",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -27,7 +27,6 @@ const messageQueues = new Map();
27
27
  const rateLimitState = new Map();
28
28
  const STUCK_AGENT_THRESHOLD_MS = 600000;
29
29
  const NO_PID_GRACE_PERIOD_MS = 60000;
30
- const STALE_SESSION_MIN_AGE_MS = 30000;
31
30
  const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60000;
32
31
 
33
32
  const debugLog = (msg) => {
@@ -231,10 +230,8 @@ const server = http.createServer(async (req, res) => {
231
230
  const conv = queries.getConversation(convMatch[1]);
232
231
  if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
233
232
 
234
- // Check both in-memory and database for active streaming status
235
233
  const latestSession = queries.getLatestSession(convMatch[1]);
236
- const isActivelyStreaming = activeExecutions.has(convMatch[1]) ||
237
- (latestSession && latestSession.status === 'active');
234
+ const isActivelyStreaming = activeExecutions.has(convMatch[1]);
238
235
 
239
236
  sendJSON(req, res, 200, {
240
237
  conversation: conv,
@@ -279,17 +276,41 @@ const server = http.createServer(async (req, res) => {
279
276
  const conv = queries.getConversation(conversationId);
280
277
  if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
281
278
  const body = await parseBody(req);
279
+ const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
282
280
  const idempotencyKey = body.idempotencyKey || null;
283
281
  const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
284
282
  queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
285
283
  broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
284
+
285
+ if (activeExecutions.has(conversationId)) {
286
+ if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
287
+ messageQueues.get(conversationId).push({ content: body.content, agentId, messageId: message.id });
288
+ const queueLength = messageQueues.get(conversationId).length;
289
+ broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: message.id, timestamp: Date.now() });
290
+ sendJSON(req, res, 200, { message, queued: true, queuePosition: queueLength, idempotencyKey });
291
+ return;
292
+ }
293
+
286
294
  const session = queries.createSession(conversationId);
287
295
  queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
288
- sendJSON(req, res, 201, { message, session, idempotencyKey });
289
- // Fire-and-forget with proper error handling
290
- processMessage(conversationId, message.id, body.content, body.agentId)
291
- .catch(err => debugLog(`[processMessage] Uncaught error: ${err.message}`));
292
- return;
296
+
297
+ activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
298
+ queries.setIsStreaming(conversationId, true);
299
+
300
+ broadcastSync({
301
+ type: 'streaming_start',
302
+ sessionId: session.id,
303
+ conversationId,
304
+ messageId: message.id,
305
+ agentId,
306
+ timestamp: Date.now()
307
+ });
308
+
309
+ sendJSON(req, res, 201, { message, session, idempotencyKey });
310
+
311
+ processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId)
312
+ .catch(err => debugLog(`[messages] Uncaught error: ${err.message}`));
313
+ return;
293
314
  }
294
315
  }
295
316
 
@@ -323,7 +344,8 @@ const server = http.createServer(async (req, res) => {
323
344
  const session = queries.createSession(conversationId);
324
345
  queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
325
346
 
326
- sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
347
+ activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
348
+ queries.setIsStreaming(conversationId, true);
327
349
 
328
350
  broadcastSync({
329
351
  type: 'streaming_start',
@@ -334,6 +356,8 @@ const server = http.createServer(async (req, res) => {
334
356
  timestamp: Date.now()
335
357
  });
336
358
 
359
+ sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
360
+
337
361
  processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId)
338
362
  .catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
339
363
  return;
@@ -362,8 +386,7 @@ const server = http.createServer(async (req, res) => {
362
386
  const conv = queries.getConversation(conversationId);
363
387
  if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
364
388
  const latestSession = queries.getLatestSession(conversationId);
365
- const isActivelyStreaming = activeExecutions.has(conversationId) ||
366
- (latestSession && latestSession.status === 'active');
389
+ const isActivelyStreaming = activeExecutions.has(conversationId);
367
390
 
368
391
  const url = new URL(req.url, 'http://localhost');
369
392
  const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
@@ -896,8 +919,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
896
919
  });
897
920
 
898
921
  batcher.drain();
899
- activeExecutions.delete(conversationId);
900
- queries.setIsStreaming(conversationId, false);
901
922
 
902
923
  setTimeout(() => {
903
924
  rateLimitState.delete(conversationId);
@@ -987,54 +1008,6 @@ function drainMessageQueue(conversationId) {
987
1008
  .catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
988
1009
  }
989
1010
 
990
- async function processMessage(conversationId, messageId, content, agentId) {
991
- try {
992
- debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
993
-
994
- const conv = queries.getConversation(conversationId);
995
- const cwd = conv?.workingDirectory || STARTUP_CWD;
996
- const resumeSessionId = conv?.claudeSessionId || null;
997
-
998
- let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
999
-
1000
- const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(contentStr, cwd, agentId || 'claude-code', {
1001
- resumeSessionId,
1002
- systemPrompt: SYSTEM_PROMPT
1003
- });
1004
-
1005
- if (claudeSessionId) {
1006
- queries.setClaudeSessionId(conversationId, claudeSessionId);
1007
- }
1008
-
1009
- let allBlocks = [];
1010
- for (const output of outputs) {
1011
- if (output.type === 'assistant' && output.message?.content) {
1012
- allBlocks.push(...(output.message.content || []));
1013
- }
1014
- }
1015
-
1016
- let messageContent = null;
1017
- if (allBlocks.length > 0) {
1018
- messageContent = JSON.stringify({ type: 'claude_execution', blocks: allBlocks, timestamp: Date.now() });
1019
- } else {
1020
- let textParts = [];
1021
- for (const output of outputs) {
1022
- if (output.type === 'result' && output.result) textParts.push(String(output.result));
1023
- else if (typeof output === 'string') textParts.push(output);
1024
- }
1025
- messageContent = textParts.join('\n').trim();
1026
- }
1027
-
1028
- if (messageContent) {
1029
- const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
1030
- broadcastSync({ type: 'message_created', conversationId, message: assistantMessage, timestamp: Date.now() });
1031
- }
1032
- } catch (error) {
1033
- debugLog(`[processMessage] Error: ${error.message}`);
1034
- const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
1035
- broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
1036
- }
1037
- }
1038
1011
 
1039
1012
  const wss = new WebSocketServer({
1040
1013
  server,
@@ -1128,10 +1101,10 @@ wss.on('connection', (ws, req) => {
1128
1101
  });
1129
1102
 
1130
1103
  const BROADCAST_TYPES = new Set([
1131
- 'message_created', 'conversation_created', 'conversations_updated',
1132
- 'conversation_deleted', 'queue_status', 'streaming_start',
1133
- 'streaming_complete', 'streaming_error', 'rate_limit_hit',
1134
- 'rate_limit_clear'
1104
+ 'message_created', 'conversation_created', 'conversation_updated',
1105
+ 'conversations_updated', 'conversation_deleted', 'queue_status',
1106
+ 'streaming_start', 'streaming_complete', 'streaming_error',
1107
+ 'rate_limit_hit', 'rate_limit_clear'
1135
1108
  ]);
1136
1109
 
1137
1110
  const wsBatchQueues = new Map();
@@ -1228,29 +1201,73 @@ function recoverStaleSessions() {
1228
1201
  try {
1229
1202
  const staleSessions = queries.getActiveSessions ? queries.getActiveSessions() : [];
1230
1203
  const now = Date.now();
1231
- let recoveredCount = 0;
1204
+ let resumedCount = 0;
1205
+ let failedCount = 0;
1206
+
1232
1207
  for (const session of staleSessions) {
1233
1208
  if (activeExecutions.has(session.conversationId)) continue;
1234
- const sessionAge = now - session.started_at;
1235
- if (sessionAge < STALE_SESSION_MIN_AGE_MS) continue;
1209
+
1236
1210
  queries.updateSession(session.id, {
1237
1211
  status: 'error',
1238
- error: 'Agent died unexpectedly (server restart)',
1212
+ error: 'Server restarted - resuming',
1239
1213
  completed_at: now
1240
1214
  });
1241
- queries.setIsStreaming(session.conversationId, false);
1215
+
1216
+ const conv = queries.getConversation(session.conversationId);
1217
+ if (!conv) {
1218
+ queries.setIsStreaming(session.conversationId, false);
1219
+ failedCount++;
1220
+ continue;
1221
+ }
1222
+
1223
+ const lastMsg = queries.getLastUserMessage(session.conversationId);
1224
+ if (!lastMsg || !conv.claudeSessionId) {
1225
+ queries.setIsStreaming(session.conversationId, false);
1226
+ debugLog(`[RECOVERY] Conv ${session.conversationId}: no user message or no claudeSessionId, cannot resume`);
1227
+ broadcastSync({
1228
+ type: 'streaming_error',
1229
+ sessionId: session.id,
1230
+ conversationId: session.conversationId,
1231
+ error: 'Server restarted - could not resume (missing context)',
1232
+ recoverable: false,
1233
+ timestamp: now
1234
+ });
1235
+ failedCount++;
1236
+ continue;
1237
+ }
1238
+
1239
+ const content = typeof lastMsg.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg.content);
1240
+ const agentId = conv.agentType || conv.agentId || 'claude-code';
1241
+
1242
+ debugLog(`[RECOVERY] Resuming conv ${session.conversationId} with claudeSessionId=${conv.claudeSessionId}`);
1243
+
1244
+ const newSession = queries.createSession(session.conversationId);
1245
+ queries.createEvent('session.created', {
1246
+ messageId: lastMsg.id,
1247
+ sessionId: newSession.id,
1248
+ retryReason: 'server_restart'
1249
+ }, session.conversationId, newSession.id);
1250
+
1242
1251
  broadcastSync({
1243
- type: 'streaming_error',
1244
- sessionId: session.id,
1252
+ type: 'streaming_start',
1253
+ sessionId: newSession.id,
1245
1254
  conversationId: session.conversationId,
1246
- error: 'Agent died unexpectedly (server restart)',
1247
- recoverable: false,
1255
+ messageId: lastMsg.id,
1256
+ agentId,
1248
1257
  timestamp: now
1249
1258
  });
1250
- recoveredCount++;
1259
+
1260
+ processMessageWithStreaming(session.conversationId, lastMsg.id, newSession.id, content, agentId)
1261
+ .catch(err => debugLog(`[RECOVERY] Resume error for ${session.conversationId}: ${err.message}`));
1262
+
1263
+ resumedCount++;
1264
+ }
1265
+
1266
+ if (resumedCount > 0) {
1267
+ console.log(`[RECOVERY] Resumed ${resumedCount} conversation(s) from previous run`);
1251
1268
  }
1252
- if (recoveredCount > 0) {
1253
- console.log(`[RECOVERY] Recovered ${recoveredCount} stale active session(s)`);
1269
+ if (failedCount > 0) {
1270
+ console.log(`[RECOVERY] Failed to resume ${failedCount} conversation(s)`);
1254
1271
  }
1255
1272
  } catch (err) {
1256
1273
  console.error('[RECOVERY] Stale session recovery error:', err.message);
@@ -24,7 +24,7 @@ class AgentGUIClient {
24
24
  isInitialized: false,
25
25
  currentSession: null,
26
26
  currentConversation: null,
27
- isStreaming: false,
27
+ streamingConversations: new Map(),
28
28
  sessionEvents: [],
29
29
  conversations: [],
30
30
  agents: []
@@ -405,11 +405,12 @@ class AgentGUIClient {
405
405
  // just track the state but do not modify the DOM or start polling
406
406
  if (this.state.currentConversation?.id !== data.conversationId) {
407
407
  console.log('Streaming started for non-active conversation:', data.conversationId);
408
+ this.state.streamingConversations.set(data.conversationId, true);
408
409
  this.emit('streaming:start', data);
409
410
  return;
410
411
  }
411
412
 
412
- this.state.isStreaming = true;
413
+ this.state.streamingConversations.set(data.conversationId, true);
413
414
  this.state.currentSession = {
414
415
  id: data.sessionId,
415
416
  conversationId: data.conversationId,
@@ -583,11 +584,12 @@ class AgentGUIClient {
583
584
  // If this event is for a conversation we are NOT currently viewing, just track state
584
585
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
585
586
  console.log('Streaming error for non-active conversation:', conversationId);
587
+ this.state.streamingConversations.delete(conversationId);
586
588
  this.emit('streaming:error', data);
587
589
  return;
588
590
  }
589
591
 
590
- this.state.isStreaming = false;
592
+ this.state.streamingConversations.delete(conversationId);
591
593
 
592
594
  // Stop polling for chunks
593
595
  this.stopChunkPolling();
@@ -613,11 +615,12 @@ class AgentGUIClient {
613
615
 
614
616
  if (conversationId && this.state.currentConversation?.id !== conversationId) {
615
617
  console.log('Streaming completed for non-active conversation:', conversationId);
618
+ this.state.streamingConversations.delete(conversationId);
616
619
  this.emit('streaming:complete', data);
617
620
  return;
618
621
  }
619
622
 
620
- this.state.isStreaming = false;
623
+ this.state.streamingConversations.delete(conversationId);
621
624
 
622
625
  // Stop polling for chunks
623
626
  this.stopChunkPolling();
@@ -662,7 +665,7 @@ class AgentGUIClient {
662
665
  return;
663
666
  }
664
667
 
665
- if (data.message.role === 'assistant' && this.state.isStreaming) {
668
+ if (data.message.role === 'assistant' && this.state.streamingConversations.has(data.conversationId)) {
666
669
  this.emit('message:created', data);
667
670
  return;
668
671
  }
@@ -707,15 +710,21 @@ class AgentGUIClient {
707
710
 
708
711
  handleRateLimitHit(data) {
709
712
  if (data.conversationId !== this.state.currentConversation?.id) return;
710
- this.state.isStreaming = false;
713
+ this.state.streamingConversations.delete(data.conversationId);
711
714
  this.stopChunkPolling();
715
+ this.enableControls();
716
+
717
+ const cooldownMs = data.retryAfterMs || 60000;
718
+ this._rateLimitSafetyTimer = setTimeout(() => {
719
+ this.enableControls();
720
+ }, cooldownMs + 10000);
712
721
 
713
722
  const sessionId = data.sessionId || this.state.currentSession?.id;
714
723
  const streamingEl = document.getElementById(`streaming-${sessionId}`);
715
724
  if (streamingEl) {
716
725
  const indicator = streamingEl.querySelector('.streaming-indicator');
717
726
  if (indicator) {
718
- const retrySeconds = Math.ceil((data.retryAfterMs || 60000) / 1000);
727
+ const retrySeconds = Math.ceil(cooldownMs / 1000);
719
728
  indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${retrySeconds}s...</span>`;
720
729
  let remaining = retrySeconds;
721
730
  const countdownTimer = setInterval(() => {
@@ -733,6 +742,10 @@ class AgentGUIClient {
733
742
 
734
743
  handleRateLimitClear(data) {
735
744
  if (data.conversationId !== this.state.currentConversation?.id) return;
745
+ if (this._rateLimitSafetyTimer) {
746
+ clearTimeout(this._rateLimitSafetyTimer);
747
+ this._rateLimitSafetyTimer = null;
748
+ }
736
749
  this.enableControls();
737
750
  }
738
751
 
@@ -1012,35 +1025,46 @@ class AgentGUIClient {
1012
1025
  pollState.lastFetchTimestamp = Date.now();
1013
1026
  pollState.backoffDelay = 150;
1014
1027
  pollState.sessionCheckCounter = 0;
1028
+ pollState.emptyPollCount = 0;
1029
+
1030
+ const checkSessionStatus = async () => {
1031
+ if (!this.state.currentSession?.id) return false;
1032
+ const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
1033
+ if (!sessionResponse.ok) return false;
1034
+ const { session } = await sessionResponse.json();
1035
+ if (session && (session.status === 'complete' || session.status === 'error')) {
1036
+ if (session.status === 'complete') {
1037
+ this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
1038
+ } else {
1039
+ this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
1040
+ }
1041
+ return true;
1042
+ }
1043
+ return false;
1044
+ };
1015
1045
 
1016
1046
  const pollOnce = async () => {
1017
1047
  if (!pollState.isPolling) return;
1018
1048
 
1019
1049
  try {
1020
1050
  pollState.sessionCheckCounter++;
1021
- if (pollState.sessionCheckCounter % 10 === 0 && this.state.currentSession?.id) {
1022
- const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
1023
- if (sessionResponse.ok) {
1024
- const { session } = await sessionResponse.json();
1025
- if (session && (session.status === 'complete' || session.status === 'error')) {
1026
- if (session.status === 'complete') {
1027
- this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
1028
- } else {
1029
- this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
1030
- }
1031
- return;
1032
- }
1033
- }
1051
+ const shouldCheckSession = pollState.sessionCheckCounter % 3 === 0 || pollState.emptyPollCount >= 3;
1052
+ if (shouldCheckSession) {
1053
+ const done = await checkSessionStatus();
1054
+ if (done) return;
1055
+ if (pollState.emptyPollCount >= 3) pollState.emptyPollCount = 0;
1034
1056
  }
1035
1057
 
1036
1058
  const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
1037
1059
 
1038
1060
  if (chunks.length > 0) {
1039
1061
  pollState.backoffDelay = 150;
1062
+ pollState.emptyPollCount = 0;
1040
1063
  const lastChunk = chunks[chunks.length - 1];
1041
1064
  pollState.lastFetchTimestamp = lastChunk.created_at;
1042
1065
  this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
1043
1066
  } else {
1067
+ pollState.emptyPollCount++;
1044
1068
  pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
1045
1069
  }
1046
1070
 
@@ -1248,7 +1272,7 @@ class AgentGUIClient {
1248
1272
  if (!convId) return;
1249
1273
  const outputEl = document.getElementById('output');
1250
1274
  if (!outputEl || !outputEl.firstChild) return;
1251
- if (this.state.isStreaming) return;
1275
+ if (this.state.streamingConversations.has(convId)) return;
1252
1276
 
1253
1277
  this.saveScrollPosition(convId);
1254
1278
  const clone = outputEl.cloneNode(true);
@@ -1272,8 +1296,7 @@ class AgentGUIClient {
1272
1296
  try {
1273
1297
  this.cacheCurrentConversation();
1274
1298
  this.stopChunkPolling();
1275
- if (this.state.isStreaming && this.state.currentConversation?.id !== conversationId) {
1276
- this.state.isStreaming = false;
1299
+ if (this.state.currentConversation?.id !== conversationId) {
1277
1300
  this.state.currentSession = null;
1278
1301
  }
1279
1302
 
@@ -1313,7 +1336,8 @@ class AgentGUIClient {
1313
1336
  const userMessages = (allMessages || []).filter(m => m.role === 'user');
1314
1337
  const hasMoreChunks = totalChunks && chunks.length < totalChunks;
1315
1338
 
1316
- const shouldResumeStreaming = isActivelyStreaming && latestSession &&
1339
+ const clientKnowsStreaming = this.state.streamingConversations.has(conversationId);
1340
+ const shouldResumeStreaming = (isActivelyStreaming || clientKnowsStreaming) && latestSession &&
1317
1341
  (latestSession.status === 'active' || latestSession.status === 'pending');
1318
1342
 
1319
1343
  const outputEl = document.getElementById('output');
@@ -1444,7 +1468,7 @@ class AgentGUIClient {
1444
1468
  }
1445
1469
 
1446
1470
  if (shouldResumeStreaming && latestSession) {
1447
- this.state.isStreaming = true;
1471
+ this.state.streamingConversations.set(conversationId, true);
1448
1472
  this.state.currentSession = {
1449
1473
  id: latestSession.id,
1450
1474
  conversationId: conversationId,
@@ -218,10 +218,11 @@ class ConversationManager {
218
218
  const data = await res.json();
219
219
  this.conversations = data.conversations || [];
220
220
 
221
- // Seed streaming state from database isStreaming flag
222
221
  for (const conv of this.conversations) {
223
222
  if (conv.isStreaming === 1 || conv.isStreaming === true) {
224
223
  this.streamingConversations.add(conv.id);
224
+ } else {
225
+ this.streamingConversations.delete(conv.id);
225
226
  }
226
227
  }
227
228
 
@@ -272,7 +273,7 @@ class ConversationManager {
272
273
  const isActive = conv.id === this.activeId;
273
274
  el.classList.toggle('active', isActive);
274
275
 
275
- const isStreaming = conv.isStreaming === 1 || conv.isStreaming === true || this.streamingConversations?.has(conv.id);
276
+ const isStreaming = this.streamingConversations.has(conv.id);
276
277
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
277
278
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
278
279
  const agent = conv.agentType || 'unknown';
@@ -298,7 +299,7 @@ class ConversationManager {
298
299
  li.dataset.convId = conv.id;
299
300
  if (conv.id === this.activeId) li.classList.add('active');
300
301
 
301
- const isStreaming = conv.isStreaming === 1 || conv.isStreaming === true || this.streamingConversations?.has(conv.id);
302
+ const isStreaming = this.streamingConversations.has(conv.id);
302
303
 
303
304
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
304
305
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
@@ -26,6 +26,7 @@ class WebSocketManager {
26
26
  this.requestMap = new Map();
27
27
  this.heartbeatTimer = null;
28
28
  this.connectionState = 'disconnected';
29
+ this.activeSubscriptions = new Set();
29
30
 
30
31
  // Statistics
31
32
  this.stats = {
@@ -124,11 +125,10 @@ class WebSocketManager {
124
125
 
125
126
  // Flush buffered messages
126
127
  this.flushMessageBuffer();
128
+ this.resubscribeAll();
127
129
 
128
- // Start heartbeat
129
130
  this.startHeartbeat();
130
131
 
131
- // Emit connected event
132
132
  this.emit('connected', { timestamp: Date.now() });
133
133
  }
134
134
 
@@ -271,8 +271,15 @@ class WebSocketManager {
271
271
  throw new Error('Invalid message data');
272
272
  }
273
273
 
274
+ if (data.type === 'subscribe') {
275
+ const key = data.sessionId ? `session:${data.sessionId}` : `conv:${data.conversationId}`;
276
+ this.activeSubscriptions.add(key);
277
+ } else if (data.type === 'unsubscribe') {
278
+ const key = data.sessionId ? `session:${data.sessionId}` : `conv:${data.conversationId}`;
279
+ this.activeSubscriptions.delete(key);
280
+ }
281
+
274
282
  if (!this.isConnected) {
275
- // Buffer message if not connected
276
283
  this.bufferMessage(data);
277
284
  return false;
278
285
  }
@@ -335,6 +342,19 @@ class WebSocketManager {
335
342
  });
336
343
  }
337
344
 
345
+ resubscribeAll() {
346
+ for (const key of this.activeSubscriptions) {
347
+ const [type, id] = key.split(':');
348
+ const msg = { type: 'subscribe', timestamp: Date.now() };
349
+ if (type === 'session') msg.sessionId = id;
350
+ else msg.conversationId = id;
351
+ try {
352
+ this.ws.send(JSON.stringify(msg));
353
+ this.stats.totalMessagesSent++;
354
+ } catch (_) {}
355
+ }
356
+ }
357
+
338
358
  /**
339
359
  * Unsubscribe from streaming session
340
360
  */