UNPKG

claude-code-company

Version:

Multi-agent tmux coordination system for Claude Code with perfect Unicode support

730 lines (624 loc) 26.6 kB
const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const { colors, rateLimitConfig } = require('./config'); // シンプルなエラー追跡 const errorStats = { timeoutErrors: 0, connectionErrors: 0, commandErrors: 0 }; // 負荷状況の追跡 const loadMetrics = { messageCount: 0, lastMessageTime: Date.now(), avgResponseTime: 0, responseTimeHistory: [] }; // Window IDキャッシュ(削除済み - 常に現在のウィンドウIDを使用) // キャッシュ関連の関数はダミー実装として残す(互換性のため) // キャッシュされたWindow IDを取得(常にnullを返す) function getCachedWindowId() { // キャッシュは使用しない - 常に現在のウィンドウIDを再取得 return null; } // Window IDをキャッシュに設定(何もしない) function setCachedWindowId(windowId, lock = false) { // キャッシュは使用しない if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] Window ID cache disabled - ignoring set request for: ${windowId}`); } } // Window IDのロック状態を確認(常にfalseを返す) function isWindowIdCacheLocked() { // ロック機能は無効 return false; } // キャッシュをクリア(何もしない) function clearWindowIdCache() { // キャッシュは使用しない if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] Window ID cache disabled - ignoring clear request`); } } // 動的遅延の計算 function calculateDynamicDelay(baseDelay = 100) { const now = Date.now(); const timeSinceLastMessage = now - loadMetrics.lastMessageTime; // 負荷判定 let loadFactor = 1; if (loadMetrics.messageCount > 10 && timeSinceLastMessage < 500) { // 高負荷: 10メッセージ以上かつ500ms以内 loadFactor = 0.5; } else if (loadMetrics.messageCount > 5 && timeSinceLastMessage < 1000) { // 中負荷 loadFactor = 0.3; } else { // 低負荷 loadFactor = 0; } // 0-50msの範囲で調整 const dynamicDelay = Math.min(50, Math.max(0, baseDelay * loadFactor)); // メトリクス更新 loadMetrics.messageCount++; loadMetrics.lastMessageTime = now; return dynamicDelay; } // Execute tmux command with enhanced error handling function tmuxCommand(...args) { return new Promise(async (resolve, reject) => { // セッション存在確認(has-sessionコマンド以外) if (args[0] !== 'has-session' && args[0] !== 'list-sessions') { try { await validateTmuxSession(); } catch (sessionError) { errorStats.connectionErrors++; reject(new Error(`tmux session validation failed: ${sessionError.message}`)); return; } } const proc = spawn('tmux', args, { stdio: 'pipe' }); // 設定可能なタイムアウト const timeoutMs = rateLimitConfig.tmuxTimeout || 10000; const timeout = setTimeout(() => { errorStats.timeoutErrors++; proc.kill('SIGKILL'); reject(new Error(`tmux command timeout (${timeoutMs}ms): ${args.join(' ')}`)); }, timeoutMs); let output = ''; let errorOutput = ''; if (proc.stdout) { proc.stdout.on('data', (data) => { output += data.toString(); }); } if (proc.stderr) { proc.stderr.on('data', (data) => { errorOutput += data.toString(); }); } proc.on('close', (code) => { clearTimeout(timeout); if (code === 0) { resolve(output); } else { errorStats.commandErrors++; const errorMsg = errorOutput || `tmux command failed with code ${code}`; reject(new Error(`${errorMsg} (command: ${args.join(' ')})`)) } }); proc.on('error', (error) => { clearTimeout(timeout); errorStats.connectionErrors++; reject(new Error(`tmux spawn error: ${error.message} (command: ${args.join(' ')})`)) }); }); } // tmuxセッション検証(現在のセッションを確認) async function validateTmuxSession() { try { // 直接tmuxコマンドでセッション情報を取得(再帰を避ける) const sessionProc = spawn('tmux', ['display-message', '-p', '#{session_name}'], { stdio: 'pipe' }); return new Promise((resolve, reject) => { let output = ''; if (sessionProc.stdout) { sessionProc.stdout.on('data', (data) => { output += data.toString(); }); } sessionProc.on('close', (code) => { if (code === 0) { const sessionName = output.trim(); if (sessionName) { resolve(true); } else { reject(new Error('No active tmux session found')); } } else { reject(new Error('tmux is not available or no session active')); } }); sessionProc.on('error', () => { reject(new Error('tmux is not available')); }); }); } catch (error) { throw new Error(`Session validation error: ${error.message}`); } } // 呼び出し元(コマンド実行場所)のwindow番号を取得 // 注意: これはエージェントがいるウィンドウではなく、コマンドを実行したプロセスのウィンドウ async function getCallerWindow() { const startTime = Date.now(); try { // キャッシュは使用しない - 常に最新の呼び出し元ウィンドウIDを取得 // デバッグログ - 取得開始 if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getCallerWindow: Starting caller window ID retrieval...`); } // 現在のwindow IDを直接取得(セッション名に依存しない) let windowId = null; let attempts = 0; const maxAttempts = 3; // リトライロジック while (attempts < maxAttempts && !windowId) { attempts++; try { const result = await tmuxCommand('display-message', '-p', '#{window_id}'); windowId = result.trim(); // window IDの検証 if (!windowId || !windowId.startsWith('@')) { if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getCallerWindow: Invalid window ID format: '${windowId}' (attempt ${attempts}/${maxAttempts})`); } windowId = null; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100 * attempts)); // 段階的待機 } } } catch (attemptError) { if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.error(`[DEBUG] getCallerWindow: Attempt ${attempts}/${maxAttempts} failed: ${attemptError.message}`); } if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100 * attempts)); } } } // 取得失敗時のフォールバック if (!windowId) { // 別の方法でwindow IDを取得を試みる try { const listResult = await tmuxCommand('list-windows', '-F', '#{window_id},#{window_active}'); const activeWindow = listResult.split('\n').find(line => line.endsWith(',1')); if (activeWindow) { windowId = activeWindow.split(',')[0]; if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getCallerWindow: Retrieved window ID from list-windows: ${windowId}`); } } } catch (listError) { if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.error(`[DEBUG] getCallerWindow: list-windows fallback failed: ${listError.message}`); } } } // 最終的なフォールバック if (!windowId || !windowId.startsWith('@')) { windowId = '@1'; if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.warn(`[WARN] getCallerWindow: Using default window ID: ${windowId}`); } } // デバッグログ - 成功 if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { const elapsed = Date.now() - startTime; const sessionName = await tmuxCommand('display-message', '-p', '#{session_name}').catch(() => 'unknown'); console.log(`[DEBUG] getCallerWindow: Successfully retrieved caller window ID: ${windowId} in session: ${sessionName.trim()} (took ${elapsed}ms, ${attempts} attempts)`); } return windowId; } catch (error) { // エラー時の詳細ログ const elapsed = Date.now() - startTime; if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.error(`[ERROR] getCallerWindow: Failed to get window ID after ${elapsed}ms: ${error.message}`); console.error(`[ERROR] getCallerWindow: Error stack: ${error.stack}`); } // エラー統計を更新 errorStats.commandErrors++; return '@1'; } } // Get tmux target for pane (window.pane format)(エラーハンドリング強化版) async function getTmuxTarget(pane, agent) { const startTime = Date.now(); try { // pane検証 if (pane === null || pane === undefined) { const error = new Error('Pane parameter is null or undefined'); if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.error(`[ERROR] getTmuxTarget: ${error.message}`); } throw error; } if (typeof pane !== 'number' && typeof pane !== 'string') { const error = new Error(`Invalid pane type: ${typeof pane}`); if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.error(`[ERROR] getTmuxTarget: ${error.message}`); } throw error; } const paneNum = parseInt(pane); if (isNaN(paneNum) || paneNum < 0 || paneNum > 50) { const error = new Error(`Invalid pane number: ${pane} (must be 0-50)`); if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.error(`[ERROR] getTmuxTarget: ${error.message}`); } throw error; } // デバッグログ if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.log(`[DEBUG] getTmuxTarget: Getting target for pane ${paneNum}${agent ? ` (agent: ${agent})` : ''}`); } // 現在のwindowでターゲットを構成(セッション名に依存しない) let currentWindow = null; let target = null; try { // キャッシュされたwindow IDがあればそれを使用 currentWindow = getCachedWindowId(); if (!currentWindow) { currentWindow = await getCallerWindow(); } target = `${currentWindow}.${paneNum}`; // ターゲットの存在確認 try { await tmuxCommand('display-message', '-t', target, '-p', '#{pane_id}'); // デバッグログ - 成功 if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { const elapsed = Date.now() - startTime; console.log(`[DEBUG] getTmuxTarget: Successfully validated target ${target} (took ${elapsed}ms)`); } } catch (validateError) { // ペインが存在しない場合 if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.warn(`[WARN] getTmuxTarget: Target ${target} does not exist: ${validateError.message}`); } // ターゲットは返すが、警告を記録 } return target; } catch (windowError) { // window ID取得エラー if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.error(`[ERROR] getTmuxTarget: Failed to get window ID: ${windowError.message}`); } // フォールバックターゲット target = `@1.${paneNum}`; if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.warn(`[WARN] getTmuxTarget: Using fallback target ${target}`); } return target; } } catch (error) { // 予期しないエラー const elapsed = Date.now() - startTime; if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.error(`[ERROR] getTmuxTarget: Unexpected error after ${elapsed}ms: ${error.message}`); console.error(`[ERROR] getTmuxTarget: Error stack: ${error.stack}`); } // エラー統計を更新 errorStats.commandErrors++; // 最終フォールバック const fallbackTarget = `@1.${pane || 0}`; if (process.env.DEBUG_TMUX || process.env.DEBUG_TARGET) { console.warn(`[WARN] getTmuxTarget: Returning final fallback target ${fallbackTarget}`); } return fallbackTarget; } } // Send keys to tmux pane async function tmuxSendKeys(pane, keys, agent = null) { const target = agent ? await getTmuxTarget(pane, agent) : await getTmuxTarget(pane, null); return tmuxCommand('send-keys', '-t', target, '-l', keys, 'C-m'); } // Send raw keys to tmux pane async function tmuxSendKeysRaw(pane, keys, agent = null) { const target = agent ? await getTmuxTarget(pane, agent) : await getTmuxTarget(pane, null); return tmuxCommand('send-keys', '-t', target, keys); } // Get pane content async function tmuxGetPaneContent(pane, agent = null) { return new Promise(async (resolve, reject) => { const target = agent ? await getTmuxTarget(pane, agent) : await getTmuxTarget(pane, null); const proc = spawn('tmux', ['capture-pane', '-t', target, '-p'], { stdio: 'pipe' }); let output = ''; if (proc.stdout) { proc.stdout.on('data', (data) => { output += data.toString(); }); } proc.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(`tmux capture-pane failed with code ${code}`)); } }); proc.on('error', reject); }); } // Get current directory function getCurrentDir() { return process.cwd(); } // 現在のペインから役割を判定 async function getCurrentRole() { try { // 現在のペイン番号を取得 const paneProc = spawn('tmux', ['display-message', '-p', '#{pane_index}'], { stdio: 'pipe' }); let paneOutput = ''; if (paneProc.stdout) { paneProc.stdout.on('data', (data) => { paneOutput += data.toString().trim(); }); } const currentPaneIndex = await new Promise((resolve, reject) => { paneProc.on('close', (code) => { if (code === 0) { resolve(parseInt(paneOutput) || 0); } else { resolve(null); } }); paneProc.on('error', () => resolve(null)); }); // pane 0はui/control用として特別扱い if (currentPaneIndex === 0) { return 'ui_control'; } // まずはペインタイトルで判定 const titleProc = spawn('tmux', ['display-message', '-p', '#{pane_title}'], { stdio: 'pipe' }); let titleOutput = ''; if (titleProc.stdout) { titleProc.stdout.on('data', (data) => { titleOutput += data.toString().trim(); }); } const titleResult = await new Promise((resolve, reject) => { titleProc.on('close', (code) => { if (code === 0) { const title = titleOutput.toLowerCase(); if (title.includes('president') || title.includes('プロジェクト管理')) { resolve('president'); } else if (title.includes('boss') || title.includes('delegation') || title.includes('work delegation')) { resolve('boss1'); } else if (title.includes('worker')) { const match = title.match(/worker(\d+)/); resolve(match ? `worker${match[1]}` : 'worker1'); } else { resolve(null); } } else { resolve(null); } }); titleProc.on('error', () => resolve(null)); }); if (titleResult) { return titleResult; } // ペインタイトルで判定できない場合、全ペインをスキャンして推測 const allPanesProc = spawn('tmux', ['list-panes', '-a', '-F', '#{pane_title},#{pane_id},#{pane_current}'], { stdio: 'pipe' }); let allPanesOutput = ''; if (allPanesProc.stdout) { allPanesProc.stdout.on('data', (data) => { allPanesOutput += data.toString(); }); } return new Promise((resolve, reject) => { allPanesProc.on('close', (code) => { if (code === 0) { const lines = allPanesOutput.trim().split('\n'); const currentPane = lines.find(line => line.endsWith(',1')); if (currentPane) { const [title, paneId, isCurrent] = currentPane.split(','); const lowerTitle = title.toLowerCase(); if (lowerTitle.includes('president') || lowerTitle.includes('プロジェクト管理')) { resolve('president'); } else if (lowerTitle.includes('boss') || lowerTitle.includes('delegation') || lowerTitle.includes('work delegation')) { resolve('boss1'); } else if (lowerTitle.includes('worker')) { const match = lowerTitle.match(/worker(\d+)/); resolve(match ? `worker${match[1]}` : 'worker1'); } else { // セッションが存在するが役割不明 resolve('in_session'); } } else { // tmuxセッション外で実行 resolve('outside_session'); } } else { // tmuxが利用できない resolve('no_tmux'); } }); allPanesProc.on('error', () => resolve('no_tmux')); }); } catch (error) { console.error('Error getting current role:', error); return 'no_tmux'; } } // Clean message sender to avoid input artifacts async function sendCleanMessage(pane, message, targetWindowId = null) { // windowIdを決定する優先順位: // 1. 引数で指定されたtargetWindowId // 2. 現在のwindow ID(常に最新を取得) let windowId = targetWindowId; let target; if (!windowId) { // 常に現在のウィンドウIDを取得(キャッシュは使用しない) try { windowId = await getCallerWindow(); // デバッグログ if (process.env.DEBUG_TMUX) { console.log(`[DEBUG] sendCleanMessage: Retrieved current window ID ${windowId}`); } } catch (error) { // エラー時はデフォルトのwindow IDとターゲット windowId = '@1'; console.warn(`[WARN] Failed to get window ID, using default: ${error.message}`); } } else { // デバッグログ if (process.env.DEBUG_TMUX) { console.log(`[DEBUG] sendCleanMessage: Using specified window ID ${windowId}`); } } target = `${windowId}.${pane}`; try { // pane検証 if (pane === null || pane === undefined) { throw new Error('Pane number is null or undefined'); } if (typeof pane !== 'number' || pane < 0 || pane > 20) { throw new Error(`Invalid pane number: ${pane} (must be 0-20)`); } // message検証 if (!message || typeof message !== 'string') { throw new Error(`Invalid message: ${message}`); } // メッセージ長検証(tmuxバッファ制限) if (message.length > 10000) { throw new Error(`Message too long: ${message.length} characters (max: 10000)`); } // 固定されたwindow IDとペイン番号を使用してselect-pane await tmuxCommand('select-pane', '-t', target); // すべてのtmuxコマンドで固定されたtargetを使用 await tmuxCommand('send-keys', '-t', target, 'C-c'); await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(500))); await tmuxCommand('send-keys', '-t', target, 'C-u'); await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(200))); await tmuxCommand('send-keys', '-t', target, 'C-k'); await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(200))); const messageLines = message.split('\n'); for (const line of messageLines) { try { if (line.trim() === '') { await tmuxCommand('send-keys', '-t', target, 'Enter'); } else { const escapedLine = line.replace(/[\$\`\\"]/g, '\\$&'); await tmuxCommand('send-keys', '-t', target, '-l', escapedLine); await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(50))); await tmuxCommand('send-keys', '-t', target, 'Enter'); } await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(100))); } catch (lineError) { console.error(`Error sending line "${line}": ${lineError.message}`); continue; } } await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(300))); await tmuxCommand('send-keys', '-t', target, 'Enter'); await new Promise(resolve => setTimeout(resolve, calculateDynamicDelay(200))); await tmuxCommand('send-keys', '-t', target, 'Enter'); // 送信完了時のデバッグログ if (process.env.DEBUG_TMUX) { console.log(`[DEBUG] Message sent successfully to ${target}`); } } catch (error) { console.error(`${colors.red}Error in sendCleanMessage to pane ${pane}: ${error.message}${colors.nc}`); throw new Error(`Failed to send message to pane ${pane}: ${error.message}`); } } // \u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u304c\u5b9f\u969b\u306b\u3044\u308b\u30a6\u30a3\u30f3\u30c9\u30a6ID\u3092\u53d6\u5f97 // \u3059\u3079\u3066\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u3092\u691c\u7d22\u3057\u3066\u3001\u6307\u5b9a\u3055\u308c\u305f\u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u540d\u3092\u542b\u3080\u30da\u30a4\u30f3\u3092\u63a2\u3059 async function getAgentWindow(agentName) { try { // \u30c7\u30d0\u30c3\u30b0\u30ed\u30b0 if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getAgentWindow: Searching for agent '${agentName}'...`); } // \u3059\u3079\u3066\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u3092\u30ea\u30b9\u30c8\u30a2\u30c3\u30d7 const windowsOutput = await tmuxCommand('list-windows', '-F', '#{window_id}'); const windowIds = windowsOutput.trim().split('\n').filter(id => id); if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getAgentWindow: Found ${windowIds.length} windows to search`); } // \u5404\u30a6\u30a3\u30f3\u30c9\u30a6\u3067\u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u3092\u691c\u7d22 for (const windowId of windowIds) { try { // \u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u3059\u3079\u3066\u306e\u30da\u30a4\u30f3\u3092\u53d6\u5f97 const panesOutput = await tmuxCommand('list-panes', '-t', windowId, '-F', '#{pane_index}:#{pane_title}'); const panes = panesOutput.trim().split('\n'); // \u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u540d\u3092\u542b\u3080\u30da\u30a4\u30f3\u3092\u63a2\u3059 for (const paneInfo of panes) { if (paneInfo.includes(agentName)) { if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getAgentWindow: Found agent '${agentName}' in window ${windowId}`); } return windowId; } } // \u30da\u30a4\u30f3\u306e\u5185\u5bb9\u3082\u30c1\u30a7\u30c3\u30af\uff08\u30bf\u30a4\u30c8\u30eb\u3067\u898b\u3064\u304b\u3089\u306a\u3044\u5834\u5408\uff09 const panesContentOutput = await tmuxCommand('list-panes', '-t', windowId, '-F', '#{pane_index}'); const paneIndices = panesContentOutput.trim().split('\n'); for (const paneIndex of paneIndices) { try { const paneContent = await tmuxCommand('capture-pane', '-t', `${windowId}.${paneIndex}`, '-p'); // \u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u8b58\u5225\u30d1\u30bf\u30fc\u30f3\u3092\u691c\u7d22 if (paneContent.includes(`=== ${agentName}`) || paneContent.includes(`${agentName} (pane`)) { if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.log(`[DEBUG] getAgentWindow: Found agent '${agentName}' in window ${windowId} (by content)`); } return windowId; } } catch (captureError) { // \u30da\u30a4\u30f3\u306e\u30ad\u30e3\u30d7\u30c1\u30e3\u306b\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u30b9\u30ad\u30c3\u30d7 continue; } } } catch (windowError) { // \u30a6\u30a3\u30f3\u30c9\u30a6\u306e\u691c\u7d22\u306b\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u6b21\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u3078 if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.error(`[DEBUG] getAgentWindow: Error searching window ${windowId}: ${windowError.message}`); } continue; } } // \u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u304c\u898b\u3064\u304b\u3089\u306a\u3044\u5834\u5408\u306f\u3001\u547c\u3073\u51fa\u3057\u5143\u306e\u30a6\u30a3\u30f3\u30c9\u30a6ID\u3092\u8fd4\u3059\uff08\u30d5\u30a9\u30fc\u30eb\u30d0\u30c3\u30af\uff09 const callerWindow = await getCallerWindow(); if (process.env.DEBUG_TMUX || process.env.DEBUG_WINDOW) { console.warn(`[WARN] getAgentWindow: Agent '${agentName}' not found in any window, using caller window ${callerWindow}`); } return callerWindow; } catch (error) { console.error(`[ERROR] getAgentWindow: Failed to find window for agent '${agentName}': ${error.message}`); // \u30a8\u30e9\u30fc\u6642\u306f\u547c\u3073\u51fa\u3057\u5143\u306e\u30a6\u30a3\u30f3\u30c9\u30a6ID\u3092\u8fd4\u3059 return await getCallerWindow(); } } // Git worktree management placeholder functions function createWorkerBranch(workerName, baseBranch = 'main') { // Placeholder - actual implementation would create branch const branchName = `worker/${workerName}/${Date.now()}`; return branchName; } module.exports = { tmuxCommand, getTmuxTarget, getCallerWindow, getAgentWindow, // 新規追加: エージェントが実際にいるウィンドウIDを取得 getCachedWindowId, setCachedWindowId, clearWindowIdCache, isWindowIdCacheLocked, tmuxSendKeys, tmuxSendKeysRaw, tmuxGetPaneContent, getCurrentDir, createWorkerBranch, sendCleanMessage, getCurrentRole };