diff --git a/CLAUDE.md b/CLAUDE.md index 98b32a8b..15cef242 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs. -**Version**: 0.1377 (must match `package.json`) +**Version**: 0.1378 (must match `package.json`) **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index 2055ebf1..eb8d882d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1377", + "version": "0.1378", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/respawn-fixes.md b/respawn-fixes.md new file mode 100644 index 00000000..ca811316 --- /dev/null +++ b/respawn-fixes.md @@ -0,0 +1,34 @@ + +### Fix for Issue #65 +**Send/wait state stuck** + +Stuck in waiting_update - the controller is trying to send a command but may not be receiving expected response. + +Check terminal output: +```bash +curl -sk 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/terminal?tail=10000' | tail -100 +``` + +Try restarting respawn: +```bash +curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/stop' +curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/start' +``` + + +### Fix for Issue #66 +**Send/wait state stuck** + +Stuck in waiting_update - the controller is trying to send a command but may not be receiving expected response. + +Check terminal output: +```bash +curl -sk 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/terminal?tail=10000' | tail -100 +``` + +Try restarting respawn: +```bash +curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/stop' +curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/start' +``` + diff --git a/respawn-issues.md b/respawn-issues.md new file mode 100644 index 00000000..c03aaae0 --- /dev/null +++ b/respawn-issues.md @@ -0,0 +1,16 @@ + +### Issue #65 [WARNING] - 2026-01-26 08:50:53 +**State stuck: waiting_update for 10+ minutes** + +The respawn controller has been in 'waiting_update' state for over 10 minutes. +Time since activity: 389ms +Detection status: {"stopHookReceived":false,"stopHookTime":null,"idlePromptReceived":false,"idlePromptTime":null,"completionMessageDetected":false,"completionMessageTime":null,"outputSilent":false,"msSinceLastOutput":389,"tokensStable":true,"lastTokenCount":9900,"msSinceTokenChange":270313,"workingPatternsAbsent":true,"msSinceLastWorking":327049,"aiCheck":{"status":"ready","lastVerdict":"IDLE","lastReasoning":"The terminal output is empty/blank, which typically indicates the session is at rest waiting for input. There are no spinner characters, no activity text, no partial output, and no signs of active processing. An empty terminal with no visible activity suggests the session has completed any previous work and is waiting for new input.","lastCheckDurationMs":6555,"cooldownEndsAt":null,"consecutiveErrors":0,"totalChecks":1,"disabledReason":null},"confidenceLevel":35,"statusText":"Respawn step: waiting_update","waitingFor":"Step completion","activeTimers":[{"name":"step-confirm","remainingMs":5106,"totalMs":10000},{"name":"no-output-fallback","remainingMs":29611,"totalMs":30000},{"name":"pre-filter","remainingMs":9611,"totalMs":10000},{"name":"auto-accept","remainingMs":7611,"totalMs":8000}],"recentActions":[{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769413853255},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769413853255},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started no-output-fallback: 30s (fallback if no output at all)","timestamp":1769413853255},{"type":"timer-cancel","detail":"no-output-fallback: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769413853126},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769413853126},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769413853126},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769413853126}],"currentPhase":"Waiting for update to complete","nextAction":"Will send /clear when done"} + + +### Issue #66 [WARNING] - 2026-01-26 09:01:06 +**State stuck: waiting_update for 10+ minutes** + +The respawn controller has been in 'waiting_update' state for over 10 minutes. +Time since activity: 115ms +Detection status: {"stopHookReceived":false,"stopHookTime":null,"idlePromptReceived":false,"idlePromptTime":null,"completionMessageDetected":false,"completionMessageTime":null,"outputSilent":false,"msSinceLastOutput":115,"tokensStable":false,"lastTokenCount":9900,"msSinceTokenChange":6401,"workingPatternsAbsent":false,"msSinceLastWorking":4171,"aiCheck":{"status":"ready","lastVerdict":"IDLE","lastReasoning":"The terminal output is empty/blank, which typically indicates the session is at rest waiting for input. There are no spinner characters, no activity text, no partial output, and no signs of active processing. An empty terminal with no visible activity suggests the session has completed any previous work and is waiting for new input.","lastCheckDurationMs":6555,"cooldownEndsAt":null,"consecutiveErrors":0,"totalChecks":1,"disabledReason":null},"confidenceLevel":0,"statusText":"Respawn step: waiting_update","waitingFor":"Step completion","activeTimers":[{"name":"step-confirm","remainingMs":5852,"totalMs":10000},{"name":"no-output-fallback","remainingMs":29885,"totalMs":30000},{"name":"pre-filter","remainingMs":9885,"totalMs":10000},{"name":"auto-accept","remainingMs":7885,"totalMs":8000}],"recentActions":[{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769414466626},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769414466626},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started no-output-fallback: 30s (fallback if no output at all)","timestamp":1769414466626},{"type":"timer-cancel","detail":"no-output-fallback: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769414466529},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769414466529},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769414466529},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769414466529}],"currentPhase":"Waiting for update to complete","nextAction":"Will send /clear when done"} + diff --git a/src/run-summary.ts b/src/run-summary.ts index 6df851a8..d58330ec 100644 --- a/src/run-summary.ts +++ b/src/run-summary.ts @@ -184,7 +184,9 @@ export class RunSummaryTracker { const now = Date.now(); // Update idle time if was previously idle - if (!this.isCurrentlyActive && this.stats.lastIdleAt) { + // Note: We count the initial idle period (from session start to first working state) + // even if lastIdleAt hasn't been set yet + if (!this.isCurrentlyActive) { this.stats.totalTimeIdleMs += now - this.lastStatusChangeAt; } diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts index ed983c91..e44cc552 100644 --- a/src/subagent-watcher.ts +++ b/src/subagent-watcher.ts @@ -242,6 +242,28 @@ export class SubagentWatcher extends EventEmitter { return this.agentInfo.get(agentId); } + /** + * Update a subagent's description. + * Used to set the short description from TaskTracker when available. + */ + updateDescription(agentId: string, description: string): boolean { + const info = this.agentInfo.get(agentId); + if (info) { + info.description = description; + this.emit('subagent:updated', info); + return true; + } + return false; + } + + /** + * Find sessions whose working directory matches a project hash. + * Returns the project hash for a given working directory. + */ + getProjectHashForDir(workingDir: string): string { + return this.getProjectHash(workingDir); + } + /** * Get recent subagents (modified within specified minutes) */ diff --git a/src/web/public/app.js b/src/web/public/app.js index fe78bc63..d924a974 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1661,6 +1661,15 @@ class ClaudemanApp { } this.renderSubagentPanel(); this.updateSubagentWindows(); + + // Auto-minimize completed subagent windows + if (this.subagentWindows.has(data.agentId)) { + const windowData = this.subagentWindows.get(data.agentId); + if (windowData && !windowData.minimized) { + this.closeSubagentWindow(data.agentId); // This minimizes to tab + this.saveSubagentWindowStates(); // Persist the minimized state + } + } }); } @@ -1722,6 +1731,9 @@ class ClaudemanApp { this.subagents.set(s.agentId, s); }); this.renderSubagentPanel(); + + // Restore subagent window states (minimized/open) after subagents are loaded + this.restoreSubagentWindowStates(); } // Auto-select first session if any @@ -1871,7 +1883,6 @@ class ClaudemanApp { ${this.escapeHtml(name)} ${hasRunningTasks ? `${taskStats.running}` : ''} ${subagentBadge} - 📊 ⚙ × `); @@ -1929,6 +1940,9 @@ class ClaudemanApp { // Re-render tabs to update badge this.renderSessionTabs(); + + // Persist the state change + this.saveSubagentWindowStates(); } // Toggle subagent dropdown visibility on click @@ -1992,6 +2006,9 @@ class ClaudemanApp { // Re-render tabs to update badge this.renderSessionTabs(); this.updateConnectionLines(); + + // Persist the state change + this.saveSubagentWindowStates(); } getSessionName(session) { @@ -3355,18 +3372,21 @@ class ClaudemanApp { closeSessionOptions() { this.editingSessionId = null; + // Stop run summary auto-refresh if it was running + this.stopRunSummaryAutoRefresh(); document.getElementById('sessionOptionsModal').classList.remove('active'); } // ========== Run Summary Modal ========== async openRunSummary(sessionId) { + // Open session options modal and switch to summary tab + this.openSessionOptions(sessionId); + this.switchOptionsTab('summary'); + this.runSummarySessionId = sessionId; this.runSummaryFilter = 'all'; - // Show modal - document.getElementById('runSummaryModal').classList.add('active'); - // Reset filter buttons document.querySelectorAll('.run-summary-filters .filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === 'all'); @@ -3379,12 +3399,14 @@ class ClaudemanApp { closeRunSummary() { this.runSummarySessionId = null; this.stopRunSummaryAutoRefresh(); - document.getElementById('runSummaryModal').classList.remove('active'); + // Close session options modal (summary is now a tab in it) + this.closeSessionOptions(); } async refreshRunSummary() { - if (!this.runSummarySessionId) return; - await this.loadRunSummary(this.runSummarySessionId); + const sessionId = this.runSummarySessionId || this.editingSessionId; + if (!sessionId) return; + await this.loadRunSummary(sessionId); } toggleRunSummaryAutoRefresh() { @@ -3632,6 +3654,12 @@ class ClaudemanApp { document.getElementById('respawn-tab').classList.toggle('hidden', tabName !== 'respawn'); document.getElementById('context-tab').classList.toggle('hidden', tabName !== 'context'); document.getElementById('ralph-tab').classList.toggle('hidden', tabName !== 'ralph'); + document.getElementById('summary-tab').classList.toggle('hidden', tabName !== 'summary'); + + // Load run summary data when switching to summary tab + if (tabName === 'summary' && this.editingSessionId) { + this.loadRunSummary(this.editingSessionId); + } } getRalphConfig() { @@ -4008,6 +4036,120 @@ class ClaudemanApp { return this.loadAppSettingsFromStorage(); } + // ========== Subagent Window State Persistence ========== + + /** + * Save subagent window states (minimized/open) to server for cross-browser persistence. + * Called when a window is minimized, restored, or auto-minimized on completion. + */ + async saveSubagentWindowStates() { + // Build state object: which agents are minimized per session + const minimizedState = {}; + for (const [sessionId, agentIds] of this.minimizedSubagents) { + minimizedState[sessionId] = Array.from(agentIds); + } + + // Also track which windows are open (not minimized) + const openWindows = []; + for (const [agentId, windowData] of this.subagentWindows) { + if (!windowData.minimized) { + openWindows.push({ + agentId, + position: windowData.position || null + }); + } + } + + const windowStates = { minimized: minimizedState, open: openWindows }; + + // Save to localStorage for quick restore + localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(windowStates)); + + // Save to server for cross-browser persistence + try { + await fetch('/api/subagent-window-states', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(windowStates) + }); + } catch (err) { + console.error('Failed to save subagent window states to server:', err); + } + } + + /** + * Load subagent window states from server (or localStorage fallback). + * Called on page load to restore minimized/open window states. + */ + async loadSubagentWindowStates() { + let states = null; + + // Try server first for cross-browser sync + try { + const res = await fetch('/api/subagent-window-states'); + if (res.ok) { + states = await res.json(); + // Also update localStorage + localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(states)); + } + } catch (err) { + console.error('Failed to load subagent window states from server:', err); + } + + // Fallback to localStorage + if (!states) { + try { + const saved = localStorage.getItem('claudeman-subagent-window-states'); + if (saved) { + states = JSON.parse(saved); + } + } catch (err) { + console.error('Failed to load subagent window states from localStorage:', err); + } + } + + return states || { minimized: {}, open: [] }; + } + + /** + * Restore subagent window states after loading subagents. + * Opens windows that were open before, keeps minimized ones minimized. + */ + async restoreSubagentWindowStates() { + const states = await this.loadSubagentWindowStates(); + + // Restore minimized state + for (const [sessionId, agentIds] of Object.entries(states.minimized || {})) { + if (Array.isArray(agentIds) && agentIds.length > 0) { + // Verify agents still exist + const validAgents = agentIds.filter(id => this.subagents.has(id)); + if (validAgents.length > 0) { + this.minimizedSubagents.set(sessionId, new Set(validAgents)); + } + } + } + + // Restore open windows (for non-completed agents only) + for (const { agentId, position } of (states.open || [])) { + const agent = this.subagents.get(agentId); + // Only restore window if agent exists and is still active/idle (not completed) + if (agent && agent.status !== 'completed') { + this.openSubagentWindow(agentId); + // Restore position if saved + if (position) { + const windowData = this.subagentWindows.get(agentId); + if (windowData && windowData.element) { + windowData.element.style.left = position.left; + windowData.element.style.top = position.top; + windowData.position = position; + } + } + } + } + + this.renderSessionTabs(); // Update tab badges + } + // ========== Help Modal ========== showHelp() { @@ -5402,6 +5544,9 @@ class ClaudemanApp { // No animation, just update connection lines this.updateConnectionLines(); } + + // Persist the state change (new window opened) + this.saveSubagentWindowStates(); } closeSubagentWindow(agentId) { @@ -5424,6 +5569,9 @@ class ClaudemanApp { // Update tab badge to show minimized agents this.renderSessionTabs(); + + // Persist the state change + this.saveSubagentWindowStates(); } else { // No parent session - fully remove the window windowData.element.remove(); diff --git a/src/web/public/index.html b/src/web/public/index.html index 35617170..ecfd01d1 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -326,6 +326,7 @@ + + + + - +