UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

453 lines (408 loc) 12.2 kB
/** * Condition-based Waiting Utility * Replaces hardcoded delays with intelligent condition-based waiting */ export interface WaitCondition { check: () => Promise<boolean> | boolean; description?: string; } export interface WaitOptions { timeout?: number; // Maximum time to wait in milliseconds interval?: number; // Polling interval in milliseconds retries?: number; // Maximum number of retries exponentialBackoff?: boolean; // Use exponential backoff for intervals minInterval?: number; // Minimum interval for exponential backoff maxInterval?: number; // Maximum interval for exponential backoff } export interface WaitResult { success: boolean; duration: number; attempts: number; reason?: string; } export class ConditionWaiter { private static readonly DEFAULT_TIMEOUT = 120000; // 2 minutes for Claude operations private static readonly DEFAULT_INTERVAL = 500; // 500ms private static readonly DEFAULT_MIN_INTERVAL = 100; // 100ms private static readonly DEFAULT_MAX_INTERVAL = 5000; // 5 seconds private static readonly DEFAULT_RETRIES = 60; /** * Wait for a condition to be met */ public static async waitForCondition( condition: WaitCondition, options: WaitOptions = {} ): Promise<WaitResult> { const startTime = Date.now(); const timeout = options.timeout || this.DEFAULT_TIMEOUT; const baseInterval = options.interval || this.DEFAULT_INTERVAL; const maxRetries = options.retries || this.DEFAULT_RETRIES; const useExponentialBackoff = options.exponentialBackoff || false; const minInterval = options.minInterval || this.DEFAULT_MIN_INTERVAL; const maxInterval = options.maxInterval || this.DEFAULT_MAX_INTERVAL; let attempts = 0; let currentInterval = baseInterval; while (attempts < maxRetries && (Date.now() - startTime) < timeout) { attempts++; try { const result = await condition.check(); if (result) { return { success: true, duration: Date.now() - startTime, attempts, reason: 'Condition met' }; } } catch (error) { console.warn(`Condition check failed on attempt ${attempts}: ${error.message}`); } // Calculate next interval if (useExponentialBackoff) { currentInterval = Math.min( Math.max(currentInterval * 1.5, minInterval), maxInterval ); } else { currentInterval = baseInterval; } // Wait before next attempt await new Promise(resolve => setTimeout(resolve, currentInterval)); } return { success: false, duration: Date.now() - startTime, attempts, reason: `Timeout after ${timeout}ms or max retries (${maxRetries}) reached` }; } /** * Wait for tmux window to be ready (output contains expected content) */ public static async waitForTmuxWindow( bridge: any, sessionName: string, windowIndex: number, expectedContent: string | RegExp, options: WaitOptions = {} ): Promise<WaitResult> { const condition: WaitCondition = { check: async () => { try { const content = await bridge.captureWindowContent(sessionName, windowIndex, 20); if (typeof expectedContent === 'string') { return content.includes(expectedContent); } else { return expectedContent.test(content); } } catch (error) { return false; } }, description: `Waiting for tmux window ${sessionName}:${windowIndex} to contain: ${expectedContent}` }; return this.waitForCondition(condition, { timeout: 30000, interval: 1000, exponentialBackoff: true, ...options }); } /** * Wait for command completion (prompt appears) */ public static async waitForPrompt( bridge: any, sessionName: string, windowIndex: number, promptPattern: RegExp = /[\$#%>]\s*$/m, options: WaitOptions = {} ): Promise<WaitResult> { const condition: WaitCondition = { check: async () => { try { const content = await bridge.captureWindowContent(sessionName, windowIndex, 5); return promptPattern.test(content); } catch (error) { return false; } }, description: `Waiting for command prompt in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 10000, interval: 500, exponentialBackoff: true, ...options }); } /** * Wait for Claude agent to be ready (specific output patterns) */ public static async waitForClaudeReady( bridge: any, sessionName: string, windowIndex: number, options: WaitOptions = {} ): Promise<WaitResult> { const readyPatterns = [ /Claude\s+Code/i, /Model:\s+Claude/i, /Ready\s+to\s+help/i, /How\s+can\s+I\s+help/i, /What\s+would\s+you\s+like/i ]; const condition: WaitCondition = { check: async () => { try { const content = await bridge.captureWindowContent(sessionName, windowIndex, 10); return readyPatterns.some(pattern => pattern.test(content)); } catch (error) { return false; } }, description: `Waiting for Claude agent to be ready in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 60000, // Increased to 60 seconds for Claude startup interval: 2000, // Check every 2 seconds exponentialBackoff: true, ...options }); } /** * Wait for git operation completion */ public static async waitForGitOperation( bridge: any, sessionName: string, windowIndex: number, operation: 'push' | 'pull' | 'commit' | 'branch', options: WaitOptions = {} ): Promise<WaitResult> { const operationPatterns = { push: [/branch.*up.to.date/i, /everything.*up.to.date/i, /\d+\s+objects/i, /Total\s+\d+/i], pull: [/Already.*up.to.date/i, /Fast.forward/i, /files?\s+changed/i], commit: [/\d+\s+files?\s+changed/i, /create\s+mode/i, /\[\w+.*\]/i], branch: [/Switched\s+to/i, /already\s+on/i, /branch.*set\s+up/i] }; const errorPatterns = [ /error:/i, /fatal:/i, /failed/i, /rejected/i, /permission\s+denied/i, /authentication\s+failed/i ]; const condition: WaitCondition = { check: async () => { try { const content = await bridge.captureWindowContent(sessionName, windowIndex, 10); // Check for errors first if (errorPatterns.some(pattern => pattern.test(content))) { throw new Error(`Git operation failed: ${content.substring(0, 200)}`); } // Check for success patterns return operationPatterns[operation].some(pattern => pattern.test(content)); } catch (error) { if (error.message.includes('Git operation failed')) { throw error; } return false; } }, description: `Waiting for git ${operation} operation to complete in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 30000, interval: 1000, exponentialBackoff: true, ...options }); } /** * Wait for file or directory to exist */ public static async waitForPath( pathToCheck: string, options: WaitOptions = {} ): Promise<WaitResult> { const fs = require('fs').promises; const condition: WaitCondition = { check: async () => { try { await fs.access(pathToCheck); return true; } catch { return false; } }, description: `Waiting for path to exist: ${pathToCheck}` }; return this.waitForCondition(condition, { timeout: 10000, interval: 200, ...options }); } /** * Wait for Claude to complete a potentially long-running operation */ public static async waitForClaudeOperation( bridge: any, sessionName: string, windowIndex: number, operationDescription: string, options: WaitOptions = {} ): Promise<WaitResult> { const completionPatterns = [ /✅/, // Success emoji /completed/i, // "completed" text /finished/i, // "finished" text /done/i, // "done" text /ready/i, // "ready" text /[>$#%]\s*$/m, // Command prompt returned ]; const condition: WaitCondition = { check: async () => { try { const content = await bridge.captureWindowContent(sessionName, windowIndex, 20); return completionPatterns.some(pattern => pattern.test(content)); } catch (error) { return false; } }, description: `Waiting for Claude to complete: ${operationDescription} in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 600000, // 10 minutes for long operations interval: 10000, // Check every 10 seconds exponentialBackoff: false, ...options }); } /** * Wait for agent to be responsive (can execute simple commands) */ public static async waitForAgentResponsive( bridge: any, sessionName: string, windowIndex: number, options: WaitOptions = {} ): Promise<WaitResult> { const condition: WaitCondition = { check: async () => { try { // Send a simple test command await bridge.sendCommandToWindow(sessionName, windowIndex, 'echo "agent_test_ready"'); // Wait a bit for response await new Promise(resolve => setTimeout(resolve, 500)); // Check if we got the expected response const content = await bridge.captureWindowContent(sessionName, windowIndex, 5); return content.includes('agent_test_ready'); } catch (error) { return false; } }, description: `Waiting for agent to be responsive in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 10000, interval: 2000, exponentialBackoff: false, ...options }); } /** * Wait for process to complete (no active processes in tmux pane) */ public static async waitForProcessCompletion( bridge: any, sessionName: string, windowIndex: number, options: WaitOptions = {} ): Promise<WaitResult> { const condition: WaitCondition = { check: async () => { try { // Check if there are any running processes await bridge.sendCommandToWindow(sessionName, windowIndex, 'ps aux | grep -v grep | grep -c "[^]]$" || echo "0"'); await new Promise(resolve => setTimeout(resolve, 300)); const content = await bridge.captureWindowContent(sessionName, windowIndex, 3); // Look for process count of 0 or 1 (just the shell) const lines = content.trim().split('\n'); const lastLine = lines[lines.length - 1]; const processCount = parseInt(lastLine.trim()); return processCount <= 1; } catch (error) { return false; } }, description: `Waiting for processes to complete in ${sessionName}:${windowIndex}` }; return this.waitForCondition(condition, { timeout: 60000, interval: 2000, exponentialBackoff: true, ...options }); } /** * Wait for network connectivity */ public static async waitForNetwork( host: string = 'google.com', options: WaitOptions = {} ): Promise<WaitResult> { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const condition: WaitCondition = { check: async () => { try { await execAsync(`ping -c 1 -W 2 ${host}`, { timeout: 3000 }); return true; } catch { return false; } }, description: `Waiting for network connectivity to ${host}` }; return this.waitForCondition(condition, { timeout: 30000, interval: 2000, exponentialBackoff: true, ...options }); } /** * Create a combined condition (AND logic) */ public static combineConditions(...conditions: WaitCondition[]): WaitCondition { return { check: async () => { for (const condition of conditions) { const result = await condition.check(); if (!result) return false; } return true; }, description: `Combined conditions: ${conditions.map(c => c.description).join(' AND ')}` }; } /** * Create an any condition (OR logic) */ public static anyCondition(...conditions: WaitCondition[]): WaitCondition { return { check: async () => { for (const condition of conditions) { const result = await condition.check(); if (result) return true; } return false; }, description: `Any condition: ${conditions.map(c => c.description).join(' OR ')}` }; } }