elsabro 2.3.0 → 3.7.0

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.
Files changed (67) hide show
  1. package/README.md +668 -20
  2. package/bin/install.js +0 -0
  3. package/flows/development-flow.json +452 -0
  4. package/flows/quick-flow.json +118 -0
  5. package/package.json +3 -2
  6. package/references/SYSTEM_INDEX.md +379 -5
  7. package/references/agent-marketplace.md +2274 -0
  8. package/references/agent-protocol.md +1126 -0
  9. package/references/ai-code-suggestions.md +2413 -0
  10. package/references/checkpointing.md +595 -0
  11. package/references/collaboration-patterns.md +851 -0
  12. package/references/collaborative-sessions.md +1081 -0
  13. package/references/configuration-management.md +1810 -0
  14. package/references/cost-tracking.md +1095 -0
  15. package/references/enterprise-sso.md +2001 -0
  16. package/references/error-contracts-v2.md +968 -0
  17. package/references/event-driven.md +1031 -0
  18. package/references/flow-orchestration.md +940 -0
  19. package/references/flow-visualization.md +1557 -0
  20. package/references/ide-integrations.md +3513 -0
  21. package/references/interrupt-system.md +681 -0
  22. package/references/kubernetes-deployment.md +3099 -0
  23. package/references/memory-system.md +683 -0
  24. package/references/mobile-companion.md +3236 -0
  25. package/references/multi-llm-providers.md +2494 -0
  26. package/references/multi-project-memory.md +1182 -0
  27. package/references/observability.md +793 -0
  28. package/references/output-schemas.md +858 -0
  29. package/references/performance-profiler.md +955 -0
  30. package/references/plugin-system.md +1526 -0
  31. package/references/prompt-management.md +292 -0
  32. package/references/sandbox-execution.md +303 -0
  33. package/references/security-system.md +1253 -0
  34. package/references/streaming.md +696 -0
  35. package/references/testing-framework.md +1151 -0
  36. package/references/time-travel.md +802 -0
  37. package/references/tool-registry.md +886 -0
  38. package/references/voice-commands.md +3296 -0
  39. package/templates/agent-marketplace-config.json +220 -0
  40. package/templates/agent-protocol-config.json +136 -0
  41. package/templates/ai-suggestions-config.json +100 -0
  42. package/templates/checkpoint-state.json +61 -0
  43. package/templates/collaboration-config.json +157 -0
  44. package/templates/collaborative-sessions-config.json +153 -0
  45. package/templates/configuration-config.json +245 -0
  46. package/templates/cost-tracking-config.json +148 -0
  47. package/templates/enterprise-sso-config.json +438 -0
  48. package/templates/events-config.json +148 -0
  49. package/templates/flow-visualization-config.json +196 -0
  50. package/templates/ide-integrations-config.json +442 -0
  51. package/templates/kubernetes-config.json +764 -0
  52. package/templates/memory-state.json +84 -0
  53. package/templates/mobile-companion-config.json +600 -0
  54. package/templates/multi-llm-config.json +544 -0
  55. package/templates/multi-project-memory-config.json +145 -0
  56. package/templates/observability-config.json +109 -0
  57. package/templates/performance-profiler-config.json +125 -0
  58. package/templates/plugin-config.json +170 -0
  59. package/templates/prompt-management-config.json +86 -0
  60. package/templates/sandbox-config.json +185 -0
  61. package/templates/schemas-config.json +65 -0
  62. package/templates/security-config.json +120 -0
  63. package/templates/streaming-config.json +72 -0
  64. package/templates/testing-config.json +81 -0
  65. package/templates/timetravel-config.json +62 -0
  66. package/templates/tool-registry-config.json +109 -0
  67. package/templates/voice-commands-config.json +658 -0
