claude-mpm 3.4.10__py3-none-any.whl → 3.4.14__py3-none-any.whl

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 (29) hide show
  1. claude_mpm/cli/commands/run.py +10 -10
  2. claude_mpm/dashboard/index.html +13 -0
  3. claude_mpm/dashboard/static/css/dashboard.css +2722 -0
  4. claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
  5. claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
  6. claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
  7. claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
  8. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
  9. claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
  10. claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
  11. claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
  12. claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
  13. claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
  14. claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
  15. claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
  16. claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
  17. claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
  18. claude_mpm/dashboard/static/js/dashboard.js +1978 -0
  19. claude_mpm/dashboard/static/js/socket-client.js +537 -0
  20. claude_mpm/dashboard/templates/index.html +346 -0
  21. claude_mpm/dashboard/test_dashboard.html +372 -0
  22. claude_mpm/scripts/socketio_daemon.py +51 -6
  23. claude_mpm/services/socketio_server.py +41 -5
  24. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/METADATA +2 -1
  25. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/RECORD +29 -9
  26. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/WHEEL +0 -0
  27. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/entry_points.txt +0 -0
  28. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/licenses/LICENSE +0 -0
  29. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,611 @@
1
+ /**
2
+ * File and Tool Tracker Module
3
+ *
4
+ * Tracks file operations and tool calls by pairing pre/post events and maintaining
5
+ * organized collections for the files and tools tabs. Provides analysis of
6
+ * tool execution patterns and file operation history.
7
+ *
8
+ * WHY: Extracted from main dashboard to isolate complex event pairing logic
9
+ * that groups related events into meaningful operations. This provides better
10
+ * maintainability for the intricate logic of matching tool events with their results.
11
+ *
12
+ * DESIGN DECISION: Uses intelligent correlation strategy for tool calls that:
13
+ * - Separates pre_tool and post_tool events first
14
+ * - Correlates based on temporal proximity, parameter similarity, and context
15
+ * - Handles timing differences between pre/post events (tools can run for minutes)
16
+ * - Prevents duplicate tool entries by ensuring each tool call appears once
17
+ * - Supports both paired and orphaned events for comprehensive tracking
18
+ */
19
+ class FileToolTracker {
20
+ constructor(agentInference, workingDirectoryManager) {
21
+ this.agentInference = agentInference;
22
+ this.workingDirectoryManager = workingDirectoryManager;
23
+
24
+ // File tracking for files tab
25
+ this.fileOperations = new Map(); // Map of file paths to operations
26
+
27
+ // Tool call tracking for tools tab
28
+ this.toolCalls = new Map(); // Map of tool call keys to paired pre/post events
29
+
30
+ console.log('File-tool tracker initialized');
31
+ }
32
+
33
+ /**
34
+ * Update file operations from events
35
+ * @param {Array} events - Events to process
36
+ */
37
+ updateFileOperations(events) {
38
+ // Clear existing data
39
+ this.fileOperations.clear();
40
+
41
+ console.log('updateFileOperations - processing', events.length, 'events');
42
+
43
+ // Group events by session and timestamp to match pre/post pairs
44
+ const eventPairs = new Map(); // Key: session_id + timestamp + tool_name
45
+ let fileOperationCount = 0;
46
+
47
+ // First pass: collect all tool events and group them
48
+ events.forEach((event, index) => {
49
+ const isFileOp = this.isFileOperation(event);
50
+ if (isFileOp) fileOperationCount++;
51
+
52
+ if (index < 5) { // Debug first 5 events with more detail
53
+ console.log(`Event ${index}:`, {
54
+ type: event.type,
55
+ subtype: event.subtype,
56
+ tool_name: event.tool_name,
57
+ tool_parameters: event.tool_parameters,
58
+ isFileOp: isFileOp
59
+ });
60
+ }
61
+
62
+ if (isFileOp) {
63
+ const toolName = event.tool_name;
64
+ const sessionId = event.session_id || 'unknown';
65
+ const eventKey = `${sessionId}_${toolName}_${Math.floor(new Date(event.timestamp).getTime() / 1000)}`; // Group by second
66
+
67
+ if (!eventPairs.has(eventKey)) {
68
+ eventPairs.set(eventKey, {
69
+ pre_event: null,
70
+ post_event: null,
71
+ tool_name: toolName,
72
+ session_id: sessionId
73
+ });
74
+ }
75
+
76
+ const pair = eventPairs.get(eventKey);
77
+ if (event.subtype === 'pre_tool' || event.type === 'hook' && !event.subtype.includes('post')) {
78
+ pair.pre_event = event;
79
+ } else if (event.subtype === 'post_tool' || event.subtype.includes('post')) {
80
+ pair.post_event = event;
81
+ } else {
82
+ // For events without clear pre/post distinction, treat as both
83
+ pair.pre_event = event;
84
+ pair.post_event = event;
85
+ }
86
+ }
87
+ });
88
+
89
+ console.log('updateFileOperations - found', fileOperationCount, 'file operations in', eventPairs.size, 'event pairs');
90
+
91
+ // Second pass: extract file paths and operations from paired events
92
+ eventPairs.forEach((pair, key) => {
93
+ const filePath = this.extractFilePathFromPair(pair);
94
+
95
+ if (filePath) {
96
+ console.log('File operation detected for:', filePath, 'from pair:', key);
97
+
98
+ if (!this.fileOperations.has(filePath)) {
99
+ this.fileOperations.set(filePath, {
100
+ path: filePath,
101
+ operations: [],
102
+ lastOperation: null
103
+ });
104
+ }
105
+
106
+ const fileData = this.fileOperations.get(filePath);
107
+ const operation = this.getFileOperationFromPair(pair);
108
+ const timestamp = pair.post_event?.timestamp || pair.pre_event?.timestamp;
109
+
110
+ const agentInfo = this.extractAgentFromPair(pair);
111
+ const workingDirectory = this.workingDirectoryManager.extractWorkingDirectoryFromPair(pair);
112
+
113
+ fileData.operations.push({
114
+ operation: operation,
115
+ timestamp: timestamp,
116
+ agent: agentInfo.name,
117
+ confidence: agentInfo.confidence,
118
+ sessionId: pair.session_id,
119
+ details: this.getFileOperationDetailsFromPair(pair),
120
+ workingDirectory: workingDirectory
121
+ });
122
+ fileData.lastOperation = timestamp;
123
+ } else {
124
+ console.log('No file path found for pair:', key, pair);
125
+ }
126
+ });
127
+
128
+ console.log('updateFileOperations - final result:', this.fileOperations.size, 'file operations');
129
+ if (this.fileOperations.size > 0) {
130
+ console.log('File operations map:', Array.from(this.fileOperations.entries()));
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Update tool calls from events - pairs pre/post tool events into complete tool calls
136
+ * @param {Array} events - Events to process
137
+ */
138
+ updateToolCalls(events) {
139
+ // Clear existing data
140
+ this.toolCalls.clear();
141
+
142
+ console.log('updateToolCalls - processing', events.length, 'events');
143
+
144
+ // Improved correlation strategy: collect events first, then correlate intelligently
145
+ const preToolEvents = [];
146
+ const postToolEvents = [];
147
+ let toolOperationCount = 0;
148
+
149
+ // First pass: separate pre_tool and post_tool events
150
+ events.forEach((event, index) => {
151
+ const isToolOp = this.isToolOperation(event);
152
+ if (isToolOp) toolOperationCount++;
153
+
154
+ if (index < 5) { // Debug first 5 events with more detail
155
+ console.log(`Tool Event ${index}:`, {
156
+ type: event.type,
157
+ subtype: event.subtype,
158
+ tool_name: event.tool_name,
159
+ tool_parameters: event.tool_parameters,
160
+ isToolOp: isToolOp
161
+ });
162
+ }
163
+
164
+ if (isToolOp) {
165
+ if (event.subtype === 'pre_tool' || (event.type === 'hook' && !event.subtype.includes('post'))) {
166
+ preToolEvents.push(event);
167
+ } else if (event.subtype === 'post_tool' || event.subtype.includes('post')) {
168
+ postToolEvents.push(event);
169
+ } else {
170
+ // For events without clear pre/post distinction, treat as standalone
171
+ preToolEvents.push(event);
172
+ postToolEvents.push(event);
173
+ }
174
+ }
175
+ });
176
+
177
+ console.log('updateToolCalls - found', toolOperationCount, 'tool operations:', preToolEvents.length, 'pre_tool,', postToolEvents.length, 'post_tool');
178
+
179
+ // Second pass: correlate pre_tool events with post_tool events
180
+ const toolCallPairs = new Map();
181
+ const usedPostEvents = new Set();
182
+
183
+ preToolEvents.forEach((preEvent, preIndex) => {
184
+ const toolName = preEvent.tool_name;
185
+ const sessionId = preEvent.session_id || 'unknown';
186
+ const preTimestamp = new Date(preEvent.timestamp).getTime();
187
+
188
+ // Create a base pair for this pre_tool event
189
+ const pairKey = `${sessionId}_${toolName}_${preIndex}_${preTimestamp}`;
190
+ const pair = {
191
+ pre_event: preEvent,
192
+ post_event: null,
193
+ tool_name: toolName,
194
+ session_id: sessionId,
195
+ operation_type: preEvent.operation_type || 'tool_execution',
196
+ timestamp: preEvent.timestamp,
197
+ duration_ms: null,
198
+ success: null,
199
+ exit_code: null,
200
+ result_summary: null,
201
+ agent_type: null,
202
+ agent_confidence: null
203
+ };
204
+
205
+ // Get agent info from pre_event
206
+ const agentInfo = this.extractAgentFromEvent(preEvent);
207
+ pair.agent_type = agentInfo.name;
208
+ pair.agent_confidence = agentInfo.confidence;
209
+
210
+ // Try to find matching post_tool event
211
+ let bestMatchIndex = -1;
212
+ let bestMatchScore = -1;
213
+ const maxTimeDiffMs = 300000; // 5 minutes max time difference
214
+
215
+ postToolEvents.forEach((postEvent, postIndex) => {
216
+ // Skip already used post events
217
+ if (usedPostEvents.has(postIndex)) return;
218
+
219
+ // Must match tool name and session
220
+ if (postEvent.tool_name !== toolName || postEvent.session_id !== sessionId) return;
221
+
222
+ const postTimestamp = new Date(postEvent.timestamp).getTime();
223
+ const timeDiff = Math.abs(postTimestamp - preTimestamp);
224
+
225
+ // Post event should generally come after pre event (or very close)
226
+ const isTemporallyValid = postTimestamp >= preTimestamp - 1000; // Allow 1s clock skew
227
+
228
+ // Calculate correlation score (higher is better)
229
+ let score = 0;
230
+ if (isTemporallyValid && timeDiff <= maxTimeDiffMs) {
231
+ score = 1000 - (timeDiff / 1000); // Prefer closer timestamps
232
+
233
+ // Boost score for parameter similarity (if available)
234
+ if (this.compareToolParameters(preEvent, postEvent)) {
235
+ score += 500;
236
+ }
237
+
238
+ // Boost score for same working directory
239
+ if (preEvent.working_directory && postEvent.working_directory &&
240
+ preEvent.working_directory === postEvent.working_directory) {
241
+ score += 100;
242
+ }
243
+ }
244
+
245
+ if (score > bestMatchScore) {
246
+ bestMatchScore = score;
247
+ bestMatchIndex = postIndex;
248
+ }
249
+ });
250
+
251
+ // If we found a good match, pair them
252
+ if (bestMatchIndex >= 0 && bestMatchScore > 0) {
253
+ const postEvent = postToolEvents[bestMatchIndex];
254
+ pair.post_event = postEvent;
255
+ pair.duration_ms = postEvent.duration_ms;
256
+ pair.success = postEvent.success;
257
+ pair.exit_code = postEvent.exit_code;
258
+ pair.result_summary = postEvent.result_summary;
259
+
260
+ usedPostEvents.add(bestMatchIndex);
261
+ console.log(`Paired pre_tool ${toolName} at ${preEvent.timestamp} with post_tool at ${postEvent.timestamp} (score: ${bestMatchScore})`);
262
+ } else {
263
+ console.log(`No matching post_tool found for ${toolName} at ${preEvent.timestamp} (still running or orphaned)`);
264
+ }
265
+
266
+ toolCallPairs.set(pairKey, pair);
267
+ });
268
+
269
+ // Third pass: handle any orphaned post_tool events (shouldn't happen but be safe)
270
+ postToolEvents.forEach((postEvent, postIndex) => {
271
+ if (usedPostEvents.has(postIndex)) return;
272
+
273
+ console.log('Orphaned post_tool event found:', postEvent.tool_name, 'at', postEvent.timestamp);
274
+
275
+ const toolName = postEvent.tool_name;
276
+ const sessionId = postEvent.session_id || 'unknown';
277
+ const postTimestamp = new Date(postEvent.timestamp).getTime();
278
+
279
+ const pairKey = `orphaned_${sessionId}_${toolName}_${postIndex}_${postTimestamp}`;
280
+ const pair = {
281
+ pre_event: null,
282
+ post_event: postEvent,
283
+ tool_name: toolName,
284
+ session_id: sessionId,
285
+ operation_type: 'tool_execution',
286
+ timestamp: postEvent.timestamp,
287
+ duration_ms: postEvent.duration_ms,
288
+ success: postEvent.success,
289
+ exit_code: postEvent.exit_code,
290
+ result_summary: postEvent.result_summary,
291
+ agent_type: null,
292
+ agent_confidence: null
293
+ };
294
+
295
+ const agentInfo = this.extractAgentFromEvent(postEvent);
296
+ pair.agent_type = agentInfo.name;
297
+ pair.agent_confidence = agentInfo.confidence;
298
+
299
+ toolCallPairs.set(pairKey, pair);
300
+ });
301
+
302
+ // Store the correlated tool calls
303
+ this.toolCalls = toolCallPairs;
304
+
305
+ console.log('updateToolCalls - final result:', this.toolCalls.size, 'tool calls');
306
+ if (this.toolCalls.size > 0) {
307
+ console.log('Tool calls map keys:', Array.from(this.toolCalls.keys()));
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Check if event is a tool operation
313
+ * @param {Object} event - Event to check
314
+ * @returns {boolean} - True if tool operation
315
+ */
316
+ isToolOperation(event) {
317
+ // Tool operations have tool_name and are hook events with pre_tool or post_tool subtype
318
+ return event.tool_name &&
319
+ event.type === 'hook' &&
320
+ (event.subtype === 'pre_tool' || event.subtype === 'post_tool' ||
321
+ event.subtype.includes('tool'));
322
+ }
323
+
324
+ /**
325
+ * Check if event is a file operation
326
+ * @param {Object} event - Event to check
327
+ * @returns {boolean} - True if file operation
328
+ */
329
+ isFileOperation(event) {
330
+ // File operations are tool events with tools that operate on files
331
+ const fileTools = ['Read', 'Write', 'Edit', 'Grep', 'MultiEdit'];
332
+ return event.tool_name && fileTools.includes(event.tool_name);
333
+ }
334
+
335
+ /**
336
+ * Extract file path from event
337
+ * @param {Object} event - Event to extract from
338
+ * @returns {string|null} - File path or null
339
+ */
340
+ extractFilePath(event) {
341
+ // Try various locations where file path might be stored
342
+ if (event.tool_parameters?.file_path) return event.tool_parameters.file_path;
343
+ if (event.tool_parameters?.path) return event.tool_parameters.path;
344
+ if (event.data?.tool_parameters?.file_path) return event.data.tool_parameters.file_path;
345
+ if (event.data?.tool_parameters?.path) return event.data.tool_parameters.path;
346
+ if (event.file_path) return event.file_path;
347
+ if (event.path) return event.path;
348
+
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * Extract file path from event pair
354
+ * @param {Object} pair - Event pair object
355
+ * @returns {string|null} - File path or null
356
+ */
357
+ extractFilePathFromPair(pair) {
358
+ // Try pre_event first, then post_event
359
+ let filePath = null;
360
+
361
+ if (pair.pre_event) {
362
+ filePath = this.extractFilePath(pair.pre_event);
363
+ }
364
+
365
+ if (!filePath && pair.post_event) {
366
+ filePath = this.extractFilePath(pair.post_event);
367
+ }
368
+
369
+ return filePath;
370
+ }
371
+
372
+ /**
373
+ * Get file operation type from event
374
+ * @param {Object} event - Event to analyze
375
+ * @returns {string} - Operation type
376
+ */
377
+ getFileOperation(event) {
378
+ if (!event.tool_name) return 'unknown';
379
+
380
+ const toolName = event.tool_name.toLowerCase();
381
+ switch (toolName) {
382
+ case 'read': return 'read';
383
+ case 'write': return 'write';
384
+ case 'edit': return 'edit';
385
+ case 'multiedit': return 'edit';
386
+ case 'grep': return 'search';
387
+ default: return toolName;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Get file operation from event pair
393
+ * @param {Object} pair - Event pair object
394
+ * @returns {string} - Operation type
395
+ */
396
+ getFileOperationFromPair(pair) {
397
+ // Try pre_event first, then post_event
398
+ if (pair.pre_event) {
399
+ return this.getFileOperation(pair.pre_event);
400
+ }
401
+
402
+ if (pair.post_event) {
403
+ return this.getFileOperation(pair.post_event);
404
+ }
405
+
406
+ return 'unknown';
407
+ }
408
+
409
+ /**
410
+ * Extract agent information from event pair
411
+ * @param {Object} pair - Event pair object
412
+ * @returns {Object} - Agent info with name and confidence
413
+ */
414
+ extractAgentFromPair(pair) {
415
+ // Try to get agent info from inference system first
416
+ const event = pair.pre_event || pair.post_event;
417
+ if (event && this.agentInference) {
418
+ const inference = this.agentInference.getInferredAgentForEvent(event);
419
+ if (inference) {
420
+ return {
421
+ name: inference.agentName || 'Unknown',
422
+ confidence: inference.confidence || 'unknown'
423
+ };
424
+ }
425
+ }
426
+
427
+ // Fallback to direct event properties
428
+ const agentName = event?.agent_type || event?.subagent_type ||
429
+ pair.pre_event?.agent_type || pair.post_event?.agent_type || 'PM';
430
+
431
+ return {
432
+ name: agentName,
433
+ confidence: 'direct'
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Get detailed operation information from event pair
439
+ * @param {Object} pair - Event pair object
440
+ * @returns {Object} - Operation details
441
+ */
442
+ getFileOperationDetailsFromPair(pair) {
443
+ const details = {};
444
+
445
+ // Extract details from pre_event (parameters)
446
+ if (pair.pre_event) {
447
+ const params = pair.pre_event.tool_parameters || pair.pre_event.data?.tool_parameters || {};
448
+ details.parameters = params;
449
+ details.tool_input = pair.pre_event.tool_input;
450
+ }
451
+
452
+ // Extract details from post_event (results)
453
+ if (pair.post_event) {
454
+ details.result = pair.post_event.result;
455
+ details.success = pair.post_event.success;
456
+ details.error = pair.post_event.error;
457
+ details.exit_code = pair.post_event.exit_code;
458
+ details.duration_ms = pair.post_event.duration_ms;
459
+ }
460
+
461
+ return details;
462
+ }
463
+
464
+ /**
465
+ * Get file operations map
466
+ * @returns {Map} - File operations map
467
+ */
468
+ getFileOperations() {
469
+ return this.fileOperations;
470
+ }
471
+
472
+ /**
473
+ * Get tool calls map
474
+ * @returns {Map} - Tool calls map
475
+ */
476
+ getToolCalls() {
477
+ return this.toolCalls;
478
+ }
479
+
480
+ /**
481
+ * Get tool calls as array for unique instance view
482
+ * Each entry represents a unique tool call instance
483
+ * @returns {Array} - Array of [key, toolCall] pairs
484
+ */
485
+ getToolCallsArray() {
486
+ return Array.from(this.toolCalls.entries());
487
+ }
488
+
489
+ /**
490
+ * Get file operations for a specific file
491
+ * @param {string} filePath - File path
492
+ * @returns {Object|null} - File operations data or null
493
+ */
494
+ getFileOperationsForFile(filePath) {
495
+ return this.fileOperations.get(filePath) || null;
496
+ }
497
+
498
+ /**
499
+ * Get tool call by key
500
+ * @param {string} key - Tool call key
501
+ * @returns {Object|null} - Tool call data or null
502
+ */
503
+ getToolCall(key) {
504
+ return this.toolCalls.get(key) || null;
505
+ }
506
+
507
+ /**
508
+ * Clear all tracking data
509
+ */
510
+ clear() {
511
+ this.fileOperations.clear();
512
+ this.toolCalls.clear();
513
+ console.log('File-tool tracker cleared');
514
+ }
515
+
516
+ /**
517
+ * Get statistics about tracked operations
518
+ * @returns {Object} - Statistics
519
+ */
520
+ getStatistics() {
521
+ return {
522
+ fileOperations: this.fileOperations.size,
523
+ toolCalls: this.toolCalls.size,
524
+ uniqueFiles: this.fileOperations.size,
525
+ totalFileOperations: Array.from(this.fileOperations.values())
526
+ .reduce((sum, data) => sum + data.operations.length, 0)
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Compare tool parameters between pre_tool and post_tool events
532
+ * to determine if they're likely from the same tool call
533
+ * @param {Object} preEvent - Pre-tool event
534
+ * @param {Object} postEvent - Post-tool event
535
+ * @returns {boolean} - True if parameters suggest same tool call
536
+ */
537
+ compareToolParameters(preEvent, postEvent) {
538
+ // Extract parameters from both events
539
+ const preParams = preEvent.tool_parameters || preEvent.data?.tool_parameters || {};
540
+ const postParams = postEvent.tool_parameters || postEvent.data?.tool_parameters || {};
541
+
542
+ // If no parameters in either event, can't compare meaningfully
543
+ if (Object.keys(preParams).length === 0 && Object.keys(postParams).length === 0) {
544
+ return false; // No boost for empty parameters
545
+ }
546
+
547
+ // Compare key parameters that are likely to be the same
548
+ const importantParams = ['file_path', 'path', 'pattern', 'command', 'notebook_path'];
549
+ let matchedParams = 0;
550
+ let totalComparableParams = 0;
551
+
552
+ importantParams.forEach(param => {
553
+ const preValue = preParams[param];
554
+ const postValue = postParams[param];
555
+
556
+ if (preValue !== undefined || postValue !== undefined) {
557
+ totalComparableParams++;
558
+ if (preValue === postValue) {
559
+ matchedParams++;
560
+ }
561
+ }
562
+ });
563
+
564
+ // If we found comparable parameters, check if most match
565
+ if (totalComparableParams > 0) {
566
+ return (matchedParams / totalComparableParams) >= 0.8; // 80% parameter match threshold
567
+ }
568
+
569
+ // If no important parameters to compare, check if the parameter structure is similar
570
+ const preKeys = Object.keys(preParams).sort();
571
+ const postKeys = Object.keys(postParams).sort();
572
+
573
+ if (preKeys.length === 0 && postKeys.length === 0) {
574
+ return false;
575
+ }
576
+
577
+ // Simple structural similarity check
578
+ if (preKeys.length === postKeys.length) {
579
+ const keyMatches = preKeys.filter(key => postKeys.includes(key)).length;
580
+ return keyMatches >= Math.max(1, preKeys.length * 0.5); // At least 50% key overlap
581
+ }
582
+
583
+ return false;
584
+ }
585
+
586
+ /**
587
+ * Extract agent information from event using inference system
588
+ * @param {Object} event - Event to extract agent from
589
+ * @returns {Object} - Agent info with name and confidence
590
+ */
591
+ extractAgentFromEvent(event) {
592
+ if (this.agentInference) {
593
+ const inference = this.agentInference.getInferredAgentForEvent(event);
594
+ if (inference) {
595
+ return {
596
+ name: inference.agentName || 'Unknown',
597
+ confidence: inference.confidence || 'unknown'
598
+ };
599
+ }
600
+ }
601
+
602
+ // Fallback to direct event properties
603
+ const agentName = event.agent_type || event.subagent_type ||
604
+ event.data?.agent_type || event.data?.subagent_type || 'PM';
605
+
606
+ return {
607
+ name: agentName,
608
+ confidence: 'direct'
609
+ };
610
+ }
611
+ }