agentgui 1.0.597 → 1.0.599

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
@@ -949,7 +949,15 @@ export const queries = {
949
949
  return result.changes || 0;
950
950
  },
951
951
 
952
- createEvent(type, data, conversationId = null, sessionId = null) {
952
+ createEvent(type, data, conversationId = null, sessionId = null, idempotencyKey = null) {
953
+ if (idempotencyKey) {
954
+ const cached = this.getIdempotencyKey(idempotencyKey);
955
+ if (cached) {
956
+ console.log(`[event-idempotency] Event already exists for key ${idempotencyKey}, returning cached`);
957
+ return JSON.parse(cached);
958
+ }
959
+ }
960
+
953
961
  const id = generateId('evt');
954
962
  const now = Date.now();
955
963
 
@@ -958,7 +966,7 @@ export const queries = {
958
966
  );
959
967
  stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
960
968
 
961
- return {
969
+ const event = {
962
970
  id,
963
971
  type,
964
972
  conversationId,
@@ -966,6 +974,12 @@ export const queries = {
966
974
  data,
967
975
  created_at: now
968
976
  };
977
+
978
+ if (idempotencyKey) {
979
+ this.setIdempotencyKey(idempotencyKey, event);
980
+ }
981
+
982
+ return event;
969
983
  },
970
984
 
971
985
  getEvent(id) {
@@ -8,6 +8,7 @@ class CheckpointManager {
8
8
  constructor(queries) {
9
9
  this.queries = queries;
10
10
  this._injectedSessions = new Set(); // Track which sessions already had checkpoints injected
11
+ this._pendingCheckpoints = new Map(); // Store checkpoints to inject on subscribe: { conversationId -> checkpoint }
11
12
  }
12
13
 
13
14
  /**
@@ -146,6 +147,36 @@ class CheckpointManager {
146
147
  reset() {
147
148
  this._injectedSessions.clear();
148
149
  }
150
+
151
+ /**
152
+ * Store a checkpoint to be injected when client subscribes
153
+ * (Used during server startup when no clients are connected yet)
154
+ */
155
+ storeCheckpointForDelay(conversationId, checkpoint) {
156
+ if (checkpoint && checkpoint.events && checkpoint.events.length > 0) {
157
+ this._pendingCheckpoints.set(conversationId, checkpoint);
158
+ console.log(`[checkpoint] Stored checkpoint for ${conversationId} to inject on subscribe (${checkpoint.events.length} events, ${checkpoint.chunks.length} chunks)`);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get and remove a pending checkpoint (called when client subscribes)
164
+ */
165
+ getPendingCheckpoint(conversationId) {
166
+ const checkpoint = this._pendingCheckpoints.get(conversationId);
167
+ if (checkpoint) {
168
+ this._pendingCheckpoints.delete(conversationId);
169
+ console.log(`[checkpoint] Retrieved pending checkpoint for ${conversationId}`);
170
+ }
171
+ return checkpoint;
172
+ }
173
+
174
+ /**
175
+ * Check if there's a pending checkpoint for a conversation
176
+ */
177
+ hasPendingCheckpoint(conversationId) {
178
+ return this._pendingCheckpoints.has(conversationId);
179
+ }
149
180
  }
150
181
 
151
182
  export default CheckpointManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.597",
3
+ "version": "1.0.599",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -4242,6 +4242,49 @@ wsRouter.onLegacy((data, ws) => {
4242
4242
  conversationId: data.conversationId,
4243
4243
  timestamp: Date.now()
4244
4244
  }));
4245
+
4246
+ // Notify client if this conversation has an active streaming execution
4247
+ if (data.conversationId && activeExecutions.has(data.conversationId)) {
4248
+ const execution = activeExecutions.get(data.conversationId);
4249
+ const conv = queries.getConversation(data.conversationId);
4250
+ ws.send(JSON.stringify({
4251
+ type: 'streaming_start',
4252
+ sessionId: execution.sessionId,
4253
+ conversationId: data.conversationId,
4254
+ agentId: conv?.agentType || conv?.agentId || 'claude-code',
4255
+ resumed: true,
4256
+ timestamp: Date.now()
4257
+ }));
4258
+ }
4259
+
4260
+ // Inject pending checkpoint events if this is a conversation subscription
4261
+ if (data.conversationId && checkpointManager.hasPendingCheckpoint(data.conversationId)) {
4262
+ const checkpoint = checkpointManager.getPendingCheckpoint(data.conversationId);
4263
+ if (checkpoint) {
4264
+ console.log(`[checkpoint] Injecting ${checkpoint.events.length} events to client for ${data.conversationId}`);
4265
+
4266
+ const latestSession = queries.getLatestSession(data.conversationId);
4267
+ if (latestSession) {
4268
+ ws.send(JSON.stringify({
4269
+ type: 'streaming_resumed',
4270
+ sessionId: latestSession.id,
4271
+ conversationId: data.conversationId,
4272
+ resumeFrom: checkpoint.sessionId,
4273
+ eventCount: checkpoint.events.length,
4274
+ chunkCount: checkpoint.chunks.length,
4275
+ timestamp: Date.now()
4276
+ }));
4277
+
4278
+ checkpointManager.injectCheckpointEvents(latestSession.id, checkpoint, (evt) => {
4279
+ ws.send(JSON.stringify({
4280
+ ...evt,
4281
+ sessionId: latestSession.id,
4282
+ conversationId: data.conversationId
4283
+ }));
4284
+ });
4285
+ }
4286
+ }
4287
+ }
4245
4288
  } else if (data.type === 'unsubscribe') {
4246
4289
  if (data.sessionId) {
4247
4290
  ws.subscriptions.delete(data.sessionId);
@@ -4585,27 +4628,11 @@ async function resumeInterruptedStreams() {
4585
4628
  timestamp: Date.now()
4586
4629
  });
4587
4630
 
4588
- // Inject checkpoint events before streaming starts
4631
+ // Store checkpoint to inject when client subscribes (not now, since no clients connected yet)
4589
4632
  if (checkpoint) {
4590
- broadcastSync({
4591
- type: 'streaming_resumed',
4592
- sessionId: session.id,
4593
- conversationId: conv.id,
4594
- resumeFrom: previousSessionId,
4595
- eventCount: checkpoint.events.length,
4596
- chunkCount: checkpoint.chunks.length,
4597
- timestamp: Date.now()
4598
- });
4599
-
4600
- checkpointManager.injectCheckpointEvents(session.id, checkpoint, (evt) => {
4601
- broadcastSync({
4602
- ...evt,
4603
- sessionId: session.id,
4604
- conversationId: conv.id
4605
- });
4606
- });
4607
-
4633
+ checkpointManager.storeCheckpointForDelay(conv.id, checkpoint);
4608
4634
  checkpointManager.markSessionResumed(previousSessionId);
4635
+ console.log(`[RESUME] Checkpoint stored for ${conv.id}, will inject on next client subscribe`);
4609
4636
  }
4610
4637
 
4611
4638
  const messageId = lastMsg?.id || null;
@@ -696,6 +696,9 @@ class AgentGUIClient {
696
696
  case 'streaming_start':
697
697
  this.handleStreamingStart(data).catch(e => console.error('handleStreamingStart error:', e));
698
698
  break;
699
+ case 'streaming_resumed':
700
+ this.handleStreamingResumed(data).catch(e => console.error('handleStreamingResumed error:', e));
701
+ break;
699
702
  case 'streaming_progress':
700
703
  this.handleStreamingProgress(data);
701
704
  break;
@@ -920,6 +923,19 @@ class AgentGUIClient {
920
923
  this.emit('streaming:start', data);
921
924
  }
922
925
 
926
+ async handleStreamingResumed(data) {
927
+ console.log('Streaming resumed:', data);
928
+ const conv = this.state.currentConversation || { id: data.conversationId };
929
+ await this.handleStreamingStart({
930
+ type: 'streaming_start',
931
+ sessionId: data.sessionId,
932
+ conversationId: data.conversationId,
933
+ agentId: conv.agentType || conv.agentId || 'claude-code',
934
+ resumed: true,
935
+ timestamp: data.timestamp
936
+ });
937
+ }
938
+
923
939
  handleStreamingProgress(data) {
924
940
  // NOTE: With chunk-based architecture, blocks are rendered from polling
925
941
  // This handler is kept for backward compatibility and to trigger polling updates
@@ -3284,122 +3300,122 @@ document.addEventListener('DOMContentLoaded', async () => {
3284
3300
  if (typeof module !== 'undefined' && module.exports) {
3285
3301
  module.exports = AgentGUIClient;
3286
3302
  }
3287
- // PHASE 2: Generate unique request ID
3288
- _generateRequestId() {
3289
- return ++this._currentRequestId;
3290
- }
3291
-
3292
- // PHASE 2: Make a load request with tracking
3293
- _makeLoadRequest(conversationId) {
3294
- const requestId = this._generateRequestId();
3295
- const abortController = new AbortController();
3296
-
3297
- // Cancel previous request for this conversation if exists
3298
- const prev = this._loadInProgress[conversationId];
3299
- if (prev?.abortController) {
3300
- prev.abortController.abort();
3301
- }
3302
-
3303
- this._loadInProgress[conversationId] = {
3304
- requestId,
3305
- abortController,
3306
- timestamp: Date.now(),
3307
- conversationId
3308
- };
3309
-
3310
- return { requestId, abortController };
3311
- }
3312
-
3313
- // PHASE 2: Verify request is current before rendering
3314
- _isCurrentRequest(conversationId, requestId) {
3315
- const current = this._loadInProgress[conversationId];
3316
- return current?.requestId === requestId;
3317
- }
3318
-
3319
- // PHASE 3: Queue WebSocket message based on priority
3320
- _queueWebSocketMessage(data) {
3321
- const highPriorityTypes = ['conversation_deleted', 'all_conversations_deleted'];
3322
-
3323
- if (highPriorityTypes.includes(data.type)) {
3324
- this._highPriorityQueue.push(data);
3325
- } else {
3326
- this._lowPriorityQueue.push(data);
3327
- }
3328
- }
3329
-
3330
- // PHASE 3: Process queued WebSocket messages
3331
- _drainMessageQueues() {
3332
- // Process high-priority first (deletions)
3333
- while (this._highPriorityQueue.length > 0) {
3334
- const msg = this._highPriorityQueue.shift();
3335
- this._processWebSocketMessageDirect(msg);
3336
- }
3337
-
3338
- // Then process low-priority (metadata)
3339
- while (this._lowPriorityQueue.length > 0) {
3340
- const msg = this._lowPriorityQueue.shift();
3341
- this._processWebSocketMessageDirect(msg);
3342
- }
3343
- }
3344
-
3345
- // PHASE 3: Direct WebSocket message processing (extracted from switch)
3346
- _processWebSocketMessageDirect(data) {
3347
- switch (data.type) {
3348
- case 'streaming_start':
3349
- this.handleStreamingStart(data).catch(e => console.error('handleStreamingStart error:', e));
3350
- break;
3351
- case 'streaming_progress':
3352
- this.handleStreamingProgress(data);
3353
- break;
3354
- case 'streaming_complete':
3355
- this.handleStreamingComplete(data);
3356
- break;
3357
- case 'streaming_error':
3358
- this.handleStreamingError(data);
3359
- break;
3360
- case 'conversation_created':
3361
- this.handleConversationCreated(data);
3362
- break;
3363
- case 'conversation_deleted':
3364
- this.handleConversationDeleted(data);
3365
- break;
3366
- case 'all_conversations_deleted':
3367
- this.handleAllConversationsDeleted(data);
3368
- break;
3369
- case 'message_created':
3370
- this.handleMessageCreated(data);
3371
- break;
3372
- case 'conversation_updated':
3373
- this.handleConversationUpdated(data);
3374
- break;
3375
- case 'message_updated':
3376
- this.handleMessageUpdated(data);
3377
- break;
3378
- default:
3379
- // Other types handled elsewhere
3380
- break;
3381
- }
3382
- }
3383
-
3384
- // PHASE 4: Track streaming event sequence
3385
- _recordStreamingSequence(sessionId, sequence) {
3386
- this._lastProcessedSequence[sessionId] = sequence;
3387
- }
3388
-
3389
- // PHASE 4: Verify streaming event is current and in-order
3390
- _isValidStreamingEvent(event) {
3391
- // Must be for current session
3392
- if (event.sessionId !== this.state.currentSession?.id) {
3393
- return false;
3394
- }
3395
-
3396
- // Check sequence number
3397
- const lastSeq = this._lastProcessedSequence[event.sessionId] || -1;
3398
- if (event.sequence !== undefined && event.sequence <= lastSeq) {
3399
- return false; // Duplicate or out-of-order
3400
- }
3401
-
3402
- return true;
3403
- }
3404
-
3405
-
3303
+ // PHASE 2: Generate unique request ID
3304
+ _generateRequestId() {
3305
+ return ++this._currentRequestId;
3306
+ }
3307
+
3308
+ // PHASE 2: Make a load request with tracking
3309
+ _makeLoadRequest(conversationId) {
3310
+ const requestId = this._generateRequestId();
3311
+ const abortController = new AbortController();
3312
+
3313
+ // Cancel previous request for this conversation if exists
3314
+ const prev = this._loadInProgress[conversationId];
3315
+ if (prev?.abortController) {
3316
+ prev.abortController.abort();
3317
+ }
3318
+
3319
+ this._loadInProgress[conversationId] = {
3320
+ requestId,
3321
+ abortController,
3322
+ timestamp: Date.now(),
3323
+ conversationId
3324
+ };
3325
+
3326
+ return { requestId, abortController };
3327
+ }
3328
+
3329
+ // PHASE 2: Verify request is current before rendering
3330
+ _isCurrentRequest(conversationId, requestId) {
3331
+ const current = this._loadInProgress[conversationId];
3332
+ return current?.requestId === requestId;
3333
+ }
3334
+
3335
+ // PHASE 3: Queue WebSocket message based on priority
3336
+ _queueWebSocketMessage(data) {
3337
+ const highPriorityTypes = ['conversation_deleted', 'all_conversations_deleted'];
3338
+
3339
+ if (highPriorityTypes.includes(data.type)) {
3340
+ this._highPriorityQueue.push(data);
3341
+ } else {
3342
+ this._lowPriorityQueue.push(data);
3343
+ }
3344
+ }
3345
+
3346
+ // PHASE 3: Process queued WebSocket messages
3347
+ _drainMessageQueues() {
3348
+ // Process high-priority first (deletions)
3349
+ while (this._highPriorityQueue.length > 0) {
3350
+ const msg = this._highPriorityQueue.shift();
3351
+ this._processWebSocketMessageDirect(msg);
3352
+ }
3353
+
3354
+ // Then process low-priority (metadata)
3355
+ while (this._lowPriorityQueue.length > 0) {
3356
+ const msg = this._lowPriorityQueue.shift();
3357
+ this._processWebSocketMessageDirect(msg);
3358
+ }
3359
+ }
3360
+
3361
+ // PHASE 3: Direct WebSocket message processing (extracted from switch)
3362
+ _processWebSocketMessageDirect(data) {
3363
+ switch (data.type) {
3364
+ case 'streaming_start':
3365
+ this.handleStreamingStart(data).catch(e => console.error('handleStreamingStart error:', e));
3366
+ break;
3367
+ case 'streaming_progress':
3368
+ this.handleStreamingProgress(data);
3369
+ break;
3370
+ case 'streaming_complete':
3371
+ this.handleStreamingComplete(data);
3372
+ break;
3373
+ case 'streaming_error':
3374
+ this.handleStreamingError(data);
3375
+ break;
3376
+ case 'conversation_created':
3377
+ this.handleConversationCreated(data);
3378
+ break;
3379
+ case 'conversation_deleted':
3380
+ this.handleConversationDeleted(data);
3381
+ break;
3382
+ case 'all_conversations_deleted':
3383
+ this.handleAllConversationsDeleted(data);
3384
+ break;
3385
+ case 'message_created':
3386
+ this.handleMessageCreated(data);
3387
+ break;
3388
+ case 'conversation_updated':
3389
+ this.handleConversationUpdated(data);
3390
+ break;
3391
+ case 'message_updated':
3392
+ this.handleMessageUpdated(data);
3393
+ break;
3394
+ default:
3395
+ // Other types handled elsewhere
3396
+ break;
3397
+ }
3398
+ }
3399
+
3400
+ // PHASE 4: Track streaming event sequence
3401
+ _recordStreamingSequence(sessionId, sequence) {
3402
+ this._lastProcessedSequence[sessionId] = sequence;
3403
+ }
3404
+
3405
+ // PHASE 4: Verify streaming event is current and in-order
3406
+ _isValidStreamingEvent(event) {
3407
+ // Must be for current session
3408
+ if (event.sessionId !== this.state.currentSession?.id) {
3409
+ return false;
3410
+ }
3411
+
3412
+ // Check sequence number
3413
+ const lastSeq = this._lastProcessedSequence[event.sessionId] || -1;
3414
+ if (event.sequence !== undefined && event.sequence <= lastSeq) {
3415
+ return false; // Duplicate or out-of-order
3416
+ }
3417
+
3418
+ return true;
3419
+ }
3420
+
3421
+