agentgui 1.0.674 → 1.0.675

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.
@@ -9,6 +9,16 @@ export function register(router, deps) {
9
9
  const { queries, activeExecutions, messageQueues, rateLimitState,
10
10
  broadcastSync, processMessageWithStreaming, activeProcessesByConvId, steeringTimeouts, cleanupExecution } = deps;
11
11
 
12
+ // Per-conversation queue seq counter for event ordering
13
+ const queueSeqByConv = new Map();
14
+
15
+ function getNextQueueSeq(conversationId) {
16
+ const current = queueSeqByConv.get(conversationId) || 0;
17
+ const next = current + 1;
18
+ queueSeqByConv.set(conversationId, next);
19
+ return next;
20
+ }
21
+
12
22
  router.handle('conv.ls', () => {
13
23
  const conversations = queries.getConversationsList();
14
24
  for (const c of conversations) { if (c.isStreaming && !activeExecutions.has(c.id)) c.isStreaming = 0; }
@@ -247,6 +257,14 @@ export function register(router, deps) {
247
257
 
248
258
  // Message is queued - don't broadcast as message_created, let queue_status handle the UI update
249
259
  const qp = enqueue(p.id, prompt, agentId, model, userMessage.id, subAgent);
260
+ const seq = getNextQueueSeq(p.id);
261
+ broadcastSync({
262
+ type: 'queue_status',
263
+ conversationId: p.id,
264
+ queueLength: messageQueues.get(p.id)?.length || 1,
265
+ seq,
266
+ timestamp: Date.now()
267
+ });
250
268
  return { message: userMessage, queued: true, queuePosition: qp };
251
269
  });
252
270
 
@@ -262,7 +280,14 @@ export function register(router, deps) {
262
280
  if (idx === -1) notFound('Queued message not found');
263
281
  queue.splice(idx, 1);
264
282
  if (queue.length === 0) messageQueues.delete(p.id);
265
- broadcastSync({ type: 'queue_status', conversationId: p.id, queueLength: queue?.length || 0, timestamp: Date.now() });
283
+ const seq = getNextQueueSeq(p.id);
284
+ broadcastSync({
285
+ type: 'queue_status',
286
+ conversationId: p.id,
287
+ queueLength: queue?.length || 0,
288
+ seq,
289
+ timestamp: Date.now()
290
+ });
266
291
  return { deleted: true };
267
292
  });
268
293
 
@@ -273,7 +298,16 @@ export function register(router, deps) {
273
298
  if (!item) notFound('Queued message not found');
274
299
  if (p.content !== undefined) item.content = p.content;
275
300
  if (p.agentId !== undefined) item.agentId = p.agentId;
276
- broadcastSync({ type: 'queue_updated', conversationId: p.id, messageId: p.messageId, content: item.content, agentId: item.agentId, timestamp: Date.now() });
301
+ const seq = getNextQueueSeq(p.id);
302
+ broadcastSync({
303
+ type: 'queue_updated',
304
+ conversationId: p.id,
305
+ messageId: p.messageId,
306
+ content: item.content,
307
+ agentId: item.agentId,
308
+ seq,
309
+ timestamp: Date.now()
310
+ });
277
311
  return { updated: true, item };
278
312
  });
279
313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.674",
3
+ "version": "1.0.675",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -3356,6 +3356,9 @@ function createChunkBatcher() {
3356
3356
  return { add, drain };
3357
3357
  }
3358
3358
 
3359
+ // Global broadcast sequence counter for event ordering
3360
+ let broadcastSeq = 0;
3361
+
3359
3362
  function parseRateLimitResetTime(text) {
3360
3363
  const match = text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
3361
3364
  if (!match) return 300;
@@ -136,23 +136,35 @@ class StreamingRenderer {
136
136
 
137
137
  /**
138
138
  * Check if event is a duplicate
139
+ * For streaming_progress events, use seq+sessionId for precise dedup
140
+ * For other events, use type+id or type+sessionId
139
141
  */
140
142
  isDuplicate(event) {
141
143
  const key = this.getEventKey(event);
142
144
  if (!key) return false;
143
145
 
144
- const lastTime = this.dedupMap.get(key);
145
- const now = Date.now();
146
+ const lastSeq = this.dedupMap.get(key);
146
147
 
147
- if (lastTime && (now - lastTime) < 100) {
148
- return true;
148
+ // For streaming_progress with seq, compare seq numbers directly
149
+ if (event.type === 'streaming_progress' && event.seq !== undefined && lastSeq !== undefined) {
150
+ if (event.seq <= lastSeq) {
151
+ return true; // Same or older seq = duplicate
152
+ }
153
+ this.dedupMap.set(key, event.seq);
154
+ return false;
155
+ }
156
+
157
+ // For other events, use time-based dedup
158
+ const now = Date.now();
159
+ if (lastSeq && typeof lastSeq === 'number' && lastSeq > now - 500) {
160
+ return true; // Recent duplicate
149
161
  }
150
162
 
151
163
  this.dedupMap.set(key, now);
152
164
  if (this.dedupMap.size > 5000) {
153
- const cutoff = now - 1000;
154
- for (const [k, t] of this.dedupMap) {
155
- if (t < cutoff) this.dedupMap.delete(k);
165
+ const cutoff = now - 5000;
166
+ for (const [k, v] of this.dedupMap) {
167
+ if (typeof v === 'number' && v < cutoff) this.dedupMap.delete(k);
156
168
  }
157
169
  }
158
170
  return false;
@@ -160,10 +172,15 @@ class StreamingRenderer {
160
172
 
161
173
  /**
162
174
  * Generate deduplication key for event
175
+ * Use sessionId:seq for streaming_progress, fallback to type:id
163
176
  */
164
177
  getEventKey(event) {
165
178
  if (!event.type) return null;
166
- return `${event.type}:${event.id || event.sessionId || ''}`;
179
+ // For streaming events, use sessionId as primary key
180
+ if (event.sessionId) {
181
+ return `${event.sessionId}:${event.type}`;
182
+ }
183
+ return `${event.type}:${event.id || ''}`;
167
184
  }
168
185
 
169
186
  /**
@@ -480,8 +480,14 @@ class WebSocketManager {
480
480
  }
481
481
 
482
482
  resubscribeAll() {
483
+ // After reconnect, query server state for all conversations with active subscriptions
484
+ // This ensures client streaming state matches server state
485
+ const conversationIds = new Set();
483
486
  for (const key of this.activeSubscriptions) {
484
487
  const [type, id] = key.split(':');
488
+ if (type === 'conversation') {
489
+ conversationIds.add(id);
490
+ }
485
491
  const msg = { type: 'subscribe', timestamp: Date.now() };
486
492
  if (type === 'session') msg.sessionId = id;
487
493
  else msg.conversationId = id;
@@ -490,6 +496,21 @@ class WebSocketManager {
490
496
  this.stats.totalMessagesSent++;
491
497
  } catch (_) {}
492
498
  }
499
+
500
+ // After resubscribing, query streaming state for each conversation
501
+ // This prevents stale UI state after network hiccup
502
+ if (conversationIds.size > 0) {
503
+ conversationIds.forEach(convId => {
504
+ this.sendMessage({
505
+ type: 'conv.get',
506
+ id: convId,
507
+ timestamp: Date.now()
508
+ }).catch(() => {
509
+ // Silently ignore query failures - server will send streaming_start
510
+ // on subscription confirmation if execution is active
511
+ });
512
+ });
513
+ }
493
514
  }
494
515
 
495
516
  unsubscribeFromSession(sessionId) {