@@ -0,0 +1,1081 @@
1
+ # ELSABRO Collaborative Sessions System
2
+
3
+ > Sistema de sesiones colaborativas en tiempo real para múltiples usuarios y agentes.
4
+
5
+ ## Arquitectura General
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────────┐
9
+ │ Collaborative Sessions System │
10
+ ├─────────────────────────────────────────────────────────────────────────┤
11
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
12
+ │ │ SessionManager │ │ RealtimeSync │ │ConflictResolver │ │
13
+ │ │ ───────────── │ │ ───────────── │ │ ───────────── │ │
14
+ │ │ • Create/Join │ │ • WebSocket │ │ • OT/CRDT │ │
15
+ │ │ • Permissions │ │ • Broadcasting │ │ • Auto-merge │ │
16
+ │ │ • History │ │ • Reconnection │ │ • Manual resolve│ │
17
+ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
18
+ │ │ │
19
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
20
+ │ │ PresenceTracker │ │
21
+ │ │ • Online users • Cursors • Activity • Typing indicators │ │
22
+ │ └────────────────────────────────────────────────────────────┘ │
23
+ │ │ │
24
+ │ ┌───────────────────────────┴───────────────────────────────┐ │
25
+ │ │ CollaborativeState │ │
26
+ │ │ • Shared state • Atomic updates • Event sourcing │ │
27
+ │ └────────────────────────────────────────────────────────────┘ │
28
+ └─────────────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 1. SessionManager
34
+
35
+ ### Propósito
36
+ Gestiona el ciclo de vida de sesiones colaborativas.
37
+
38
+ ### Interfaz
39
+
40
+ ```typescript
41
+ interface CollaborativeSession {
42
+ id: string;
43
+ name: string;
44
+ type: 'flow' | 'planning' | 'review' | 'debug';
45
+ host: Participant;
46
+ participants: Participant[];
47
+ state: SessionState;
48
+ permissions: SessionPermissions;
49
+ settings: SessionSettings;
50
+ createdAt: string;
51
+ expiresAt?: string;
52
+ }
53
+
54
+ interface Participant {
55
+ id: string;
56
+ name: string;
57
+ type: 'user' | 'agent';
58
+ role: 'host' | 'editor' | 'viewer';
59
+ color: string; // For presence indicators
60
+ cursor?: CursorPosition;
61
+ status: 'online' | 'away' | 'offline';
62
+ joinedAt: string;
63
+ lastActivity: string;
64
+ }
65
+
66
+ interface SessionState {
67
+ version: number;
68
+ data: Record<string, unknown>;
69
+ locks: Map<string, string>; // path -> participantId
70
+ history: StateChange[];
71
+ }
72
+
73
+ interface SessionPermissions {
74
+ canEdit: string[]; // Role or participant IDs
75
+ canInvite: string[];
76
+ canKick: string[];
77
+ canLock: string[];
78
+ maxParticipants: number;
79
+ }
80
+
81
+ interface SessionSettings {
82
+ autoSave: boolean;
83
+ saveInterval: number;
84
+ conflictResolution: 'automatic' | 'manual' | 'last-write-wins';
85
+ allowAnonymous: boolean;
86
+ requireApproval: boolean;
87
+ idleTimeout: number;
88
+ }
89
+
90
+ interface SessionManager {
91
+ // Session lifecycle
92
+ create(config: CreateSessionConfig): Promise<CollaborativeSession>;
93
+ join(sessionId: string, participant: Omit<Participant, 'joinedAt' | 'lastActivity'>): Promise<CollaborativeSession>;
94
+ leave(sessionId: string, participantId: string): Promise<void>;
95
+ close(sessionId: string): Promise<void>;
96
+
97
+ // Session queries
98
+ get(sessionId: string): Promise<CollaborativeSession | null>;
99
+ list(filter?: SessionFilter): Promise<CollaborativeSession[]>;
100
+ findByParticipant(participantId: string): Promise<CollaborativeSession[]>;
101
+
102
+ // Invitations
103
+ invite(sessionId: string, email: string, role: Participant['role']): Promise<Invitation>;
104
+ acceptInvitation(invitationId: string, participant: Participant): Promise<CollaborativeSession>;
105
+ revokeInvitation(invitationId: string): Promise<void>;
106
+
107
+ // Permissions
108
+ updatePermissions(sessionId: string, permissions: Partial<SessionPermissions>): Promise<void>;
109
+ updateParticipantRole(sessionId: string, participantId: string, role: Participant['role']): Promise<void>;
110
+ kickParticipant(sessionId: string, participantId: string, reason?: string): Promise<void>;
111
+
112
+ // Events
113
+ on(event: SessionEvent, callback: Function): () => void;
114
+ }
115
+
116
+ type SessionEvent =
117
+ | 'session:created'
118
+ | 'session:closed'
119
+ | 'participant:joined'
120
+ | 'participant:left'
121
+ | 'participant:kicked'
122
+ | 'state:changed'
123
+ | 'conflict:detected'
124
+ | 'error';
125
+ ```
126
+
127
+ ### Implementación
128
+
129
+ ```typescript
130
+ class SessionManagerImpl implements SessionManager {
131
+ private sessions: Map<string, CollaborativeSession> = new Map();
132
+ private invitations: Map<string, Invitation> = new Map();
133
+ private eventEmitter: EventEmitter = new EventEmitter();
134
+
135
+ constructor(
136
+ private sync: RealtimeSync,
137
+ private presence: PresenceTracker,
138
+ private config: SessionManagerConfig
139
+ ) {}
140
+
141
+ async create(config: CreateSessionConfig): Promise<CollaborativeSession> {
142
+ const sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
143
+
144
+ const session: CollaborativeSession = {
145
+ id: sessionId,
146
+ name: config.name,
147
+ type: config.type,
148
+ host: config.host,
149
+ participants: [config.host],
150
+ state: {
151
+ version: 0,
152
+ data: config.initialData || {},
153
+ locks: new Map(),
154
+ history: []
155
+ },
156
+ permissions: config.permissions || this.getDefaultPermissions(),
157
+ settings: { ...this.config.defaultSettings, ...config.settings },
158
+ createdAt: new Date().toISOString()
159
+ };
160
+
161
+ if (config.expiresIn) {
162
+ session.expiresAt = new Date(Date.now() + config.expiresIn).toISOString();
163
+ }
164
+
165
+ this.sessions.set(sessionId, session);
166
+
167
+ // Initialize realtime sync
168
+ await this.sync.initSession(sessionId);
169
+
170
+ // Track host presence
171
+ this.presence.track(sessionId, config.host.id);
172
+
173
+ this.eventEmitter.emit('session:created', session);
174
+ return session;
175
+ }
176
+
177
+ async join(
178
+ sessionId: string,
179
+ participant: Omit<Participant, 'joinedAt' | 'lastActivity'>
180
+ ): Promise<CollaborativeSession> {
181
+ const session = this.sessions.get(sessionId);
182
+ if (!session) {
183
+ throw new Error(`Session not found: ${sessionId}`);
184
+ }
185
+
186
+ // Check max participants
187
+ if (session.participants.length >= session.permissions.maxParticipants) {
188
+ throw new Error('Session is full');
189
+ }
190
+
191
+ // Check if requires approval
192
+ if (session.settings.requireApproval) {
193
+ await this.requestApproval(session, participant);
194
+ }
195
+
196
+ const fullParticipant: Participant = {
197
+ ...participant,
198
+ color: this.assignColor(session),
199
+ status: 'online',
200
+ joinedAt: new Date().toISOString(),
201
+ lastActivity: new Date().toISOString()
202
+ };
203
+
204
+ session.participants.push(fullParticipant);
205
+
206
+ // Subscribe to realtime updates
207
+ await this.sync.subscribe(sessionId, participant.id);
208
+
209
+ // Track presence
210
+ this.presence.track(sessionId, participant.id);
211
+
212
+ this.eventEmitter.emit('participant:joined', {
213
+ session,
214
+ participant: fullParticipant
215
+ });
216
+
217
+ // Broadcast to other participants
218
+ await this.sync.broadcast(sessionId, {
219
+ type: 'participant:joined',
220
+ participant: fullParticipant
221
+ });
222
+
223
+ return session;
224
+ }
225
+
226
+ async leave(sessionId: string, participantId: string): Promise<void> {
227
+ const session = this.sessions.get(sessionId);
228
+ if (!session) return;
229
+
230
+ const participantIndex = session.participants.findIndex(p => p.id === participantId);
231
+ if (participantIndex === -1) return;
232
+
233
+ const participant = session.participants[participantIndex];
234
+ session.participants.splice(participantIndex, 1);
235
+
236
+ // Release any locks held by this participant
237
+ for (const [path, lockHolder] of session.state.locks) {
238
+ if (lockHolder === participantId) {
239
+ session.state.locks.delete(path);
240
+ }
241
+ }
242
+
243
+ // Unsubscribe from updates
244
+ await this.sync.unsubscribe(sessionId, participantId);
245
+
246
+ // Stop presence tracking
247
+ this.presence.untrack(sessionId, participantId);
248
+
249
+ this.eventEmitter.emit('participant:left', { session, participant });
250
+
251
+ // Broadcast to remaining participants
252
+ await this.sync.broadcast(sessionId, {
253
+ type: 'participant:left',
254
+ participantId
255
+ });
256
+
257
+ // If host left, transfer or close
258
+ if (participant.role === 'host') {
259
+ await this.handleHostLeave(session);
260
+ }
261
+ }
262
+
263
+ async close(sessionId: string): Promise<void> {
264
+ const session = this.sessions.get(sessionId);
265
+ if (!session) return;
266
+
267
+ // Notify all participants
268
+ await this.sync.broadcast(sessionId, {
269
+ type: 'session:closing',
270
+ reason: 'Host closed the session'
271
+ });
272
+
273
+ // Clean up all participants
274
+ for (const participant of session.participants) {
275
+ await this.leave(sessionId, participant.id);
276
+ }
277
+
278
+ // Destroy sync channel
279
+ await this.sync.destroySession(sessionId);
280
+
281
+ this.sessions.delete(sessionId);
282
+ this.eventEmitter.emit('session:closed', session);
283
+ }
284
+
285
+ private async handleHostLeave(session: CollaborativeSession): Promise<void> {
286
+ // Find next editor to promote
287
+ const newHost = session.participants.find(p => p.role === 'editor');
288
+
289
+ if (newHost) {
290
+ newHost.role = 'host';
291
+ session.host = newHost;
292
+
293
+ await this.sync.broadcast(session.id, {
294
+ type: 'host:transferred',
295
+ newHostId: newHost.id
296
+ });
297
+ } else {
298
+ // No editors, close session
299
+ await this.close(session.id);
300
+ }
301
+ }
302
+
303
+ private assignColor(session: CollaborativeSession): string {
304
+ const colors = [
305
+ '#ef4444', '#f97316', '#f59e0b', '#84cc16',
306
+ '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6',
307
+ '#6366f1', '#8b5cf6', '#a855f7', '#ec4899'
308
+ ];
309
+
310
+ const usedColors = new Set(session.participants.map(p => p.color));
311
+ return colors.find(c => !usedColors.has(c)) || colors[0];
312
+ }
313
+
314
+ private getDefaultPermissions(): SessionPermissions {
315
+ return {
316
+ canEdit: ['host', 'editor'],
317
+ canInvite: ['host'],
318
+ canKick: ['host'],
319
+ canLock: ['host', 'editor'],
320
+ maxParticipants: 10
321
+ };
322
+ }
323
+
324
+ on(event: SessionEvent, callback: Function): () => void {
325
+ this.eventEmitter.on(event, callback);
326
+ return () => this.eventEmitter.off(event, callback);
327
+ }
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## 2. RealtimeSync
334
+
335
+ ### Propósito
336
+ Sincronización en tiempo real usando WebSocket con reconexión automática.
337
+
338
+ ### Interfaz
339
+
340
+ ```typescript
341
+ interface SyncMessage {
342
+ type: string;
343
+ sessionId: string;
344
+ senderId: string;
345
+ timestamp: string;
346
+ payload: unknown;
347
+ version?: number;
348
+ }
349
+
350
+ interface RealtimeSync {
351
+ // Connection
352
+ connect(): Promise<void>;
353
+ disconnect(): Promise<void>;
354
+ isConnected(): boolean;
355
+
356
+ // Session management
357
+ initSession(sessionId: string): Promise<void>;
358
+ destroySession(sessionId: string): Promise<void>;
359
+ subscribe(sessionId: string, participantId: string): Promise<void>;
360
+ unsubscribe(sessionId: string, participantId: string): Promise<void>;
361
+
362
+ // Messaging
363
+ broadcast(sessionId: string, message: Omit<SyncMessage, 'sessionId' | 'timestamp'>): Promise<void>;
364
+ send(sessionId: string, targetId: string, message: Omit<SyncMessage, 'sessionId' | 'timestamp'>): Promise<void>;
365
+
366
+ // State sync
367
+ pushState(sessionId: string, path: string, value: unknown): Promise<void>;
368
+ pullState(sessionId: string): Promise<Record<string, unknown>>;
369
+
370
+ // Events
371
+ onMessage(callback: (message: SyncMessage) => void): () => void;
372
+ onConnectionChange(callback: (connected: boolean) => void): () => void;
373
+ }
374
+ ```
375
+
376
+ ### Implementación
377
+
378
+ ```typescript
379
+ class RealtimeSyncImpl implements RealtimeSync {
380
+ private ws: WebSocket | null = null;
381
+ private subscriptions: Map<string, Set<string>> = new Map();
382
+ private messageQueue: SyncMessage[] = [];
383
+ private reconnectAttempts: number = 0;
384
+ private messageCallbacks: Set<(message: SyncMessage) => void> = new Set();
385
+ private connectionCallbacks: Set<(connected: boolean) => void> = new Set();
386
+
387
+ constructor(private config: RealtimeSyncConfig) {}
388
+
389
+ async connect(): Promise<void> {
390
+ return new Promise((resolve, reject) => {
391
+ try {
392
+ this.ws = new WebSocket(this.config.serverUrl);
393
+
394
+ this.ws.onopen = () => {
395
+ this.reconnectAttempts = 0;
396
+ this.flushMessageQueue();
397
+ this.notifyConnectionChange(true);
398
+ resolve();
399
+ };
400
+
401
+ this.ws.onmessage = (event) => {
402
+ const message = JSON.parse(event.data) as SyncMessage;
403
+ this.handleMessage(message);
404
+ };
405
+
406
+ this.ws.onclose = () => {
407
+ this.notifyConnectionChange(false);
408
+ this.scheduleReconnect();
409
+ };
410
+
411
+ this.ws.onerror = (error) => {
412
+ console.error('WebSocket error:', error);
413
+ reject(error);
414
+ };
415
+ } catch (error) {
416
+ reject(error);
417
+ }
418
+ });
419
+ }
420
+
421
+ async disconnect(): Promise<void> {
422
+ if (this.ws) {
423
+ this.ws.close();
424
+ this.ws = null;
425
+ }
426
+ }
427
+
428
+ isConnected(): boolean {
429
+ return this.ws?.readyState === WebSocket.OPEN;
430
+ }
431
+
432
+ private scheduleReconnect(): void {
433
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
434
+ console.error('Max reconnection attempts reached');
435
+ return;
436
+ }
437
+
438
+ const delay = Math.min(
439
+ this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts),
440
+ this.config.reconnectMaxDelay
441
+ );
442
+
443
+ this.reconnectAttempts++;
444
+
445
+ setTimeout(() => {
446
+ this.connect().catch(console.error);
447
+ }, delay);
448
+ }
449
+
450
+ async broadcast(
451
+ sessionId: string,
452
+ message: Omit<SyncMessage, 'sessionId' | 'timestamp'>
453
+ ): Promise<void> {
454
+ const fullMessage: SyncMessage = {
455
+ ...message,
456
+ sessionId,
457
+ timestamp: new Date().toISOString()
458
+ };
459
+
460
+ if (this.isConnected()) {
461
+ this.ws!.send(JSON.stringify({
462
+ action: 'broadcast',
463
+ ...fullMessage
464
+ }));
465
+ } else {
466
+ this.messageQueue.push(fullMessage);
467
+ }
468
+ }
469
+
470
+ async send(
471
+ sessionId: string,
472
+ targetId: string,
473
+ message: Omit<SyncMessage, 'sessionId' | 'timestamp'>
474
+ ): Promise<void> {
475
+ const fullMessage: SyncMessage = {
476
+ ...message,
477
+ sessionId,
478
+ timestamp: new Date().toISOString()
479
+ };
480
+
481
+ if (this.isConnected()) {
482
+ this.ws!.send(JSON.stringify({
483
+ action: 'direct',
484
+ targetId,
485
+ ...fullMessage
486
+ }));
487
+ } else {
488
+ this.messageQueue.push(fullMessage);
489
+ }
490
+ }
491
+
492
+ async pushState(sessionId: string, path: string, value: unknown): Promise<void> {
493
+ await this.broadcast(sessionId, {
494
+ type: 'state:update',
495
+ senderId: 'system',
496
+ payload: { path, value }
497
+ });
498
+ }
499
+
500
+ async pullState(sessionId: string): Promise<Record<string, unknown>> {
501
+ return new Promise((resolve) => {
502
+ const callback = (message: SyncMessage) => {
503
+ if (message.type === 'state:full' && message.sessionId === sessionId) {
504
+ this.messageCallbacks.delete(callback);
505
+ resolve(message.payload as Record<string, unknown>);
506
+ }
507
+ };
508
+
509
+ this.messageCallbacks.add(callback);
510
+
511
+ this.ws?.send(JSON.stringify({
512
+ action: 'pull_state',
513
+ sessionId
514
+ }));
515
+
516
+ // Timeout fallback
517
+ setTimeout(() => {
518
+ this.messageCallbacks.delete(callback);
519
+ resolve({});
520
+ }, 5000);
521
+ });
522
+ }
523
+
524
+ private handleMessage(message: SyncMessage): void {
525
+ this.messageCallbacks.forEach(cb => cb(message));
526
+ }
527
+
528
+ private flushMessageQueue(): void {
529
+ while (this.messageQueue.length > 0 && this.isConnected()) {
530
+ const message = this.messageQueue.shift()!;
531
+ this.ws!.send(JSON.stringify({
532
+ action: 'broadcast',
533
+ ...message
534
+ }));
535
+ }
536
+ }
537
+
538
+ private notifyConnectionChange(connected: boolean): void {
539
+ this.connectionCallbacks.forEach(cb => cb(connected));
540
+ }
541
+
542
+ onMessage(callback: (message: SyncMessage) => void): () => void {
543
+ this.messageCallbacks.add(callback);
544
+ return () => this.messageCallbacks.delete(callback);
545
+ }
546
+
547
+ onConnectionChange(callback: (connected: boolean) => void): () => void {
548
+ this.connectionCallbacks.add(callback);
549
+ return () => this.connectionCallbacks.delete(callback);
550
+ }
551
+ }
552
+ ```
553
+
554
+ ---
555
+
556
+ ## 3. ConflictResolver
557
+
558
+ ### Propósito
559
+ Resuelve conflictos de edición concurrente usando OT (Operational Transformation) o CRDT.
560
+
561
+ ### Interfaz
562
+
563
+ ```typescript
564
+ interface Operation {
565
+ id: string;
566
+ type: 'insert' | 'delete' | 'update' | 'move';
567
+ path: string;
568
+ value?: unknown;
569
+ previousValue?: unknown;
570
+ position?: number;
571
+ length?: number;
572
+ participantId: string;
573
+ timestamp: string;
574
+ version: number;
575
+ }
576
+
577
+ interface Conflict {
578
+ id: string;
579
+ operations: Operation[];
580
+ detectedAt: string;
581
+ status: 'pending' | 'auto-resolved' | 'manual-resolved' | 'rejected';
582
+ resolution?: ConflictResolution;
583
+ }
584
+
585
+ interface ConflictResolution {
586
+ strategy: 'accept-first' | 'accept-last' | 'merge' | 'custom';
587
+ resultingOperation: Operation;
588
+ resolvedBy?: string;
589
+ resolvedAt: string;
590
+ }
591
+
592
+ interface ConflictResolver {
593
+ // Detection
594
+ detectConflict(op1: Operation, op2: Operation): boolean;
595
+ getConflicts(sessionId: string): Conflict[];
596
+
597
+ // Resolution
598
+ resolve(conflictId: string, resolution: ConflictResolution): Promise<void>;
599
+ autoResolve(conflict: Conflict): Promise<ConflictResolution>;
600
+ rejectOperation(operationId: string): Promise<void>;
601
+
602
+ // Transformation (OT)
603
+ transform(op1: Operation, op2: Operation): [Operation, Operation];
604
+ transformAgainst(op: Operation, history: Operation[]): Operation;
605
+
606
+ // Events
607
+ onConflict(callback: (conflict: Conflict) => void): () => void;
608
+ }
609
+ ```
610
+
611
+ ### Implementación
612
+
613
+ ```typescript
614
+ class ConflictResolverImpl implements ConflictResolver {
615
+ private conflicts: Map<string, Conflict> = new Map();
616
+ private conflictCallbacks: Set<(conflict: Conflict) => void> = new Set();
617
+
618
+ constructor(private settings: ConflictResolverSettings) {}
619
+
620
+ detectConflict(op1: Operation, op2: Operation): boolean {
621
+ // Same path
622
+ if (op1.path !== op2.path) return false;
623
+
624
+ // Same version (concurrent)
625
+ if (op1.version !== op2.version) return false;
626
+
627
+ // Different participants
628
+ if (op1.participantId === op2.participantId) return false;
629
+
630
+ // Conflicting operation types
631
+ const conflictingPairs = [
632
+ ['update', 'update'],
633
+ ['update', 'delete'],
634
+ ['delete', 'delete'],
635
+ ['insert', 'insert'] // at same position
636
+ ];
637
+
638
+ const pair = [op1.type, op2.type].sort();
639
+ return conflictingPairs.some(cp => cp[0] === pair[0] && cp[1] === pair[1]);
640
+ }
641
+
642
+ async autoResolve(conflict: Conflict): Promise<ConflictResolution> {
643
+ const [op1, op2] = conflict.operations;
644
+
645
+ switch (this.settings.defaultStrategy) {
646
+ case 'last-write-wins':
647
+ // Use timestamp to determine winner
648
+ const winner = op1.timestamp > op2.timestamp ? op1 : op2;
649
+ return {
650
+ strategy: 'accept-last',
651
+ resultingOperation: winner,
652
+ resolvedAt: new Date().toISOString()
653
+ };
654
+
655
+ case 'first-write-wins':
656
+ const first = op1.timestamp < op2.timestamp ? op1 : op2;
657
+ return {
658
+ strategy: 'accept-first',
659
+ resultingOperation: first,
660
+ resolvedAt: new Date().toISOString()
661
+ };
662
+
663
+ case 'merge':
664
+ // Try to merge operations
665
+ const merged = this.mergeOperations(op1, op2);
666
+ return {
667
+ strategy: 'merge',
668
+ resultingOperation: merged,
669
+ resolvedAt: new Date().toISOString()
670
+ };
671
+
672
+ default:
673
+ // Use OT to transform
674
+ const [transformed1, transformed2] = this.transform(op1, op2);
675
+ return {
676
+ strategy: 'custom',
677
+ resultingOperation: transformed1,
678
+ resolvedAt: new Date().toISOString()
679
+ };
680
+ }
681
+ }
682
+
683
+ transform(op1: Operation, op2: Operation): [Operation, Operation] {
684
+ // Operational Transformation for concurrent operations
685
+
686
+ if (op1.type === 'insert' && op2.type === 'insert') {
687
+ // Both inserts - adjust positions
688
+ if (op1.position! <= op2.position!) {
689
+ return [
690
+ op1,
691
+ { ...op2, position: op2.position! + (op1.value as string).length }
692
+ ];
693
+ } else {
694
+ return [
695
+ { ...op1, position: op1.position! + (op2.value as string).length },
696
+ op2
697
+ ];
698
+ }
699
+ }
700
+
701
+ if (op1.type === 'delete' && op2.type === 'insert') {
702
+ if (op2.position! <= op1.position!) {
703
+ return [
704
+ { ...op1, position: op1.position! + (op2.value as string).length },
705
+ op2
706
+ ];
707
+ } else if (op2.position! >= op1.position! + op1.length!) {
708
+ return [op1, { ...op2, position: op2.position! - op1.length! }];
709
+ } else {
710
+ // Insert is within delete range - complex case
711
+ return [
712
+ { ...op1, length: op1.length! + (op2.value as string).length },
713
+ { ...op2, position: op1.position! }
714
+ ];
715
+ }
716
+ }
717
+
718
+ if (op1.type === 'update' && op2.type === 'update') {
719
+ // Both updating same path - last write wins by default
720
+ const winner = op1.timestamp > op2.timestamp ? op1 : op2;
721
+ const loser = winner === op1 ? op2 : op1;
722
+ return [winner, { ...loser, type: 'update', value: winner.value }];
723
+ }
724
+
725
+ // Default: return as-is
726
+ return [op1, op2];
727
+ }
728
+
729
+ transformAgainst(op: Operation, history: Operation[]): Operation {
730
+ let transformed = op;
731
+
732
+ for (const historicalOp of history) {
733
+ if (historicalOp.version > op.version) continue;
734
+ if (historicalOp.id === op.id) continue;
735
+
736
+ if (this.detectConflict(transformed, historicalOp)) {
737
+ const [newOp] = this.transform(transformed, historicalOp);
738
+ transformed = newOp;
739
+ }
740
+ }
741
+
742
+ return transformed;
743
+ }
744
+
745
+ private mergeOperations(op1: Operation, op2: Operation): Operation {
746
+ if (op1.type === 'update' && op2.type === 'update') {
747
+ // Merge object updates
748
+ if (typeof op1.value === 'object' && typeof op2.value === 'object') {
749
+ return {
750
+ ...op1,
751
+ value: { ...op1.value as object, ...op2.value as object },
752
+ timestamp: new Date().toISOString()
753
+ };
754
+ }
755
+
756
+ // Merge string updates (concatenate)
757
+ if (typeof op1.value === 'string' && typeof op2.value === 'string') {
758
+ return {
759
+ ...op1,
760
+ value: `${op1.value}\n---\n${op2.value}`,
761
+ timestamp: new Date().toISOString()
762
+ };
763
+ }
764
+ }
765
+
766
+ // Default: use last write
767
+ return op1.timestamp > op2.timestamp ? op1 : op2;
768
+ }
769
+
770
+ onConflict(callback: (conflict: Conflict) => void): () => void {
771
+ this.conflictCallbacks.add(callback);
772
+ return () => this.conflictCallbacks.delete(callback);
773
+ }
774
+ }
775
+ ```
776
+
777
+ ---
778
+
779
+ ## 4. PresenceTracker
780
+
781
+ ### Propósito
782
+ Rastrea la presencia de participantes incluyendo cursores, actividad y estado.
783
+
784
+ ### Interfaz
785
+
786
+ ```typescript
787
+ interface PresenceState {
788
+ participantId: string;
789
+ sessionId: string;
790
+ status: 'online' | 'away' | 'busy' | 'offline';
791
+ cursor?: CursorPosition;
792
+ selection?: SelectionRange;
793
+ activity?: ActivityInfo;
794
+ lastSeen: string;
795
+ }
796
+
797
+ interface CursorPosition {
798
+ path: string; // e.g., "nodes[0].config"
799
+ line?: number;
800
+ column?: number;
801
+ label?: string; // What they're editing
802
+ }
803
+
804
+ interface SelectionRange {
805
+ start: CursorPosition;
806
+ end: CursorPosition;
807
+ }
808
+
809
+ interface ActivityInfo {
810
+ type: 'editing' | 'viewing' | 'typing' | 'idle';
811
+ target?: string;
812
+ startedAt: string;
813
+ }
814
+
815
+ interface PresenceTracker {
816
+ // Tracking
817
+ track(sessionId: string, participantId: string): void;
818
+ untrack(sessionId: string, participantId: string): void;
819
+
820
+ // Updates
821
+ updateStatus(sessionId: string, participantId: string, status: PresenceState['status']): void;
822
+ updateCursor(sessionId: string, participantId: string, cursor: CursorPosition): void;
823
+ updateSelection(sessionId: string, participantId: string, selection: SelectionRange): void;
824
+ updateActivity(sessionId: string, participantId: string, activity: ActivityInfo): void;
825
+
826
+ // Queries
827
+ getPresence(sessionId: string, participantId: string): PresenceState | null;
828
+ getAllPresence(sessionId: string): PresenceState[];
829
+ getOnlineParticipants(sessionId: string): string[];
830
+
831
+ // Events
832
+ onPresenceChange(sessionId: string, callback: (presence: PresenceState) => void): () => void;
833
+ }
834
+ ```
835
+
836
+ ### Implementación
837
+
838
+ ```typescript
839
+ class PresenceTrackerImpl implements PresenceTracker {
840
+ private presence: Map<string, Map<string, PresenceState>> = new Map();
841
+ private idleTimers: Map<string, NodeJS.Timeout> = new Map();
842
+ private callbacks: Map<string, Set<(presence: PresenceState) => void>> = new Map();
843
+
844
+ constructor(
845
+ private sync: RealtimeSync,
846
+ private config: PresenceConfig
847
+ ) {
848
+ this.setupSyncListener();
849
+ }
850
+
851
+ private setupSyncListener(): void {
852
+ this.sync.onMessage((message) => {
853
+ if (message.type.startsWith('presence:')) {
854
+ this.handlePresenceMessage(message);
855
+ }
856
+ });
857
+ }
858
+
859
+ track(sessionId: string, participantId: string): void {
860
+ if (!this.presence.has(sessionId)) {
861
+ this.presence.set(sessionId, new Map());
862
+ }
863
+
864
+ const state: PresenceState = {
865
+ participantId,
866
+ sessionId,
867
+ status: 'online',
868
+ lastSeen: new Date().toISOString()
869
+ };
870
+
871
+ this.presence.get(sessionId)!.set(participantId, state);
872
+ this.broadcastPresence(sessionId, state);
873
+ this.setupIdleDetection(sessionId, participantId);
874
+ }
875
+
876
+ untrack(sessionId: string, participantId: string): void {
877
+ const sessionPresence = this.presence.get(sessionId);
878
+ if (!sessionPresence) return;
879
+
880
+ const state = sessionPresence.get(participantId);
881
+ if (state) {
882
+ state.status = 'offline';
883
+ this.broadcastPresence(sessionId, state);
884
+ }
885
+
886
+ sessionPresence.delete(participantId);
887
+ this.clearIdleTimer(sessionId, participantId);
888
+ }
889
+
890
+ updateCursor(
891
+ sessionId: string,
892
+ participantId: string,
893
+ cursor: CursorPosition
894
+ ): void {
895
+ const state = this.getPresence(sessionId, participantId);
896
+ if (!state) return;
897
+
898
+ state.cursor = cursor;
899
+ state.lastSeen = new Date().toISOString();
900
+
901
+ this.broadcastPresence(sessionId, state);
902
+ this.resetIdleTimer(sessionId, participantId);
903
+ }
904
+
905
+ updateActivity(
906
+ sessionId: string,
907
+ participantId: string,
908
+ activity: ActivityInfo
909
+ ): void {
910
+ const state = this.getPresence(sessionId, participantId);
911
+ if (!state) return;
912
+
913
+ state.activity = activity;
914
+ state.lastSeen = new Date().toISOString();
915
+
916
+ // Auto-update status based on activity
917
+ if (activity.type === 'typing' || activity.type === 'editing') {
918
+ state.status = 'busy';
919
+ } else if (activity.type === 'idle') {
920
+ state.status = 'away';
921
+ } else {
922
+ state.status = 'online';
923
+ }
924
+
925
+ this.broadcastPresence(sessionId, state);
926
+ }
927
+
928
+ getPresence(sessionId: string, participantId: string): PresenceState | null {
929
+ return this.presence.get(sessionId)?.get(participantId) || null;
930
+ }
931
+
932
+ getAllPresence(sessionId: string): PresenceState[] {
933
+ const sessionPresence = this.presence.get(sessionId);
934
+ return sessionPresence ? Array.from(sessionPresence.values()) : [];
935
+ }
936
+
937
+ getOnlineParticipants(sessionId: string): string[] {
938
+ return this.getAllPresence(sessionId)
939
+ .filter(p => p.status !== 'offline')
940
+ .map(p => p.participantId);
941
+ }
942
+
943
+ private broadcastPresence(sessionId: string, state: PresenceState): void {
944
+ this.sync.broadcast(sessionId, {
945
+ type: 'presence:update',
946
+ senderId: state.participantId,
947
+ payload: state
948
+ });
949
+
950
+ this.notifyCallbacks(sessionId, state);
951
+ }
952
+
953
+ private setupIdleDetection(sessionId: string, participantId: string): void {
954
+ const key = `${sessionId}:${participantId}`;
955
+
956
+ const timer = setTimeout(() => {
957
+ this.updateActivity(sessionId, participantId, {
958
+ type: 'idle',
959
+ startedAt: new Date().toISOString()
960
+ });
961
+ }, this.config.idleTimeout);
962
+
963
+ this.idleTimers.set(key, timer);
964
+ }
965
+
966
+ private resetIdleTimer(sessionId: string, participantId: string): void {
967
+ this.clearIdleTimer(sessionId, participantId);
968
+ this.setupIdleDetection(sessionId, participantId);
969
+ }
970
+
971
+ private clearIdleTimer(sessionId: string, participantId: string): void {
972
+ const key = `${sessionId}:${participantId}`;
973
+ const timer = this.idleTimers.get(key);
974
+ if (timer) {
975
+ clearTimeout(timer);
976
+ this.idleTimers.delete(key);
977
+ }
978
+ }
979
+
980
+ onPresenceChange(
981
+ sessionId: string,
982
+ callback: (presence: PresenceState) => void
983
+ ): () => void {
984
+ if (!this.callbacks.has(sessionId)) {
985
+ this.callbacks.set(sessionId, new Set());
986
+ }
987
+ this.callbacks.get(sessionId)!.add(callback);
988
+
989
+ return () => {
990
+ this.callbacks.get(sessionId)?.delete(callback);
991
+ };
992
+ }
993
+
994
+ private notifyCallbacks(sessionId: string, state: PresenceState): void {
995
+ this.callbacks.get(sessionId)?.forEach(cb => cb(state));
996
+ }
997
+
998
+ private handlePresenceMessage(message: SyncMessage): void {
999
+ if (message.type === 'presence:update') {
1000
+ const state = message.payload as PresenceState;
1001
+ const sessionPresence = this.presence.get(message.sessionId);
1002
+
1003
+ if (sessionPresence) {
1004
+ sessionPresence.set(state.participantId, state);
1005
+ this.notifyCallbacks(message.sessionId, state);
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## 5. Visualización de Presencia
1015
+
1016
+ ```
1017
+ ╔══════════════════════════════════════════════════════════════════════════════╗
1018
+ ║ Collaborative Session: Flow Design ║
1019
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1020
+ ║ Session ID: session_abc123 │ Participants: 4 │ Active: 3 ║
1021
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1022
+ ║ Participants ║
1023
+ ├──────────────────────────────────────────────────────────────────────────────┤
1024
+ │ ● Alice (Host) 🟢 online Editing: nodes[2].config │
1025
+ │ ● Bob 🟢 online Viewing: flow overview │
1026
+ │ ● Agent-Explore 🟡 busy Executing: exploration task │
1027
+ │ ○ Carol ⚪ away Idle for 5 minutes │
1028
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1029
+ ║ Activity Feed ║
1030
+ ├──────────────────────────────────────────────────────────────────────────────┤
1031
+ │ [10:32:15] Alice moved node "Analyze" to position (200, 150) │
1032
+ │ [10:32:08] Bob added comment on "Review" node │
1033
+ │ [10:31:45] Agent-Explore updated node "Explore" configuration │
1034
+ │ [10:31:30] Alice connected "Analyze" → "Implement" │
1035
+ ╠══════════════════════════════════════════════════════════════════════════════╣
1036
+ ║ Cursors (Canvas) ║
1037
+ ├──────────────────────────────────────────────────────────────────────────────┤
1038
+ │ │
1039
+ │ ┌─────────────┐ 🔴Alice │
1040
+ │ │ Analyze │ ↓ │
1041
+ │ └─────────────┘ ┌─────────────┐ │
1042
+ │ │ Implement │←🔵Bob │
1043
+ │ └─────────────┘ │
1044
+ │ │
1045
+ ╚══════════════════════════════════════════════════════════════════════════════╝
1046
+ ```
1047
+
1048
+ ---
1049
+
1050
+ ## 6. Comandos CLI
1051
+
1052
+ ```bash
1053
+ # Crear sesión colaborativa
1054
+ /elsabro:collab create "Flow Design Session" --type flow
1055
+
1056
+ # Unirse a sesión
1057
+ /elsabro:collab join <session-id>
1058
+
1059
+ # Invitar participante
1060
+ /elsabro:collab invite <email> --role editor
1061
+
1062
+ # Ver participantes
1063
+ /elsabro:collab participants
1064
+
1065
+ # Ver actividad
1066
+ /elsabro:collab activity
1067
+
1068
+ # Salir de sesión
1069
+ /elsabro:collab leave
1070
+
1071
+ # Cerrar sesión (solo host)
1072
+ /elsabro:collab close
1073
+ ```
1074
+
1075
+ ---
1076
+
1077
+ ## Referencias
1078
+
1079
+ - **REF-019**: Event-Driven Architecture
1080
+ - **REF-028**: Flow Visualization
1081
+ - **REF-030**: Esta referencia (Collaborative Sessions)