agentgui 1.0.94 → 1.0.96

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/.prd CHANGED
@@ -1 +1,7 @@
1
+ CHAT UI LAYOUT REDESIGN - Standard Chat Interface Pattern
1
2
 
3
+ GOAL: Make AgentGUI look and behave like Claude.ai/ChatGPT/Gemini/Grok standard chat layout
4
+
5
+ ITEMS:
6
+
7
+ 7. [PENDING] Commit and push changes
package/database.js CHANGED
@@ -198,7 +198,9 @@ try {
198
198
  gitBranch: 'TEXT',
199
199
  sourcePath: 'TEXT',
200
200
  lastSyncedAt: 'INTEGER',
201
- workingDirectory: 'TEXT'
201
+ workingDirectory: 'TEXT',
202
+ claudeSessionId: 'TEXT',
203
+ isStreaming: 'INTEGER DEFAULT 0'
202
204
  };
203
205
 
204
206
  let addedColumns = false;
@@ -286,6 +288,46 @@ export const queries = {
286
288
  };
287
289
  },
288
290
 
291
+ setClaudeSessionId(conversationId, claudeSessionId) {
292
+ const stmt = db.prepare('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
293
+ stmt.run(claudeSessionId, Date.now(), conversationId);
294
+ },
295
+
296
+ getClaudeSessionId(conversationId) {
297
+ const stmt = db.prepare('SELECT claudeSessionId FROM conversations WHERE id = ?');
298
+ const row = stmt.get(conversationId);
299
+ return row?.claudeSessionId || null;
300
+ },
301
+
302
+ setIsStreaming(conversationId, isStreaming) {
303
+ const stmt = db.prepare('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
304
+ stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
305
+ },
306
+
307
+ getIsStreaming(conversationId) {
308
+ const stmt = db.prepare('SELECT isStreaming FROM conversations WHERE id = ?');
309
+ const row = stmt.get(conversationId);
310
+ return row?.isStreaming === 1;
311
+ },
312
+
313
+ markSessionIncomplete(sessionId, errorMsg) {
314
+ const stmt = db.prepare('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
315
+ stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
316
+ },
317
+
318
+ getSessionsProcessingLongerThan(minutes) {
319
+ const cutoff = Date.now() - (minutes * 60 * 1000);
320
+ const stmt = db.prepare('SELECT * FROM sessions WHERE status = ? AND started_at < ?');
321
+ return stmt.all('pending', cutoff);
322
+ },
323
+
324
+ cleanupOrphanedSessions(days) {
325
+ const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
326
+ const stmt = db.prepare('DELETE FROM sessions WHERE status = ? AND started_at < ?');
327
+ const result = stmt.run('pending', cutoff);
328
+ return result.changes || 0;
329
+ },
330
+
289
331
  createMessage(conversationId, role, content, idempotencyKey = null) {
290
332
  if (idempotencyKey) {
291
333
  const cached = this.getIdempotencyKey(idempotencyKey);
@@ -1,23 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
2
 
3
- /**
4
- * Configuration for Claude runner
5
- * @typedef {Object} ClaudeRunnerConfig
6
- * @property {boolean} [skipPermissions=false] - Use --dangerously-skip-permissions flag
7
- * @property {boolean} [verbose=true] - Use --verbose flag
8
- * @property {string} [outputFormat='stream-json'] - Output format (stream-json, json, text)
9
- * @property {number} [timeout=300000] - Timeout in milliseconds (default 5 minutes)
10
- * @property {boolean} [print=true] - Use --print flag
11
- */
12
-
13
- /**
14
- * Run Claude with streaming JSON output
15
- * @param {string} prompt - The prompt to send to Claude
16
- * @param {string} cwd - Working directory
17
- * @param {string} agentId - Agent identifier (for logging)
18
- * @param {ClaudeRunnerConfig} [config={}] - Configuration options
19
- * @returns {Promise<Array>} Array of parsed JSON objects from Claude output
20
- */
21
3
  export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
22
4
  return new Promise((resolve, reject) => {
23
5
  const {
@@ -25,20 +7,25 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
25
7
  verbose = true,
26
8
  outputFormat = 'stream-json',
27
9
  timeout = 300000,
28
- print = true
10
+ print = true,
11
+ resumeSessionId = null,
12
+ systemPrompt = null,
13
+ onEvent = null
29
14
  } = config;
30
15
 
31
- // Build flags array
32
16
  const flags = [];
33
17
  if (print) flags.push('--print');
34
18
  if (verbose) flags.push('--verbose');
35
19
  flags.push(`--output-format=${outputFormat}`);
36
20
  if (skipPermissions) flags.push('--dangerously-skip-permissions');
21
+ if (resumeSessionId) flags.push('--resume', resumeSessionId);
22
+ if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
37
23
 
38
24
  const proc = spawn('claude', flags, { cwd });
39
25
  let jsonBuffer = '';
40
26
  const outputs = [];
41
27
  let timedOut = false;
28
+ let sessionId = null;
42
29
 
43
30
  const timeoutHandle = setTimeout(() => {
44
31
  timedOut = true;
@@ -61,6 +48,16 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
61
48
  try {
62
49
  const parsed = JSON.parse(line);
63
50
  outputs.push(parsed);
51
+
52
+ if (parsed.session_id) {
53
+ sessionId = parsed.session_id;
54
+ }
55
+
56
+ if (onEvent) {
57
+ try { onEvent(parsed); } catch (e) {
58
+ console.error(`[claude-runner] onEvent error: ${e.message}`);
59
+ }
60
+ }
64
61
  } catch (e) {
65
62
  console.error(`[claude-runner] JSON parse error on line: ${line.substring(0, 100)}`);
66
63
  }
@@ -79,12 +76,17 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
79
76
  if (code === 0) {
80
77
  if (jsonBuffer.trim()) {
81
78
  try {
82
- outputs.push(JSON.parse(jsonBuffer));
79
+ const parsed = JSON.parse(jsonBuffer);
80
+ outputs.push(parsed);
81
+ if (parsed.session_id) sessionId = parsed.session_id;
82
+ if (onEvent) {
83
+ try { onEvent(parsed); } catch (e) {}
84
+ }
83
85
  } catch (e) {
84
86
  console.error(`[claude-runner] Final JSON parse error: ${jsonBuffer.substring(0, 100)}`);
85
87
  }
86
88
  }
87
- resolve(outputs);
89
+ resolve({ outputs, sessionId });
88
90
  } else {
89
91
  reject(new Error(`Claude CLI exited with code ${code} for agent ${agentId}`));
90
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.94",
3
+ "version": "1.0.96",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -13,10 +13,11 @@ const express = require('express');
13
13
  const Busboy = require('busboy');
14
14
  const fsbrowse = require('fsbrowse');
15
15
 
16
- // System prompt for Claude to format responses as HTML
17
16
  const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
18
17
 
19
- // Debug logging to file
18
+ const activeExecutions = new Map();
19
+ const messageQueues = new Map();
20
+
20
21
  const debugLog = (msg) => {
21
22
  const timestamp = new Date().toISOString();
22
23
  console.error(`[${timestamp}] ${msg}`);
@@ -234,19 +235,30 @@ const server = http.createServer(async (req, res) => {
234
235
  const agentId = body.agentId || 'claude-code';
235
236
  const skipPermissions = body.skipPermissions || false;
236
237
 
237
- debugLog(`[stream] Starting stream: conversationId=${conversationId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
238
-
239
- // Create user message and session immediately
240
238
  const userMessage = queries.createMessage(conversationId, 'user', prompt);
241
- const session = queries.createSession(conversationId);
242
239
  queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
240
+
241
+ broadcastSync({ type: 'message_created', conversationId, message: userMessage, timestamp: Date.now() });
242
+
243
+ if (activeExecutions.has(conversationId)) {
244
+ debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
245
+ if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
246
+ messageQueues.get(conversationId).push({ content: prompt, agentId, skipPermissions, messageId: userMessage.id });
247
+
248
+ const queueLength = messageQueues.get(conversationId).length;
249
+ broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
250
+
251
+ res.writeHead(200, { 'Content-Type': 'application/json' });
252
+ res.end(JSON.stringify({ message: userMessage, queued: true, queuePosition: queueLength }));
253
+ return;
254
+ }
255
+
256
+ const session = queries.createSession(conversationId);
243
257
  queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
244
258
 
245
- // Send immediate response with session info
246
259
  res.writeHead(200, { 'Content-Type': 'application/json' });
247
260
  res.end(JSON.stringify({ message: userMessage, session, streamId: session.id }));
248
261
 
249
- // Emit streaming start event
250
262
  broadcastSync({
251
263
  type: 'streaming_start',
252
264
  sessionId: session.id,
@@ -256,7 +268,6 @@ const server = http.createServer(async (req, res) => {
256
268
  timestamp: Date.now()
257
269
  });
258
270
 
259
- // Fire-and-forget streaming with error handling
260
271
  processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, skipPermissions)
261
272
  .catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
262
273
  return;
@@ -450,58 +461,63 @@ function serveFile(filePath, res) {
450
461
 
451
462
  async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, skipPermissions = false) {
452
463
  const startTime = Date.now();
464
+ activeExecutions.set(conversationId, true);
465
+ queries.setIsStreaming(conversationId, true);
466
+
453
467
  try {
454
- debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
468
+ debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
455
469
 
456
470
  const conv = queries.getConversation(conversationId);
457
471
  const cwd = conv?.workingDirectory || '/config';
458
- const actualAgentId = agentId || 'claude-code';
472
+ const resumeSessionId = conv?.claudeSessionId || null;
459
473
 
460
- debugLog(`[stream] Calling runClaudeWithStreaming with config: skipPermissions=${skipPermissions}`);
461
- const config = {
462
- skipPermissions,
463
- verbose: true,
464
- outputFormat: 'stream-json',
465
- timeout: 1800000, // 30 minutes
466
- print: true
467
- };
468
-
469
- // Prepend system prompt to user content
470
- const promptWithSystem = `${SYSTEM_PROMPT}\n\n${content}`;
471
-
472
- const outputs = await runClaudeWithStreaming(promptWithSystem, cwd, actualAgentId, config);
473
- debugLog(`[stream] Claude returned ${outputs.length} streaming outputs`);
474
-
475
- // Process streaming outputs similar to processMessage
476
- // But emit WebSocket events for each block
477
474
  let allBlocks = [];
478
- let lastAssistantMessage = null;
479
475
  let eventCount = 0;
480
476
 
481
- for (const output of outputs) {
482
- if (output.type === 'assistant' && output.message?.content) {
483
- debugLog(`[stream] Found assistant message with ${output.message.content.length} content blocks`);
484
- lastAssistantMessage = output.message;
485
- allBlocks.push(...(output.message.content || []));
486
-
487
- // Emit progress event for each block
477
+ const onEvent = (parsed) => {
478
+ if (parsed.type === 'assistant' && parsed.message?.content) {
479
+ for (const block of parsed.message.content) {
480
+ allBlocks.push(block);
481
+ eventCount++;
482
+ broadcastSync({
483
+ type: 'streaming_progress',
484
+ sessionId,
485
+ conversationId,
486
+ block,
487
+ blockIndex: allBlocks.length - 1,
488
+ timestamp: Date.now()
489
+ });
490
+ }
491
+ } else if (parsed.type === 'result' && parsed.result && allBlocks.length === 0) {
488
492
  broadcastSync({
489
493
  type: 'streaming_progress',
490
494
  sessionId,
491
495
  conversationId,
492
- blockCount: allBlocks.length,
496
+ block: { type: 'text', text: parsed.result },
497
+ blockIndex: 0,
498
+ isResult: true,
493
499
  timestamp: Date.now()
494
500
  });
495
- eventCount++;
496
- } else if (output.type === 'tool_result' && output.result) {
497
- debugLog(`[stream] Found tool result`);
498
- allBlocks.push({
499
- type: 'tool_result',
500
- result: output.result,
501
- tool_use_id: output.tool_use_id
502
- });
503
- eventCount++;
504
501
  }
502
+ };
503
+
504
+ const config = {
505
+ skipPermissions,
506
+ verbose: true,
507
+ outputFormat: 'stream-json',
508
+ timeout: 1800000,
509
+ print: true,
510
+ resumeSessionId,
511
+ systemPrompt: SYSTEM_PROMPT,
512
+ onEvent
513
+ };
514
+
515
+ const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
516
+ debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
517
+
518
+ if (claudeSessionId && !conv?.claudeSessionId) {
519
+ queries.setClaudeSessionId(conversationId, claudeSessionId);
520
+ debugLog(`[stream] Stored claudeSessionId=${claudeSessionId}`);
505
521
  }
506
522
 
507
523
  let messageContent = null;
@@ -511,28 +527,20 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
511
527
  blocks: allBlocks,
512
528
  timestamp: Date.now()
513
529
  });
514
- debugLog(`[stream] Storing full execution with ${allBlocks.length} blocks`);
515
530
  } else {
516
531
  let textParts = [];
517
532
  for (const output of outputs) {
518
- if (typeof output === 'string') {
519
- textParts.push(output);
520
- } else if (output.text) {
521
- textParts.push(output.text);
522
- } else if (output.content?.text) {
523
- textParts.push(output.content.text);
524
- } else if (output.result) {
533
+ if (output.type === 'result' && output.result) {
525
534
  textParts.push(String(output.result));
535
+ } else if (typeof output === 'string') {
536
+ textParts.push(output);
526
537
  }
527
538
  }
528
539
  messageContent = textParts.join('\n').trim();
529
- debugLog(`[stream] Storing text response: "${messageContent.substring(0, 100)}..."`);
530
540
  }
531
541
 
532
542
  if (messageContent) {
533
- debugLog(`[stream] Creating assistant message`);
534
543
  const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
535
- debugLog(`[stream] Created message with id: ${assistantMessage.id}`);
536
544
  broadcastSync({
537
545
  type: 'streaming_complete',
538
546
  sessionId,
@@ -541,30 +549,25 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
541
549
  eventCount,
542
550
  timestamp: Date.now()
543
551
  });
544
- } else {
545
- debugLog(`[stream] No response content extracted!`);
552
+ broadcastSync({
553
+ type: 'message_created',
554
+ conversationId,
555
+ message: assistantMessage,
556
+ timestamp: Date.now()
557
+ });
546
558
  }
547
559
 
548
- debugLog(`[stream] Completed: ${outputs.length} outputs received, ${eventCount} events emitted`);
560
+ debugLog(`[stream] Completed: ${outputs.length} outputs, ${eventCount} events`);
549
561
  } catch (error) {
550
562
  const elapsed = Date.now() - startTime;
551
563
  debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
552
564
 
553
- // Mark session as incomplete for recovery
554
- try {
555
- const sessionStatus = error.message.includes('timeout') ? 'timeout' : 'error';
556
- queries.markSessionIncomplete(sessionId, error.message);
557
- debugLog(`[stream] Session ${sessionId} marked as incomplete (${sessionStatus})`);
558
- } catch (err) {
559
- debugLog(`[stream] Failed to mark session: ${err.message}`);
560
- }
561
-
562
565
  broadcastSync({
563
566
  type: 'streaming_error',
564
567
  sessionId,
565
568
  conversationId,
566
569
  error: error.message,
567
- recoverable: elapsed < 60000, // Retryable if failed within 1 minute
570
+ recoverable: elapsed < 60000,
568
571
  timestamp: Date.now()
569
572
  });
570
573
 
@@ -575,102 +578,91 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
575
578
  message: errorMessage,
576
579
  timestamp: Date.now()
577
580
  });
581
+ } finally {
582
+ activeExecutions.delete(conversationId);
583
+ queries.setIsStreaming(conversationId, false);
584
+ drainMessageQueue(conversationId);
578
585
  }
579
586
  }
580
587
 
588
+ function drainMessageQueue(conversationId) {
589
+ const queue = messageQueues.get(conversationId);
590
+ if (!queue || queue.length === 0) return;
591
+
592
+ const next = queue.shift();
593
+ if (queue.length === 0) messageQueues.delete(conversationId);
594
+
595
+ debugLog(`[queue] Draining next message for ${conversationId}`);
596
+
597
+ const session = queries.createSession(conversationId);
598
+ queries.createEvent('session.created', { messageId: next.messageId, sessionId: session.id }, conversationId, session.id);
599
+
600
+ broadcastSync({
601
+ type: 'streaming_start',
602
+ sessionId: session.id,
603
+ conversationId,
604
+ messageId: next.messageId,
605
+ agentId: next.agentId,
606
+ timestamp: Date.now()
607
+ });
608
+
609
+ broadcastSync({
610
+ type: 'queue_status',
611
+ conversationId,
612
+ queueLength: queue?.length || 0,
613
+ timestamp: Date.now()
614
+ });
615
+
616
+ processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.skipPermissions)
617
+ .catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
618
+ }
619
+
581
620
  async function processMessage(conversationId, messageId, content, agentId) {
582
621
  try {
583
622
  debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
584
623
 
585
624
  const conv = queries.getConversation(conversationId);
586
625
  const cwd = conv?.workingDirectory || '/config';
587
- const actualAgentId = agentId || 'claude-code';
626
+ const resumeSessionId = conv?.claudeSessionId || null;
588
627
 
589
- // Handle both string content and object content (for structured messages)
590
- let contentStr = content;
591
- if (typeof content === 'object') {
592
- contentStr = JSON.stringify(content);
593
- }
628
+ let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
594
629
 
595
- debugLog(`[processMessage] Calling runClaudeWithStreaming with prompt: "${contentStr.substring(0, 50)}..."`);
596
- // Prepend system prompt to user content
597
- const promptWithSystem = `${SYSTEM_PROMPT}\n\n${contentStr}`;
598
- const outputs = await runClaudeWithStreaming(promptWithSystem, cwd, actualAgentId);
599
- debugLog(`[processMessage] Claude returned ${outputs.length} outputs`);
630
+ const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(contentStr, cwd, agentId || 'claude-code', {
631
+ resumeSessionId,
632
+ systemPrompt: SYSTEM_PROMPT
633
+ });
600
634
 
601
- // Collect all message blocks to preserve full execution details
602
- let allBlocks = [];
603
- let lastAssistantMessage = null;
635
+ if (claudeSessionId && !conv?.claudeSessionId) {
636
+ queries.setClaudeSessionId(conversationId, claudeSessionId);
637
+ }
604
638
 
639
+ let allBlocks = [];
605
640
  for (const output of outputs) {
606
641
  if (output.type === 'assistant' && output.message?.content) {
607
- debugLog(`[processMessage] Found assistant message with ${output.message.content.length} content blocks`);
608
- lastAssistantMessage = output.message;
609
642
  allBlocks.push(...(output.message.content || []));
610
- } else if (output.type === 'tool_result' && output.result) {
611
- debugLog(`[processMessage] Found tool result: ${typeof output.result}`);
612
- allBlocks.push({
613
- type: 'tool_result',
614
- result: output.result,
615
- tool_use_id: output.tool_use_id
616
- });
617
643
  }
618
644
  }
619
645
 
620
- // Store full message structure if we have execution data, otherwise fallback to text
621
646
  let messageContent = null;
622
-
623
647
  if (allBlocks.length > 0) {
624
- // Store full message structure as JSON for proper rendering
625
- messageContent = JSON.stringify({
626
- type: 'claude_execution',
627
- blocks: allBlocks,
628
- timestamp: Date.now()
629
- });
630
- debugLog(`[processMessage] Storing full execution with ${allBlocks.length} blocks`);
648
+ messageContent = JSON.stringify({ type: 'claude_execution', blocks: allBlocks, timestamp: Date.now() });
631
649
  } else {
632
- // Fallback: extract text for simple responses
633
650
  let textParts = [];
634
651
  for (const output of outputs) {
635
- if (typeof output === 'string') {
636
- textParts.push(output);
637
- } else if (output.text) {
638
- textParts.push(output.text);
639
- } else if (output.content?.text) {
640
- textParts.push(output.content.text);
641
- } else if (output.result) {
642
- textParts.push(String(output.result));
643
- }
652
+ if (output.type === 'result' && output.result) textParts.push(String(output.result));
653
+ else if (typeof output === 'string') textParts.push(output);
644
654
  }
645
655
  messageContent = textParts.join('\n').trim();
646
- debugLog(`[processMessage] Storing text response: "${messageContent.substring(0, 100)}..."`);
647
656
  }
648
657
 
649
658
  if (messageContent) {
650
- debugLog(`[processMessage] Creating assistant message`);
651
659
  const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
652
- debugLog(`[processMessage] Created message with id: ${assistantMessage.id}`);
653
- broadcastSync({
654
- type: 'message_created',
655
- conversationId,
656
- message: assistantMessage,
657
- timestamp: Date.now()
658
- });
659
- } else {
660
- debugLog(`[processMessage] No response content extracted!`);
660
+ broadcastSync({ type: 'message_created', conversationId, message: assistantMessage, timestamp: Date.now() });
661
661
  }
662
-
663
- debugLog(`[processMessage] ✅ Completed: ${outputs.length} outputs received`);
664
662
  } catch (error) {
665
663
  debugLog(`[processMessage] Error: ${error.message}`);
666
- debugLog(`[processMessage] Stack: ${error.stack}`);
667
664
  const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
668
- broadcastSync({
669
- type: 'message_created',
670
- conversationId,
671
- message: errorMessage,
672
- timestamp: Date.now()
673
- });
665
+ broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
674
666
  }
675
667
  }
676
668
 
@@ -700,11 +692,14 @@ wss.on('connection', (ws, req) => {
700
692
  try {
701
693
  const data = JSON.parse(msg);
702
694
  if (data.type === 'subscribe') {
703
- ws.subscriptions.add(data.sessionId);
704
- debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${data.sessionId}`);
695
+ if (data.sessionId) ws.subscriptions.add(data.sessionId);
696
+ if (data.conversationId) ws.subscriptions.add(`conv-${data.conversationId}`);
697
+ const subTarget = data.sessionId || data.conversationId;
698
+ debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
705
699
  ws.send(JSON.stringify({
706
700
  type: 'subscription_confirmed',
707
701
  sessionId: data.sessionId,
702
+ conversationId: data.conversationId,
708
703
  timestamp: Date.now()
709
704
  }));
710
705
  } else if (data.type === 'unsubscribe') {
@@ -742,25 +737,17 @@ wss.on('connection', (ws, req) => {
742
737
 
743
738
  function broadcastSync(event) {
744
739
  const data = JSON.stringify(event);
745
- const isStreamingEvent = event.type && event.type.startsWith('streaming_');
746
- const targetSessionId = event.sessionId || (event.conversationId && `conv-${event.conversationId}`);
747
740
 
748
741
  for (const ws of syncClients) {
749
742
  if (ws.readyState !== 1) continue;
750
743
 
751
744
  let shouldSend = false;
752
745
 
753
- if (isStreamingEvent && targetSessionId) {
754
- // Streaming events require sessionId subscription
755
- shouldSend = ws.subscriptions && ws.subscriptions.has(targetSessionId);
756
- } else if (event.sessionId) {
757
- // Regular session events require sessionId subscription
758
- shouldSend = ws.subscriptions && ws.subscriptions.has(event.sessionId);
759
- } else if (event.type === 'message_created' || event.type === 'conversation_created') {
760
- // Global events sent to all clients
746
+ if (event.sessionId && ws.subscriptions?.has(event.sessionId)) {
761
747
  shouldSend = true;
762
- } else {
763
- // Default: send to all connected clients
748
+ } else if (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`)) {
749
+ shouldSend = true;
750
+ } else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status') {
764
751
  shouldSend = true;
765
752
  }
766
753