agentgui 1.0.110 → 1.0.112
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/.prd +281 -0
- package/database.js +87 -0
- package/package.json +2 -2
- package/server.js +25 -2
- package/static/index.html +50 -4
- package/static/js/client.js +14 -7
- package/static/js/conversations.js +41 -2
- package/conversation-importer.js +0 -63
- package/hot-reload-manager.js +0 -186
- package/lib/database-service.ts +0 -640
- package/lib/machines.ts +0 -190
- package/lib/schemas.ts +0 -190
- package/lib/sync-service.ts +0 -615
- package/lib/types.ts +0 -413
package/lib/sync-service.ts
DELETED
|
@@ -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;
|