feat(execution): add ExecutionBridge for parallel task execution

Implements the Execution Optimizer Rework plan that enables the system
to actually use the optimizer metadata (parallelGroups, agentType,
recommendedModel, requiresFreshContext, estimatedTokens) that was
previously generated but ignored.

New components:
- ExecutionBridge: Central coordinator that loads optimized plans,
  manages parallel execution within groups, and coordinates with
  SpawnOrchestrator for session-based execution
- ModelSelector: Routes tasks to appropriate models (opus/sonnet/haiku)
  based on user defaults and agent type overrides. Optimizer
  recommendations are advisory only - user preferences always win
- GroupScheduler: Builds topologically ordered execution groups,
  manages dependencies, determines execution mode (session vs task-tool)
- ContextManager: Handles fresh context requirements via /clear+/init
  or new session spawning

Features:
- Parallel task execution within groups (configurable limit)
- Group-level dependency tracking (lower groups complete first)
- Partial failure handling (continue with non-dependent tasks)
- Model configuration in App Settings > Models tab
- Agent type overrides (explore, implement, test, review)
- Execution control API endpoints (start, pause, resume, cancel)
- SSE events for real-time execution progress visibility
- Execution history tracking

API endpoints:
- GET/POST/PUT /api/execution/* for execution control
- GET/PUT /api/execution/model-config for model settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-28 07:07:18 +01:00
co-authored by Claude Opus 4.5
parent 9eccd061b9
commit 55e9f9f0a5
12 changed files with 3140 additions and 9 deletions
+174
View File
@@ -6193,6 +6193,7 @@ class ClaudemanApp {
document.getElementById('modalAutoCompactPrompt').value = session.autoCompactPrompt ?? '';
document.getElementById('modalAutoClearEnabled').checked = session.autoClearEnabled ?? false;
document.getElementById('modalAutoClearThreshold').value = session.autoClearThreshold ?? 140000;
document.getElementById('modalImageWatcherEnabled').checked = session.imageWatcherEnabled ?? true;
// Populate Ralph Wiggum form with current session values
const ralphState = this.ralphStates.get(sessionId);
@@ -6234,6 +6235,26 @@ class ClaudemanApp {
} catch { /* silent */ }
}
async toggleSessionImageWatcher() {
if (!this.editingSessionId) return;
const enabled = document.getElementById('modalImageWatcherEnabled').checked;
try {
await fetch(`/api/sessions/${this.editingSessionId}/image-watcher`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
// Update local session state
const session = this.sessions.get(this.editingSessionId);
if (session) {
session.imageWatcherEnabled = enabled;
}
this.showToast(`Image watcher ${enabled ? 'enabled' : 'disabled'}`, 'success');
} catch (err) {
this.showToast('Failed to toggle image watcher', 'error');
}
}
async autoSaveRespawnConfig() {
if (!this.editingSessionId) return;
const config = {
@@ -6834,6 +6855,7 @@ class ClaudemanApp {
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? true;
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? true;
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? false;
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? true;
// Claude CLI settings
const claudeModeSelect = document.getElementById('appSettingsClaudeMode');
const allowedToolsRow = document.getElementById('allowedToolsRow');
@@ -6848,6 +6870,8 @@ class ClaudemanApp {
const niceSettings = settings.nice || {};
document.getElementById('appSettingsNiceEnabled').checked = niceSettings.enabled ?? false;
document.getElementById('appSettingsNiceValue').value = niceSettings.niceValue ?? 10;
// Model configuration (loaded from server)
this.loadModelConfigForSettings();
// Notification settings
const notifPrefs = this.notificationManager?.preferences || {};
document.getElementById('appSettingsNotifEnabled').checked = notifPrefs.enabled ?? true;
@@ -6906,6 +6930,7 @@ class ClaudemanApp {
showSubagents: document.getElementById('appSettingsShowSubagents').checked,
subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked,
subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked,
imageWatcherEnabled: document.getElementById('appSettingsImageWatcherEnabled').checked,
// Claude CLI settings
claudeMode: document.getElementById('appSettingsClaudeMode').value,
allowedTools: document.getElementById('appSettingsAllowedTools').value.trim(),
@@ -6947,6 +6972,10 @@ class ClaudemanApp {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...settings, notificationPreferences: notifPrefsToSave })
});
// Save model configuration separately
await this.saveModelConfigFromSettings();
this.showToast('Settings saved', 'success');
} catch (err) {
// Server save failed but localStorage succeeded
@@ -6956,6 +6985,71 @@ class ClaudemanApp {
this.closeAppSettings();
}
// Load model configuration from server for the settings modal
async loadModelConfigForSettings() {
try {
const res = await fetch('/api/execution/model-config');
const data = await res.json();
if (data.success && data.data) {
const config = data.data;
// Default model
const defaultModelEl = document.getElementById('appSettingsDefaultModel');
if (defaultModelEl) {
defaultModelEl.value = config.defaultModel || 'sonnet';
}
// Show recommendations
const showRecsEl = document.getElementById('appSettingsShowModelRecommendations');
if (showRecsEl) {
showRecsEl.checked = config.showRecommendations ?? true;
}
// Agent type overrides
const overrides = config.agentTypeOverrides || {};
const exploreEl = document.getElementById('appSettingsModelExplore');
const implementEl = document.getElementById('appSettingsModelImplement');
const testEl = document.getElementById('appSettingsModelTest');
const reviewEl = document.getElementById('appSettingsModelReview');
if (exploreEl) exploreEl.value = overrides.explore || '';
if (implementEl) implementEl.value = overrides.implement || '';
if (testEl) testEl.value = overrides.test || '';
if (reviewEl) reviewEl.value = overrides.review || '';
}
} catch (err) {
console.warn('Failed to load model config:', err);
}
}
// Save model configuration from settings modal to server
async saveModelConfigFromSettings() {
const defaultModelEl = document.getElementById('appSettingsDefaultModel');
const showRecsEl = document.getElementById('appSettingsShowModelRecommendations');
const exploreEl = document.getElementById('appSettingsModelExplore');
const implementEl = document.getElementById('appSettingsModelImplement');
const testEl = document.getElementById('appSettingsModelTest');
const reviewEl = document.getElementById('appSettingsModelReview');
const agentTypeOverrides = {};
if (exploreEl?.value) agentTypeOverrides.explore = exploreEl.value;
if (implementEl?.value) agentTypeOverrides.implement = implementEl.value;
if (testEl?.value) agentTypeOverrides.test = testEl.value;
if (reviewEl?.value) agentTypeOverrides.review = reviewEl.value;
const config = {
defaultModel: defaultModelEl?.value || 'sonnet',
showRecommendations: showRecsEl?.checked ?? true,
agentTypeOverrides,
};
try {
await fetch('/api/execution/model-config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
} catch (err) {
console.warn('Failed to save model config:', err);
}
}
// Get the global Ralph tracker enabled setting
isRalphTrackerEnabledByDefault() {
const settings = this.loadAppSettingsFromStorage();
@@ -7067,6 +7161,40 @@ class ClaudemanApp {
localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
}
async clearAllSubagents() {
const count = this.subagents.size;
if (count === 0) {
this.showToast('No subagents to clear', 'info');
return;
}
if (!confirm(`Clear all ${count} tracked subagent(s)? This removes them from the UI but does not affect running processes.`)) {
return;
}
try {
const res = await fetch('/api/subagents', { method: 'DELETE' });
const data = await res.json();
if (data.success) {
// Clear local state
this.subagents.clear();
this.subagentActivity.clear();
this.subagentToolResults.clear();
// Close any open subagent windows
this.cleanupAllFloatingWindows();
// Update UI
this.renderSubagentPanel();
this.renderMonitorSubagents();
this.updateSubagentBadge();
this.showToast(`Cleared ${data.data.cleared} subagent(s)`, 'success');
} else {
this.showToast('Failed to clear subagents: ' + data.error, 'error');
}
} catch (err) {
this.showToast('Failed to clear subagents', 'error');
}
}
toggleSubagentsPanel() {
const panel = document.getElementById('subagentsPanel');
const toggleBtn = document.getElementById('subagentsToggleBtn');
@@ -8713,6 +8841,9 @@ class ClaudemanApp {
// Always update badge count
this.updateSubagentBadge();
// Always update monitor panel (even if subagent panel is hidden)
this.renderMonitorSubagents();
// If panel is not visible, don't render content
if (!this.subagentPanelVisible) {
return;
@@ -11002,6 +11133,49 @@ class ClaudemanApp {
body.innerHTML = html;
}
renderMonitorSubagents() {
const body = document.getElementById('monitorSubagentsBody');
const stats = document.getElementById('monitorSubagentStats');
if (!body) return;
const subagents = Array.from(this.subagents.values());
const activeCount = subagents.filter(s => s.status === 'active' || s.status === 'idle').length;
if (stats) {
stats.textContent = `${subagents.length} tracked` + (activeCount > 0 ? `, ${activeCount} active` : '');
}
if (subagents.length === 0) {
body.innerHTML = '<div class="monitor-empty">No background agents</div>';
return;
}
let html = '';
for (const agent of subagents) {
const statusClass = agent.status === 'active' ? 'active' : agent.status === 'idle' ? 'idle' : 'completed';
const modelBadge = agent.modelShort ? `<span class="model-badge ${agent.modelShort}">${agent.modelShort}</span>` : '';
const desc = agent.description ? this.escapeHtml(agent.description.substring(0, 40)) : agent.agentId;
html += `
<div class="process-item">
<span class="process-mode ${statusClass}">${agent.status}</span>
<div class="process-info">
<div class="process-name">${modelBadge} ${desc}</div>
<div class="process-meta">
<span>ID: ${agent.agentId}</span>
<span>${agent.toolCallCount || 0} tools</span>
</div>
</div>
<div class="process-actions">
${agent.status !== 'completed' ? `<button class="btn-toolbar btn-sm btn-danger" onclick="app.killSubagent('${agent.agentId}')" title="Kill agent">Kill</button>` : ''}
</div>
</div>
`;
}
body.innerHTML = html;
}
async killScreen(sessionId) {
if (!confirm('Kill this screen session?')) return;
+93
View File
@@ -331,6 +331,16 @@
<div class="monitor-empty">No screen sessions</div>
</div>
</div>
<div class="monitor-section" id="monitorSubagentsSection">
<div class="monitor-section-header">
<span>Background Agents</span>
<span class="task-stats" id="monitorSubagentStats">0 tracked</span>
<button class="btn-toolbar btn-sm btn-danger" onclick="app.clearAllSubagents()" title="Clear all">Clear</button>
</div>
<div class="monitor-section-body" id="monitorSubagentsBody">
<div class="monitor-empty">No background agents</div>
</div>
</div>
<div class="monitor-section" id="backgroundTasksSection" style="display: none;">
<div class="monitor-section-header">
<span>Background Tasks</span>
@@ -349,6 +359,7 @@
Subagents <span id="subagentCountBadge" class="subagent-badge"></span>
</div>
<div class="subagents-panel-actions">
<button class="btn-toolbar btn-sm btn-danger" onclick="app.clearAllSubagents()" title="Clear all tracked subagents">Clear All</button>
<button class="btn-icon-sm" onclick="app.toggleSubagentsDetach()" title="Detach panel" id="subagentsDetachBtn">&#x29C9;</button>
<button class="btn-icon-sm" onclick="app.toggleSubagentsPanel()" title="Toggle panel" id="subagentsToggleBtn">&#x25B2;</button>
<button class="btn-icon-sm" onclick="app.closeSubagentsPanel()" title="Close">&times;</button>
@@ -489,6 +500,15 @@
</div>
<span class="form-hint">Completely resets context (use higher threshold than compact)</span>
</div>
<div class="form-row form-row-switch">
<label>Image Watcher</label>
<label class="switch">
<input type="checkbox" id="modalImageWatcherEnabled" checked onchange="app.toggleSessionImageWatcher()">
<span class="slider"></span>
</label>
<span class="form-hint">Auto-popup new images (screenshots, etc.) in this session's directory</span>
</div>
</div><!-- End context-tab -->
<!-- Ralph Wiggum Tab Content -->
@@ -685,6 +705,7 @@
<div class="modal-tabs">
<button class="modal-tab-btn active" data-tab="settings-display">Display</button>
<button class="modal-tab-btn" data-tab="settings-claude">Claude CLI</button>
<button class="modal-tab-btn" data-tab="settings-models">Models</button>
<button class="modal-tab-btn" data-tab="settings-paths">Paths</button>
<button class="modal-tab-btn" data-tab="settings-notifications">Notifications</button>
</div>
@@ -763,6 +784,16 @@
<span class="slider"></span>
</label>
</div>
<!-- Image Watcher Options Section -->
<div class="settings-section-header">Image Watcher</div>
<div class="settings-item" title="Automatically detect and popup new images in session directories">
<span class="settings-item-label">Enable Globally</span>
<label class="switch switch-sm">
<input type="checkbox" id="appSettingsImageWatcherEnabled" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
<!-- Claude CLI Tab -->
@@ -805,6 +836,68 @@
<span class="form-hint">Process priority (-20 to 19, higher = lower priority, default: 10)</span>
</div>
</div>
<!-- Models Tab -->
<div class="modal-tab-content hidden" id="settings-models">
<div class="form-row">
<label>Default Model</label>
<select id="appSettingsDefaultModel" class="form-select">
<option value="sonnet">Sonnet (Recommended - balanced)</option>
<option value="opus">Opus (Most capable)</option>
<option value="haiku">Haiku (Fast & cheap)</option>
</select>
<span class="form-hint">Model used for execution tasks. Optimizer suggestions are advisory only.</span>
</div>
<div class="form-row form-row-switch">
<label>Show Optimizer Recommendations</label>
<label class="switch">
<input type="checkbox" id="appSettingsShowModelRecommendations" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Show what the optimizer recommends (doesn't override your choice)</span>
</div>
<div class="form-section-header">Agent Type Overrides</div>
<p class="form-hint" style="margin: 0 0 12px 0;">Optionally set specific models for each task type. Leave as "Use Default" to use your default model.</p>
<div class="form-row">
<label>Explore Tasks</label>
<select id="appSettingsModelExplore" class="form-select">
<option value="">Use Default</option>
<option value="haiku">Haiku</option>
<option value="sonnet">Sonnet</option>
<option value="opus">Opus</option>
</select>
<span class="form-hint">Quick searches, codebase exploration</span>
</div>
<div class="form-row">
<label>Implement Tasks</label>
<select id="appSettingsModelImplement" class="form-select">
<option value="">Use Default</option>
<option value="haiku">Haiku</option>
<option value="sonnet">Sonnet</option>
<option value="opus">Opus</option>
</select>
<span class="form-hint">Code writing, feature implementation</span>
</div>
<div class="form-row">
<label>Test Tasks</label>
<select id="appSettingsModelTest" class="form-select">
<option value="">Use Default</option>
<option value="haiku">Haiku</option>
<option value="sonnet">Sonnet</option>
<option value="opus">Opus</option>
</select>
<span class="form-hint">Writing and running tests</span>
</div>
<div class="form-row">
<label>Review Tasks</label>
<select id="appSettingsModelReview" class="form-select">
<option value="">Use Default</option>
<option value="haiku">Haiku</option>
<option value="sonnet">Sonnet</option>
<option value="opus">Opus</option>
</select>
<span class="form-hint">Code review, quality checks</span>
</div>
</div>
<!-- Paths Tab -->
<div class="modal-tab-content hidden" id="settings-paths">
<div class="form-row">