agentgui 1.0.110 → 1.0.111

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.
@@ -1,615 +0,0 @@
1
- /**
2
- * SYNC-SERVICE.TS - Independent sync engine
3
- * Handles all conversation and message synchronization
4
- * Guaranteed eventual consistency with conflict resolution
5
- * Deduplicates operations and implements exponential backoff
6
- */
7
-
8
- import { EventEmitter } from 'events';
9
- import {
10
- Conversation,
11
- Message,
12
- SyncEvent,
13
- SyncStatus,
14
- SyncError,
15
- ConflictResolutionStrategy,
16
- StreamingEvent,
17
- ExecutionMetadata,
18
- } from './types';
19
- import DatabaseService from './database-service';
20
-
21
- interface SyncOptions {
22
- retryAttempts?: number;
23
- retryDelay?: number;
24
- maxRetryDelay?: number;
25
- conflictResolution?: ConflictResolutionStrategy;
26
- batchSize?: number;
27
- }
28
-
29
- /**
30
- * SyncService - Independent sync operations
31
- * Handles conversations and messages with conflict resolution
32
- */
33
- export class SyncService extends EventEmitter {
34
- private db: DatabaseService;
35
- private syncInProgress = false;
36
- private lastSyncTime = 0;
37
- private pendingOperations: Map<string, SyncEvent> = new Map();
38
- private retryAttempts = 0;
39
- private options: Required<SyncOptions>;
40
-
41
- constructor(db: DatabaseService, options: SyncOptions = {}) {
42
- super();
43
- this.db = db;
44
- this.options = {
45
- retryAttempts: options.retryAttempts ?? 5,
46
- retryDelay: options.retryDelay ?? 1000,
47
- maxRetryDelay: options.maxRetryDelay ?? 30000,
48
- conflictResolution: options.conflictResolution ?? 'last-write-wins',
49
- batchSize: options.batchSize ?? 50,
50
- };
51
- }
52
-
53
- // =========================================================================
54
- // SYNC OPERATIONS
55
- // =========================================================================
56
-
57
- async syncConversations(fromServer: Conversation[]): Promise<SyncStatus> {
58
- if (this.syncInProgress) {
59
- return {
60
- state: 'loading',
61
- retryCount: this.retryAttempts,
62
- maxRetries: this.options.retryAttempts,
63
- };
64
- }
65
-
66
- this.syncInProgress = true;
67
- try {
68
- this.emit('sync:start', { type: 'conversations' });
69
-
70
- const local = this.db.getConversationsList();
71
- const changes = this.detectChanges(local, fromServer);
72
-
73
- if (changes.added.length > 0) {
74
- await this.applyAddedConversations(changes.added);
75
- }
76
-
77
- if (changes.updated.length > 0) {
78
- await this.applyUpdatedConversations(changes.updated);
79
- }
80
-
81
- if (changes.deleted.length > 0) {
82
- await this.applyDeletedConversations(changes.deleted);
83
- }
84
-
85
- this.lastSyncTime = Date.now();
86
- this.retryAttempts = 0;
87
-
88
- this.emit('sync:complete', {
89
- type: 'conversations',
90
- changes,
91
- });
92
-
93
- return {
94
- state: 'synced',
95
- lastSyncTime: this.lastSyncTime,
96
- retryCount: 0,
97
- maxRetries: this.options.retryAttempts,
98
- };
99
- } catch (err) {
100
- return this.handleSyncError(err as Error);
101
- } finally {
102
- this.syncInProgress = false;
103
- }
104
- }
105
-
106
- async syncMessages(conversationId: string, fromServer: Message[]): Promise<SyncStatus> {
107
- try {
108
- this.emit('sync:start', { type: 'messages', conversationId });
109
-
110
- const local = this.db.getConversationMessages(conversationId);
111
- const changes = this.detectMessageChanges(local, fromServer);
112
-
113
- if (changes.added.length > 0) {
114
- await this.applyAddedMessages(conversationId, changes.added);
115
- }
116
-
117
- if (changes.deleted.length > 0) {
118
- await this.applyDeletedMessages(changes.deleted);
119
- }
120
-
121
- this.emit('sync:complete', {
122
- type: 'messages',
123
- conversationId,
124
- changes,
125
- });
126
-
127
- return {
128
- state: 'synced',
129
- lastSyncTime: Date.now(),
130
- retryCount: 0,
131
- maxRetries: this.options.retryAttempts,
132
- };
133
- } catch (err) {
134
- return this.handleSyncError(err as Error);
135
- }
136
- }
137
-
138
- // =========================================================================
139
- // CHANGE DETECTION
140
- // =========================================================================
141
-
142
- private detectChanges(local: Conversation[], remote: Conversation[]) {
143
- const localMap = new Map(local.map((c) => [c.id, c]));
144
- const remoteMap = new Map(remote.map((c) => [c.id, c]));
145
-
146
- const added = remote.filter((c) => !localMap.has(c.id));
147
- const deleted = local.filter((c) => !remoteMap.has(c.id) && c.status !== 'deleted');
148
- const updated = remote.filter((c) => {
149
- const localVersion = localMap.get(c.id);
150
- return localVersion && localVersion.updated_at < c.updated_at;
151
- });
152
-
153
- return { added, updated, deleted };
154
- }
155
-
156
- private detectMessageChanges(local: Message[], remote: Message[]) {
157
- const localMap = new Map(local.map((m) => [m.id, m]));
158
- const remoteMap = new Map(remote.map((m) => [m.id, m]));
159
-
160
- const added = remote.filter((m) => !localMap.has(m.id));
161
- const deleted = local.filter((m) => !remoteMap.has(m.id));
162
-
163
- return { added, deleted };
164
- }
165
-
166
- // =========================================================================
167
- // APPLY CHANGES
168
- // =========================================================================
169
-
170
- private async applyAddedConversations(conversations: Conversation[]): Promise<void> {
171
- for (const conv of conversations) {
172
- try {
173
- // Note: In real implementation, would insert into DB
174
- // Here we just validate the data
175
- if (!conv.id || !conv.agentId) {
176
- throw new Error('Invalid conversation: missing id or agentId');
177
- }
178
- } catch (err) {
179
- this.emit('sync:error', {
180
- type: 'add_conversation',
181
- error: (err as Error).message,
182
- data: conv,
183
- });
184
- }
185
- }
186
- }
187
-
188
- private async applyUpdatedConversations(conversations: Conversation[]): Promise<void> {
189
- for (const conv of conversations) {
190
- try {
191
- if (!conv.id) throw new Error('Invalid conversation: missing id');
192
- // Update would happen here in real implementation
193
- } catch (err) {
194
- this.emit('sync:error', {
195
- type: 'update_conversation',
196
- error: (err as Error).message,
197
- data: conv,
198
- });
199
- }
200
- }
201
- }
202
-
203
- private async applyDeletedConversations(conversations: Conversation[]): Promise<void> {
204
- for (const conv of conversations) {
205
- try {
206
- if (!conv.id) throw new Error('Invalid conversation: missing id');
207
- this.db.deleteConversation(conv.id);
208
- } catch (err) {
209
- this.emit('sync:error', {
210
- type: 'delete_conversation',
211
- error: (err as Error).message,
212
- data: conv,
213
- });
214
- }
215
- }
216
- }
217
-
218
- private async applyAddedMessages(conversationId: string, messages: Message[]): Promise<void> {
219
- for (const msg of messages) {
220
- try {
221
- if (!msg.id || !msg.role) {
222
- throw new Error('Invalid message: missing id or role');
223
- }
224
- // Message insert would happen here in real implementation
225
- } catch (err) {
226
- this.emit('sync:error', {
227
- type: 'add_message',
228
- error: (err as Error).message,
229
- data: msg,
230
- });
231
- }
232
- }
233
- }
234
-
235
- private async applyDeletedMessages(messages: Message[]): Promise<void> {
236
- for (const msg of messages) {
237
- try {
238
- if (!msg.id) throw new Error('Invalid message: missing id');
239
- this.db.deleteMessage(msg.id);
240
- } catch (err) {
241
- this.emit('sync:error', {
242
- type: 'delete_message',
243
- error: (err as Error).message,
244
- data: msg,
245
- });
246
- }
247
- }
248
- }
249
-
250
- // =========================================================================
251
- // ERROR HANDLING & RETRY LOGIC
252
- // =========================================================================
253
-
254
- private handleSyncError(error: Error): SyncStatus {
255
- this.retryAttempts++;
256
- const isRetryable = this.retryAttempts < this.options.retryAttempts;
257
-
258
- const delay = Math.min(
259
- this.options.retryDelay * Math.pow(2, this.retryAttempts - 1),
260
- this.options.maxRetryDelay
261
- );
262
-
263
- if (isRetryable) {
264
- console.log(`[SyncService] Retry in ${delay}ms (attempt ${this.retryAttempts}/${this.options.retryAttempts})`);
265
- setTimeout(() => this.emit('sync:retry'), delay);
266
- }
267
-
268
- this.emit('sync:error', {
269
- error: error.message,
270
- retryable: isRetryable,
271
- attempts: this.retryAttempts,
272
- });
273
-
274
- return {
275
- state: isRetryable ? 'error' : 'error',
276
- error: error.message,
277
- retryCount: this.retryAttempts,
278
- maxRetries: this.options.retryAttempts,
279
- nextRetryTime: isRetryable ? Date.now() + delay : undefined,
280
- };
281
- }
282
-
283
- // =========================================================================
284
- // QUEUE MANAGEMENT
285
- // =========================================================================
286
-
287
- queueOperation(op: SyncEvent): void {
288
- const key = `${op.type}:${op.data.id || 'global'}`;
289
- this.pendingOperations.set(key, op);
290
- this.emit('queue:updated', { size: this.pendingOperations.size });
291
- }
292
-
293
- async flushQueue(): Promise<void> {
294
- if (this.pendingOperations.size === 0) return;
295
-
296
- const ops = Array.from(this.pendingOperations.values());
297
- this.pendingOperations.clear();
298
-
299
- for (const op of ops) {
300
- try {
301
- await this.processOperation(op);
302
- } catch (err) {
303
- this.emit('queue:error', {
304
- operation: op,
305
- error: (err as Error).message,
306
- });
307
- // Re-queue failed operation
308
- this.queueOperation(op);
309
- }
310
- }
311
- }
312
-
313
- private async processOperation(op: SyncEvent): Promise<void> {
314
- // Implementation would process each operation based on type
315
- this.emit('operation:processed', op);
316
- }
317
-
318
- // =========================================================================
319
- // STREAMING EXECUTION SYNC
320
- // =========================================================================
321
-
322
- async syncSessionExecution(sessionId: string, events: StreamingEvent[]): Promise<SyncStatus> {
323
- try {
324
- this.emit('sync:start', { type: 'streaming', sessionId });
325
-
326
- // Deduplicate by eventId (if present)
327
- const dedupMap = new Map<string, StreamingEvent>();
328
- for (const event of events) {
329
- const key = event.eventId || `${event.type}:${event.timestamp}`;
330
- dedupMap.set(key, event);
331
- }
332
-
333
- // Store all execution events in batch
334
- const uniqueEvents = Array.from(dedupMap.values());
335
- this.db.batchStoreExecutionEvents(sessionId, uniqueEvents);
336
-
337
- this.emit('sync:complete', {
338
- type: 'streaming',
339
- sessionId,
340
- eventCount: uniqueEvents.length,
341
- });
342
-
343
- return {
344
- state: 'synced',
345
- lastSyncTime: Date.now(),
346
- retryCount: 0,
347
- maxRetries: this.options.retryAttempts,
348
- };
349
- } catch (err) {
350
- return this.handleSyncError(err as Error);
351
- }
352
- }
353
-
354
- async syncExecutionMetadata(sessionId: string, metadata: ExecutionMetadata): Promise<SyncStatus> {
355
- try {
356
- this.emit('sync:start', { type: 'metadata', sessionId });
357
-
358
- this.db.storeExecutionMetadata(sessionId, metadata);
359
-
360
- this.emit('sync:complete', {
361
- type: 'metadata',
362
- sessionId,
363
- metadata,
364
- });
365
-
366
- return {
367
- state: 'synced',
368
- lastSyncTime: Date.now(),
369
- retryCount: 0,
370
- maxRetries: this.options.retryAttempts,
371
- };
372
- } catch (err) {
373
- return this.handleSyncError(err as Error);
374
- }
375
- }
376
-
377
- async flushStreamingQueue(): Promise<{ flushed: number; failed: number; deduplicated: number }> {
378
- if (this.pendingOperations.size === 0) {
379
- return { flushed: 0, failed: 0, deduplicated: 0 };
380
- }
381
-
382
- const ops = Array.from(this.pendingOperations.values());
383
- this.pendingOperations.clear();
384
-
385
- // Dedup by operation key
386
- const dedupMap = new Map<string, SyncEvent>();
387
- let dedupCount = 0;
388
- for (const op of ops) {
389
- const key = `${op.type}:${op.data.id || op.data.sessionId || 'global'}`;
390
- if (dedupMap.has(key)) {
391
- dedupCount++;
392
- } else {
393
- dedupMap.set(key, op);
394
- }
395
- }
396
-
397
- // Group by type for ordering preservation
398
- const byType = new Map<string, SyncEvent[]>();
399
- for (const op of dedupMap.values()) {
400
- if (!byType.has(op.type)) byType.set(op.type, []);
401
- byType.get(op.type)!.push(op);
402
- }
403
-
404
- // Sort by timestamp within each type
405
- for (const events of byType.values()) {
406
- events.sort((a, b) => a.timestamp - b.timestamp);
407
- }
408
-
409
- let flushed = 0;
410
- let failed = 0;
411
-
412
- for (const [type, events] of byType) {
413
- for (const op of events) {
414
- try {
415
- await this.processOperation(op);
416
- this.emit('queue:item_processed', op);
417
- flushed++;
418
- } catch (err) {
419
- this.emit('queue:error', {
420
- operation: op,
421
- error: (err as Error).message,
422
- });
423
- this.queueOperation(op);
424
- failed++;
425
- }
426
- }
427
- }
428
-
429
- this.emit('queue:flushed', { flushed, failed, deduplicated: dedupCount });
430
- return { flushed, failed, deduplicated: dedupCount };
431
- }
432
-
433
- async flushStreamingQueueWithTimeout(timeoutMs = 30000): Promise<{ flushed: number; failed: number; timedOut: boolean }> {
434
- const startTime = Date.now();
435
- let flushed = 0;
436
- let failed = 0;
437
- let timedOut = false;
438
-
439
- try {
440
- while (this.pendingOperations.size > 0) {
441
- const elapsed = Date.now() - startTime;
442
- if (elapsed > timeoutMs) {
443
- timedOut = true;
444
- break;
445
- }
446
-
447
- const result = await this.flushStreamingQueue();
448
- flushed += result.flushed;
449
- failed += result.failed;
450
-
451
- if (result.flushed === 0) break;
452
- }
453
- } catch (err) {
454
- this.emit('queue:timeout', {
455
- error: (err as Error).message,
456
- flushed,
457
- failed,
458
- });
459
- }
460
-
461
- return { flushed, failed, timedOut };
462
- }
463
-
464
- // =========================================================================
465
- // STATUS & INFO
466
- // =========================================================================
467
-
468
- getStatus(): SyncStatus {
469
- return {
470
- state: this.syncInProgress ? 'loading' : 'synced',
471
- lastSyncTime: this.lastSyncTime,
472
- retryCount: this.retryAttempts,
473
- maxRetries: this.options.retryAttempts,
474
- };
475
- }
476
-
477
- getPendingOperationsCount(): number {
478
- return this.pendingOperations.size;
479
- }
480
-
481
- clear(): void {
482
- this.pendingOperations.clear();
483
- this.retryAttempts = 0;
484
- this.lastSyncTime = 0;
485
- }
486
-
487
- // =========================================================================
488
- // RECOVERY & RESILIENCE
489
- // =========================================================================
490
-
491
- async recoverIncompleteStreams(): Promise<{ recovered: number; failed: number; timedOut: number }> {
492
- try {
493
- this.emit('recovery:start', { type: 'incomplete_streams' });
494
-
495
- const incompleteSessionsOlderThan30Min = this.db.getIncompleteSessionsOlderThan(30);
496
- let recovered = 0;
497
- let failed = 0;
498
- let timedOut = 0;
499
-
500
- for (const session of incompleteSessionsOlderThan30Min) {
501
- try {
502
- const ageMinutes = (Date.now() - session.started_at) / (60 * 1000);
503
-
504
- if (ageMinutes > 120) {
505
- // 2 hour timeout
506
- this.db.markSessionComplete(session.id, 'timeout');
507
- timedOut++;
508
- this.emit('recovery:timeout', { sessionId: session.id, ageMinutes });
509
- } else {
510
- // Mark for retry
511
- this.queueOperation({
512
- type: 'retry_incomplete_session',
513
- timestamp: Date.now(),
514
- data: { sessionId: session.id }
515
- });
516
- recovered++;
517
- this.emit('recovery:queued', { sessionId: session.id });
518
- }
519
- } catch (err) {
520
- failed++;
521
- this.emit('recovery:error', {
522
- sessionId: session.id,
523
- error: (err as Error).message,
524
- });
525
- }
526
- }
527
-
528
- this.emit('recovery:complete', { recovered, failed, timedOut });
529
- return { recovered, failed, timedOut };
530
- } catch (err) {
531
- this.emit('recovery:failed', { error: (err as Error).message });
532
- return { recovered: 0, failed: 0, timedOut: 0 };
533
- }
534
- }
535
-
536
- async resolveExecutionConflicts(
537
- sessionId: string,
538
- localMetadata: ExecutionMetadata,
539
- remoteMetadata: ExecutionMetadata
540
- ): Promise<ExecutionMetadata> {
541
- try {
542
- // Last-write-wins strategy: use the one with latest completion time
543
- const localTime = localMetadata.endTime || 0;
544
- const remoteTime = remoteMetadata.endTime || 0;
545
-
546
- const winner = remoteTime >= localTime ? remoteMetadata : localMetadata;
547
-
548
- // Merge metadata: take token counts from both, use max
549
- const merged: ExecutionMetadata = {
550
- ...winner,
551
- inputTokens: Math.max(localMetadata.inputTokens || 0, remoteMetadata.inputTokens || 0),
552
- outputTokens: Math.max(localMetadata.outputTokens || 0, remoteMetadata.outputTokens || 0),
553
- totalTokens: Math.max(localMetadata.totalTokens || 0, remoteMetadata.totalTokens || 0),
554
- toolCalls: Math.max(localMetadata.toolCalls, remoteMetadata.toolCalls),
555
- toolResults: Math.max(localMetadata.toolResults, remoteMetadata.toolResults),
556
- errorCount: Math.max(localMetadata.errorCount, remoteMetadata.errorCount),
557
- };
558
-
559
- this.db.storeExecutionMetadata(sessionId, merged);
560
-
561
- this.emit('conflict:resolved', {
562
- sessionId,
563
- strategy: 'last-write-wins',
564
- winner: remoteTime >= localTime ? 'remote' : 'local',
565
- merged,
566
- });
567
-
568
- return merged;
569
- } catch (err) {
570
- this.emit('conflict:error', {
571
- sessionId,
572
- error: (err as Error).message,
573
- });
574
- throw err;
575
- }
576
- }
577
-
578
- async detectAndResolveConflicts(): Promise<{ resolved: number; failed: number }> {
579
- try {
580
- this.emit('conflict:detection_start');
581
-
582
- // This would scan for duplicate sessions with same conversation+timestamp
583
- // For now, return placeholder
584
- let resolved = 0;
585
- let failed = 0;
586
-
587
- this.emit('conflict:detection_complete', { resolved, failed });
588
- return { resolved, failed };
589
- } catch (err) {
590
- this.emit('conflict:detection_failed', { error: (err as Error).message });
591
- return { resolved: 0, failed: 0 };
592
- }
593
- }
594
-
595
- async flushOfflineQueue(): Promise<{ flushed: number; failed: number; retried: number }> {
596
- try {
597
- this.emit('offline_queue:flush_start');
598
-
599
- const result = await this.flushStreamingQueueWithTimeout(60000);
600
-
601
- this.emit('offline_queue:flush_complete', {
602
- flushed: result.flushed,
603
- failed: result.failed,
604
- timedOut: result.timedOut,
605
- });
606
-
607
- return { flushed: result.flushed, failed: result.failed, retried: 0 };
608
- } catch (err) {
609
- this.emit('offline_queue:flush_error', { error: (err as Error).message });
610
- return { flushed: 0, failed: 0, retried: 0 };
611
- }
612
- }
613
- }
614
-
615
- export default SyncService;