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.
- package/lib/ws-handlers-conv.js +36 -2
- package/package.json +1 -1
- package/server.js +3 -0
- package/static/js/streaming-renderer.js +25 -8
- package/static/js/websocket-manager.js +21 -0
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
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
|
|
145
|
-
const now = Date.now();
|
|
146
|
+
const lastSeq = this.dedupMap.get(key);
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
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 -
|
|
154
|
-
for (const [k,
|
|
155
|
-
if (
|
|
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
|
-
|
|
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) {
|