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 +16 -2
- package/lib/checkpoint-manager.js +31 -0
- package/package.json +1 -1
- package/server.js +46 -19
- package/static/js/client.js +135 -119
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
|
-
|
|
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
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
|
-
//
|
|
4631
|
+
// Store checkpoint to inject when client subscribes (not now, since no clients connected yet)
|
|
4589
4632
|
if (checkpoint) {
|
|
4590
|
-
|
|
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;
|
package/static/js/client.js
CHANGED
|
@@ -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
|
+
|