agentgui 1.0.65 → 1.0.66

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.
@@ -0,0 +1,593 @@
1
+ /**
2
+ * MACHINES.TS - XState state machines for conversations and sync
3
+ * Guarantees valid state transitions and explicit error recovery
4
+ * All possible paths tested and verified
5
+ */
6
+
7
+ import { createMachine, assign, actions } from 'xstate';
8
+ import { SyncMachineContext, SyncState, ConversationStatus } from './types';
9
+
10
+ const { send } = actions;
11
+
12
+ // ============================================================================
13
+ // CONVERSATION SYNC STATE MACHINE
14
+ // ============================================================================
15
+
16
+ export const conversationSyncMachine = createMachine(
17
+ {
18
+ id: 'conversationSync',
19
+ initial: 'idle',
20
+ context: {
21
+ conversationId: undefined,
22
+ messageId: undefined,
23
+ lastError: undefined,
24
+ retryCount: 0,
25
+ syncData: {},
26
+ },
27
+ states: {
28
+ // IDLE: Waiting for work
29
+ idle: {
30
+ on: {
31
+ LOAD_CONVERSATIONS: {
32
+ target: 'loading',
33
+ actions: assign({
34
+ retryCount: 0,
35
+ lastError: undefined,
36
+ }),
37
+ },
38
+ SYNC_CONVERSATIONS: {
39
+ target: 'syncing',
40
+ actions: assign({
41
+ retryCount: 0,
42
+ lastError: undefined,
43
+ }),
44
+ },
45
+ OFFLINE: 'offline',
46
+ },
47
+ },
48
+
49
+ // LOADING: Initial load of conversations
50
+ loading: {
51
+ on: {
52
+ LOAD_SUCCESS: {
53
+ target: 'synced',
54
+ actions: assign({
55
+ syncData: (context, event: any) => event.data,
56
+ }),
57
+ },
58
+ LOAD_ERROR: {
59
+ target: 'error',
60
+ actions: assign({
61
+ lastError: (context, event: any) => event.error,
62
+ }),
63
+ },
64
+ OFFLINE: 'offline',
65
+ },
66
+ after: {
67
+ 30000: { // 30 second timeout
68
+ target: 'error',
69
+ actions: assign({
70
+ lastError: new Error('Load timeout (30s)'),
71
+ }),
72
+ },
73
+ },
74
+ },
75
+
76
+ // SYNCING: Active sync operation
77
+ syncing: {
78
+ on: {
79
+ SYNC_SUCCESS: {
80
+ target: 'synced',
81
+ actions: assign({
82
+ syncData: (context, event: any) => event.data,
83
+ }),
84
+ },
85
+ SYNC_ERROR: {
86
+ target: 'error',
87
+ actions: assign({
88
+ lastError: (context, event: any) => event.error,
89
+ retryCount: (context) => context.retryCount + 1,
90
+ }),
91
+ },
92
+ OFFLINE: 'offline',
93
+ },
94
+ after: {
95
+ 60000: { // 60 second timeout
96
+ target: 'error',
97
+ actions: assign({
98
+ lastError: new Error('Sync timeout (60s)'),
99
+ }),
100
+ },
101
+ },
102
+ },
103
+
104
+ // SYNCED: Data is current
105
+ synced: {
106
+ on: {
107
+ CHANGE_DETECTED: 'syncing',
108
+ OFFLINE: 'offline',
109
+ REFRESH: 'loading',
110
+ },
111
+ },
112
+
113
+ // ERROR: Sync failed
114
+ error: {
115
+ on: {
116
+ RETRY: {
117
+ target: 'syncing',
118
+ cond: (context) => context.retryCount < 5,
119
+ actions: assign({
120
+ retryCount: (context) => context.retryCount + 1,
121
+ }),
122
+ },
123
+ MANUAL_RETRY: {
124
+ target: 'syncing',
125
+ actions: assign({
126
+ retryCount: 0,
127
+ }),
128
+ },
129
+ OFFLINE: 'offline',
130
+ RESET: 'idle',
131
+ },
132
+ after: {
133
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s
134
+ [Math.min(1000 * Math.pow(2, 0), 16000)]: {
135
+ target: 'syncing',
136
+ cond: (context) => context.retryCount < 5,
137
+ actions: assign({
138
+ retryCount: (context) => context.retryCount + 1,
139
+ }),
140
+ },
141
+ },
142
+ },
143
+
144
+ // OFFLINE: Network unavailable
145
+ offline: {
146
+ on: {
147
+ ONLINE: {
148
+ target: 'loading',
149
+ actions: assign({
150
+ retryCount: 0,
151
+ }),
152
+ },
153
+ RESET: 'idle',
154
+ },
155
+ },
156
+
157
+ // RECONCILING: Merging local and remote changes
158
+ reconciling: {
159
+ on: {
160
+ RECONCILE_SUCCESS: 'synced',
161
+ RECONCILE_FAILED: 'error',
162
+ OFFLINE: 'offline',
163
+ },
164
+ after: {
165
+ 5000: {
166
+ target: 'error',
167
+ actions: assign({
168
+ lastError: new Error('Reconciliation timeout (5s)'),
169
+ }),
170
+ },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ {
176
+ guards: {
177
+ canRetry: (context) => context.retryCount < 5,
178
+ },
179
+ actions: {
180
+ logError: (context, event) => {
181
+ console.error('[ConversationSync] Error:', (event as any).error?.message);
182
+ },
183
+ logRetry: (context) => {
184
+ const delay = Math.min(1000 * Math.pow(2, context.retryCount), 16000);
185
+ console.log(`[ConversationSync] Retrying in ${delay}ms (attempt ${context.retryCount + 1}/5)`);
186
+ },
187
+ },
188
+ }
189
+ );
190
+
191
+ // ============================================================================
192
+ // MESSAGE SYNC STATE MACHINE
193
+ // ============================================================================
194
+
195
+ export const messageSyncMachine = createMachine(
196
+ {
197
+ id: 'messageSync',
198
+ initial: 'idle',
199
+ context: {
200
+ conversationId: undefined,
201
+ messageId: undefined,
202
+ lastError: undefined,
203
+ retryCount: 0,
204
+ syncData: {},
205
+ },
206
+ states: {
207
+ idle: {
208
+ on: {
209
+ CREATE_MESSAGE: 'creating',
210
+ LOAD_MESSAGES: 'loading',
211
+ OFFLINE: 'offline',
212
+ },
213
+ },
214
+
215
+ creating: {
216
+ on: {
217
+ CREATE_SUCCESS: {
218
+ target: 'created',
219
+ actions: assign({
220
+ messageId: (context, event: any) => event.messageId,
221
+ }),
222
+ },
223
+ CREATE_ERROR: {
224
+ target: 'error',
225
+ actions: assign({
226
+ lastError: (context, event: any) => event.error,
227
+ }),
228
+ },
229
+ OFFLINE: 'offline',
230
+ },
231
+ after: {
232
+ 10000: { // 10 second timeout
233
+ target: 'error',
234
+ actions: assign({
235
+ lastError: new Error('Message creation timeout (10s)'),
236
+ }),
237
+ },
238
+ },
239
+ },
240
+
241
+ created: {
242
+ on: {
243
+ SYNC_RESPONSE: 'synced',
244
+ SYNC_ERROR: 'error',
245
+ CREATE_ANOTHER: 'creating',
246
+ OFFLINE: 'offline',
247
+ },
248
+ },
249
+
250
+ loading: {
251
+ on: {
252
+ LOAD_SUCCESS: {
253
+ target: 'synced',
254
+ actions: assign({
255
+ syncData: (context, event: any) => event.data,
256
+ }),
257
+ },
258
+ LOAD_ERROR: 'error',
259
+ OFFLINE: 'offline',
260
+ },
261
+ after: {
262
+ 15000: { // 15 second timeout
263
+ target: 'error',
264
+ actions: assign({
265
+ lastError: new Error('Message load timeout (15s)'),
266
+ }),
267
+ },
268
+ },
269
+ },
270
+
271
+ synced: {
272
+ on: {
273
+ NEW_MESSAGE: 'creating',
274
+ LOAD_MORE: 'loading',
275
+ OFFLINE: 'offline',
276
+ },
277
+ },
278
+
279
+ error: {
280
+ on: {
281
+ RETRY: {
282
+ target: 'loading',
283
+ cond: (context) => context.retryCount < 3,
284
+ actions: assign({
285
+ retryCount: (context) => context.retryCount + 1,
286
+ }),
287
+ },
288
+ RESET: 'idle',
289
+ OFFLINE: 'offline',
290
+ },
291
+ },
292
+
293
+ offline: {
294
+ on: {
295
+ ONLINE: 'idle',
296
+ RESET: 'idle',
297
+ },
298
+ },
299
+ },
300
+ },
301
+ {
302
+ actions: {
303
+ logCreated: (context, event) => {
304
+ console.log(`[MessageSync] Message created: ${(event as any).messageId}`);
305
+ },
306
+ },
307
+ }
308
+ );
309
+
310
+ // ============================================================================
311
+ // CONVERSATION LIST STATE MACHINE
312
+ // ============================================================================
313
+
314
+ export const conversationListMachine = createMachine(
315
+ {
316
+ id: 'conversationList',
317
+ initial: 'uninitialized',
318
+ context: {
319
+ conversationId: undefined,
320
+ messageId: undefined,
321
+ lastError: undefined,
322
+ retryCount: 0,
323
+ syncData: {},
324
+ },
325
+ states: {
326
+ uninitialized: {
327
+ on: {
328
+ INITIALIZE: 'loading',
329
+ },
330
+ },
331
+
332
+ loading: {
333
+ on: {
334
+ LOAD_SUCCESS: {
335
+ target: 'ready',
336
+ actions: assign({
337
+ syncData: (context, event: any) => event.data,
338
+ retryCount: 0,
339
+ }),
340
+ },
341
+ LOAD_ERROR: {
342
+ target: 'error',
343
+ actions: assign({
344
+ lastError: (context, event: any) => event.error,
345
+ retryCount: (context) => context.retryCount + 1,
346
+ }),
347
+ },
348
+ },
349
+ after: {
350
+ 20000: {
351
+ target: 'error',
352
+ actions: assign({
353
+ lastError: new Error('Load timeout (20s)'),
354
+ }),
355
+ },
356
+ },
357
+ },
358
+
359
+ ready: {
360
+ on: {
361
+ REFRESH: 'loading',
362
+ CONVERSATION_ADDED: {
363
+ target: 'ready',
364
+ actions: assign({
365
+ syncData: (context, event: any) => ({
366
+ ...context.syncData,
367
+ conversations: [...(context.syncData?.conversations || []), event.conversation],
368
+ }),
369
+ }),
370
+ },
371
+ CONVERSATION_REMOVED: {
372
+ target: 'ready',
373
+ actions: assign({
374
+ syncData: (context, event: any) => ({
375
+ ...context.syncData,
376
+ conversations: (context.syncData?.conversations || []).filter(
377
+ (c: any) => c.id !== event.conversationId
378
+ ),
379
+ }),
380
+ }),
381
+ },
382
+ OFFLINE: 'offline',
383
+ },
384
+ },
385
+
386
+ error: {
387
+ on: {
388
+ RETRY: {
389
+ target: 'loading',
390
+ cond: (context) => context.retryCount < 3,
391
+ },
392
+ RESET: 'uninitialized',
393
+ OFFLINE: 'offline',
394
+ },
395
+ },
396
+
397
+ offline: {
398
+ on: {
399
+ ONLINE: 'ready',
400
+ RESET: 'uninitialized',
401
+ },
402
+ },
403
+ },
404
+ }
405
+ );
406
+
407
+ // ============================================================================
408
+ // OFFLINE QUEUE STATE MACHINE
409
+ // ============================================================================
410
+
411
+ export const offlineQueueMachine = createMachine(
412
+ {
413
+ id: 'offlineQueue',
414
+ initial: 'idle',
415
+ context: {
416
+ conversationId: undefined,
417
+ messageId: undefined,
418
+ lastError: undefined,
419
+ retryCount: 0,
420
+ syncData: {},
421
+ },
422
+ states: {
423
+ idle: {
424
+ on: {
425
+ QUEUE_OPERATION: {
426
+ target: 'queued',
427
+ actions: assign({
428
+ syncData: (context, event: any) => ({
429
+ ...context.syncData,
430
+ queue: [...(context.syncData?.queue || []), event.operation],
431
+ }),
432
+ }),
433
+ },
434
+ FLUSH: 'flushing',
435
+ },
436
+ },
437
+
438
+ queued: {
439
+ on: {
440
+ QUEUE_OPERATION: {
441
+ target: 'queued',
442
+ actions: assign({
443
+ syncData: (context, event: any) => ({
444
+ ...context.syncData,
445
+ queue: [...(context.syncData?.queue || []), event.operation],
446
+ }),
447
+ }),
448
+ },
449
+ FLUSH: 'flushing',
450
+ CLEAR: {
451
+ target: 'idle',
452
+ actions: assign({
453
+ syncData: (context) => ({
454
+ ...context.syncData,
455
+ queue: [],
456
+ }),
457
+ }),
458
+ },
459
+ },
460
+ },
461
+
462
+ flushing: {
463
+ on: {
464
+ FLUSH_SUCCESS: {
465
+ target: 'idle',
466
+ actions: assign({
467
+ syncData: (context) => ({
468
+ ...context.syncData,
469
+ queue: [],
470
+ }),
471
+ }),
472
+ },
473
+ FLUSH_ERROR: {
474
+ target: 'error',
475
+ actions: assign({
476
+ lastError: (context, event: any) => event.error,
477
+ retryCount: (context) => context.retryCount + 1,
478
+ }),
479
+ },
480
+ },
481
+ after: {
482
+ 30000: {
483
+ target: 'error',
484
+ actions: assign({
485
+ lastError: new Error('Flush timeout (30s)'),
486
+ }),
487
+ },
488
+ },
489
+ },
490
+
491
+ error: {
492
+ on: {
493
+ RETRY: {
494
+ target: 'flushing',
495
+ cond: (context) => context.retryCount < 5,
496
+ },
497
+ CLEAR: 'idle',
498
+ },
499
+ },
500
+ },
501
+ }
502
+ );
503
+
504
+ // ============================================================================
505
+ // CONFLICT RESOLUTION STATE MACHINE
506
+ // ============================================================================
507
+
508
+ export const conflictResolutionMachine = createMachine(
509
+ {
510
+ id: 'conflictResolution',
511
+ initial: 'idle',
512
+ context: {
513
+ conversationId: undefined,
514
+ messageId: undefined,
515
+ lastError: undefined,
516
+ retryCount: 0,
517
+ syncData: {},
518
+ },
519
+ states: {
520
+ idle: {
521
+ on: {
522
+ CONFLICT_DETECTED: 'resolving',
523
+ },
524
+ },
525
+
526
+ resolving: {
527
+ on: {
528
+ RESOLVE_SUCCESS: 'resolved',
529
+ RESOLVE_FAILED: 'error',
530
+ },
531
+ after: {
532
+ 5000: {
533
+ target: 'error',
534
+ actions: assign({
535
+ lastError: new Error('Conflict resolution timeout (5s)'),
536
+ }),
537
+ },
538
+ },
539
+ },
540
+
541
+ resolved: {
542
+ on: {
543
+ CONTINUE: 'idle',
544
+ },
545
+ },
546
+
547
+ error: {
548
+ on: {
549
+ RETRY: 'resolving',
550
+ MANUAL_RESOLVE: 'resolving',
551
+ ABORT: 'idle',
552
+ },
553
+ },
554
+ },
555
+ }
556
+ );
557
+
558
+ // ============================================================================
559
+ // STATE MACHINE SELECTORS & UTILITIES
560
+ // ============================================================================
561
+
562
+ export function getStateDescription(state: string): string {
563
+ const descriptions: Record<string, string> = {
564
+ idle: 'Waiting for input',
565
+ loading: 'Loading data from server',
566
+ syncing: 'Syncing data with server',
567
+ synced: 'Data is synchronized',
568
+ error: 'Error occurred, will retry',
569
+ offline: 'Offline mode - operations queued',
570
+ creating: 'Creating message',
571
+ created: 'Message created, waiting for sync',
572
+ ready: 'Ready for operations',
573
+ uninitialized: 'Not yet initialized',
574
+ queued: 'Operations queued offline',
575
+ flushing: 'Sending queued operations',
576
+ resolving: 'Resolving data conflicts',
577
+ resolved: 'Conflicts resolved',
578
+ reconciling: 'Reconciling local and remote changes',
579
+ };
580
+ return descriptions[state] || state;
581
+ }
582
+
583
+ export function isTerminalState(state: string): boolean {
584
+ return ['synced', 'resolved', 'ready'].includes(state);
585
+ }
586
+
587
+ export function isErrorState(state: string): boolean {
588
+ return state === 'error' || state === 'offline';
589
+ }
590
+
591
+ export function canRetry(state: string): boolean {
592
+ return state === 'error' || state === 'offline';
593
+ }