- Fix regex pattern in semantic-validator.js that was truncating domain names (google.com -> google) - Remove unnecessary 'info' type error reporting to bug tracker - Only add bug tracker activity when errorId is valid - Add terminal approval UI design document Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
428 lines
17 KiB
JavaScript
428 lines
17 KiB
JavaScript
/**
|
|
* Command Execution Tracker
|
|
* Monitors command lifecycle, detects behavioral anomalies,
|
|
* and identifies patterns in command execution failures.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ============================================================
|
|
// COMMAND TRACKER CLASS
|
|
// ============================================================
|
|
|
|
class CommandTracker {
|
|
constructor() {
|
|
this.pendingCommands = new Map();
|
|
this.executionHistory = [];
|
|
this.maxHistorySize = 100;
|
|
this.anomalyThreshold = 3; // Report if 3+ similar issues in 5 minutes
|
|
this.anomalyWindow = 5 * 60 * 1000; // 5 minutes
|
|
}
|
|
|
|
/**
|
|
* Generate a unique command ID
|
|
* @returns {string} - Command ID
|
|
*/
|
|
generateId() {
|
|
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Start tracking a command execution
|
|
* @param {string} message - Command message
|
|
* @param {string} mode - Chat mode (webcontainer, auto, native)
|
|
* @param {string} sessionId - Session ID
|
|
* @returns {string} - Command ID
|
|
*/
|
|
startCommand(message, mode, sessionId) {
|
|
const commandId = this.generateId();
|
|
|
|
const commandData = {
|
|
id: commandId,
|
|
message: message,
|
|
extractedCommand: this.extractActualCommand(message),
|
|
mode: mode,
|
|
sessionId: sessionId,
|
|
startTime: Date.now(),
|
|
state: 'pending',
|
|
metadata: this.getCommandMetadata(message)
|
|
};
|
|
|
|
this.pendingCommands.set(commandId, commandData);
|
|
|
|
console.log('[CommandTracker] Started tracking:', {
|
|
id: commandId,
|
|
command: message.substring(0, 50)
|
|
});
|
|
|
|
return commandId;
|
|
}
|
|
|
|
/**
|
|
* Extract actual command (strip conversational wrappers)
|
|
* @param {string} message - Original message
|
|
* @returns {string} - Extracted command
|
|
*/
|
|
extractActualCommand(message) {
|
|
if (window.semanticValidator && typeof window.semanticValidator.extractCommand === 'function') {
|
|
const extracted = window.semanticValidator.extractCommand(message);
|
|
return extracted || message;
|
|
}
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Get command metadata for analysis
|
|
* @param {string} message - Command message
|
|
* @returns {object} - Metadata
|
|
*/
|
|
getCommandMetadata(message) {
|
|
const metadata = {
|
|
length: message.length,
|
|
wordCount: message.split(/\s+/).length,
|
|
hasSpecialChars: /[\|>]/.test(message),
|
|
hasPath: /^\//.test(message) || /\//.test(message),
|
|
isConversational: false,
|
|
isCommandRequest: false
|
|
};
|
|
|
|
if (window.semanticValidator) {
|
|
metadata.isConversational = window.semanticValidator.isConversational(message);
|
|
metadata.isCommandRequest = message.match(/\b(run|execute|exec|do)\s+/i) !== null;
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
/**
|
|
* Complete command execution
|
|
* @param {string} commandId - Command ID
|
|
* @param {number|null} exitCode - Exit code
|
|
* @param {string} output - Command output
|
|
*/
|
|
completeCommand(commandId, exitCode, output) {
|
|
const command = this.pendingCommands.get(commandId);
|
|
if (!command) {
|
|
console.warn('[CommandTracker] Unknown command ID:', commandId);
|
|
return;
|
|
}
|
|
|
|
const duration = Date.now() - command.startTime;
|
|
const success = exitCode === 0;
|
|
|
|
// Update command data
|
|
command.endTime = Date.now();
|
|
command.exitCode = exitCode;
|
|
command.output = output ? output.substring(0, 500) : '';
|
|
command.duration = duration;
|
|
command.state = success ? 'completed' : 'failed';
|
|
command.success = success;
|
|
|
|
// Add to history
|
|
this.addToHistory(command);
|
|
|
|
// Run semantic checks
|
|
this.checkForSemanticErrors(command);
|
|
|
|
// Remove from pending
|
|
this.pendingCommands.delete(commandId);
|
|
|
|
console.log('[CommandTracker] Completed:', {
|
|
id: commandId,
|
|
success: success,
|
|
duration: duration,
|
|
exitCode: exitCode
|
|
});
|
|
|
|
// Periodically check for anomalies
|
|
if (this.executionHistory.length % 10 === 0) {
|
|
this.detectBehavioralAnomalies();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add completed command to history
|
|
* @param {object} command - Command data
|
|
*/
|
|
addToHistory(command) {
|
|
this.executionHistory.push(command);
|
|
|
|
// Trim history if too large
|
|
if (this.executionHistory.length > this.maxHistorySize) {
|
|
this.executionHistory = this.executionHistory.slice(-this.maxHistorySize);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for semantic errors in command execution
|
|
* @param {object} command - Command data
|
|
*/
|
|
checkForSemanticErrors(command) {
|
|
// Check 1: Undefined exit code (UX issue)
|
|
if (command.exitCode === undefined || command.exitCode === null) {
|
|
this.reportSemanticError({
|
|
type: 'ux_issue',
|
|
subtype: 'undefined_exit_code',
|
|
message: 'Command completed with undefined exit code',
|
|
details: {
|
|
command: command.message,
|
|
extractedCommand: command.extractedCommand,
|
|
output: command.output,
|
|
suggestedFix: 'Ensure exit code is always returned from command execution'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check 2: Conversational message that failed as command
|
|
if (command.metadata.isConversational && command.exitCode !== 0) {
|
|
this.reportSemanticError({
|
|
type: 'intent_error',
|
|
subtype: 'conversational_failed_as_command',
|
|
message: 'Conversational message failed when executed as shell command',
|
|
details: {
|
|
command: command.message,
|
|
mode: command.mode,
|
|
exitCode: command.exitCode,
|
|
error: this.extractError(command.output),
|
|
suggestedFix: 'Improve conversational pattern detection to prevent this'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check 3: Command request pattern with no actual command extracted
|
|
if (command.metadata.isCommandRequest && command.extractedCommand === command.message) {
|
|
// Command was not extracted properly
|
|
if (command.exitCode !== 0) {
|
|
this.reportSemanticError({
|
|
type: 'intent_error',
|
|
subtype: 'command_extraction_failed',
|
|
message: 'Command request pattern detected but extraction may have failed',
|
|
details: {
|
|
command: command.message,
|
|
exitCode: command.exitCode,
|
|
error: this.extractError(command.output)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check 4: Long-running command (> 30 seconds)
|
|
if (command.duration > 30000) {
|
|
console.warn('[CommandTracker] Long-running command:', {
|
|
command: command.extractedCommand,
|
|
duration: command.duration
|
|
});
|
|
|
|
this.reportSemanticError({
|
|
type: 'performance_issue',
|
|
subtype: 'long_running_command',
|
|
message: 'Command took longer than 30 seconds to execute',
|
|
details: {
|
|
command: command.extractedCommand,
|
|
duration: command.duration,
|
|
suggestedFix: 'Consider timeout or progress indicator'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check 5: Empty output for successful command
|
|
if (command.success && (!command.output || command.output.trim() === '')) {
|
|
this.reportSemanticError({
|
|
type: 'ux_issue',
|
|
subtype: 'empty_success_output',
|
|
message: 'Command succeeded but produced no output',
|
|
details: {
|
|
command: command.extractedCommand,
|
|
suggestedFix: 'Show "Command completed successfully" message for empty output'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract error message from output
|
|
* @param {string} output - Command output
|
|
* @returns {string} - Error message
|
|
*/
|
|
extractError(output) {
|
|
if (!output) return 'No output';
|
|
|
|
// Try to extract first line of error
|
|
const lines = output.split('\n');
|
|
const errorLine = lines.find(line =>
|
|
line.toLowerCase().includes('error') ||
|
|
line.toLowerCase().includes('failed') ||
|
|
line.toLowerCase().includes('cannot')
|
|
);
|
|
|
|
return errorLine ? errorLine.substring(0, 100) : output.substring(0, 100);
|
|
}
|
|
|
|
/**
|
|
* Detect behavioral anomalies in command history
|
|
*/
|
|
detectBehavioralAnomalies() {
|
|
const now = Date.now();
|
|
const recentWindow = this.executionHistory.filter(
|
|
cmd => now - cmd.endTime < this.anomalyWindow
|
|
);
|
|
|
|
// Anomaly 1: Multiple conversational messages failing as commands
|
|
const conversationalFailures = recentWindow.filter(cmd =>
|
|
cmd.metadata.isConversational && cmd.exitCode !== 0
|
|
);
|
|
|
|
if (conversationalFailures.length >= this.anomalyThreshold) {
|
|
this.reportSemanticError({
|
|
type: 'behavioral_anomaly',
|
|
subtype: 'repeated_conversational_failures',
|
|
message: `Pattern detected: ${conversationalFailures.length} conversational messages failed as commands in last 5 minutes`,
|
|
details: {
|
|
count: conversationalFailures.length,
|
|
examples: conversationalFailures.slice(0, 3).map(f => f.message),
|
|
suggestedFix: 'Improve conversational detection or add user education'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Anomaly 2: High failure rate for specific command patterns
|
|
const commandFailures = new Map();
|
|
recentWindow.forEach(cmd => {
|
|
if (!cmd.success) {
|
|
const key = cmd.extractedCommand.split(' ')[0]; // First word (command name)
|
|
commandFailures.set(key, (commandFailures.get(key) || 0) + 1);
|
|
}
|
|
});
|
|
|
|
commandFailures.forEach((count, commandName) => {
|
|
if (count >= this.anomalyThreshold) {
|
|
this.reportSemanticError({
|
|
type: 'behavioral_anomaly',
|
|
subtype: 'high_failure_rate',
|
|
message: `Command "${commandName}" failed ${count} times in last 5 minutes`,
|
|
details: {
|
|
command: commandName,
|
|
failureCount: count,
|
|
suggestedFix: 'Check if command exists or is being used correctly'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Anomaly 3: Commands with undefined exit codes
|
|
const undefinedExitCodes = recentWindow.filter(cmd =>
|
|
cmd.exitCode === undefined || cmd.exitCode === null
|
|
);
|
|
|
|
if (undefinedExitCodes.length >= 5) {
|
|
this.reportSemanticError({
|
|
type: 'behavioral_anomaly',
|
|
subtype: 'undefined_exit_code_pattern',
|
|
message: `${undefinedExitCodes.length} commands completed with undefined exit code`,
|
|
details: {
|
|
count: undefinedExitCodes.length,
|
|
affectedCommands: undefinedExitCodes.slice(0, 5).map(c => c.extractedCommand),
|
|
suggestedFix: 'Fix command execution to always return exit code'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report a semantic error
|
|
* @param {object} errorData - Error details
|
|
*/
|
|
reportSemanticError(errorData) {
|
|
const semanticError = {
|
|
type: 'semantic',
|
|
subtype: errorData.subtype || errorData.type,
|
|
message: errorData.message,
|
|
details: errorData.details || {},
|
|
timestamp: new Date().toISOString(),
|
|
url: window.location.href,
|
|
context: {
|
|
chatMode: window.currentChatMode || 'unknown',
|
|
sessionId: window.attachedSessionId || window.chatSessionId,
|
|
pendingCommands: this.pendingCommands.size,
|
|
recentHistory: this.executionHistory.slice(-5).map(cmd => ({
|
|
command: cmd.extractedCommand.substring(0, 30),
|
|
success: cmd.success,
|
|
timestamp: cmd.endTime
|
|
}))
|
|
}
|
|
};
|
|
|
|
// Report to bug tracker
|
|
if (window.bugTracker) {
|
|
const errorId = window.bugTracker.addError(semanticError);
|
|
// Only add activity if error was actually added (not skipped)
|
|
if (errorId) {
|
|
window.bugTracker.addActivity(errorId, '📊', `Pattern: ${errorData.subtype}`, 'warning');
|
|
}
|
|
}
|
|
|
|
// Report to server
|
|
fetch('/claude/api/log-error', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(semanticError)
|
|
}).catch(err => console.error('[CommandTracker] Failed to report error:', err));
|
|
|
|
console.log('[CommandTracker] Semantic error reported:', semanticError);
|
|
}
|
|
|
|
/**
|
|
* Get statistics about command execution
|
|
* @returns {object} - Statistics
|
|
*/
|
|
getStatistics() {
|
|
const total = this.executionHistory.length;
|
|
const successful = this.executionHistory.filter(cmd => cmd.success).length;
|
|
const failed = total - successful;
|
|
const avgDuration = total > 0
|
|
? this.executionHistory.reduce((sum, cmd) => sum + (cmd.duration || 0), 0) / total
|
|
: 0;
|
|
|
|
return {
|
|
total,
|
|
successful,
|
|
failed,
|
|
successRate: total > 0 ? (successful / total * 100).toFixed(1) : 0,
|
|
avgDuration: Math.round(avgDuration),
|
|
pending: this.pendingCommands.size
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear old history (older than 1 hour)
|
|
*/
|
|
clearOldHistory() {
|
|
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
|
this.executionHistory = this.executionHistory.filter(
|
|
cmd => cmd.endTime > oneHourAgo
|
|
);
|
|
|
|
console.log('[CommandTracker] Cleared old history, remaining:', this.executionHistory.length);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// GLOBAL INSTANCE
|
|
// ============================================================
|
|
|
|
// Create global instance
|
|
window.commandTracker = new CommandTracker();
|
|
|
|
// Auto-clean old history every 10 minutes
|
|
setInterval(() => {
|
|
window.commandTracker.clearOldHistory();
|
|
}, 10 * 60 * 1000);
|
|
|
|
// Expose statistics to console for debugging
|
|
window.getCommandStats = () => window.commandTracker.getStatistics();
|
|
|
|
console.log('[CommandTracker] Command lifecycle tracking initialized');
|
|
|
|
})();
|