Skip to content

Commit 729cf73

Browse files
committed
feat(core,ui,tauri): parallel task execution with scan-aware concurrency
Enable multiple tasks to run simultaneously now that the worktree migration makes each task filesystem-isolated. Default concurrency is 5, configurable in Settings → General. Scans count against the same pool so SUSTN never over-subscribes Claude CLI. Backend (Rust): - EngineState.running_tasks is now Mutex<HashMap<String, CurrentTask>> instead of Mutex<Option<CurrentTask>>, tracking multiple tasks by id - New concurrency_limit (RwLock<usize>, default 5), tokens_reserved (Mutex<i64>), active_scans (Mutex<usize>) on EngineState - engine_start_task and engine_address_review: capacity check sums running_tasks + active_scans; register/release on task boundaries; reserve/release estimated tokens via budget::estimated_task_tokens - engine_scan_now: increments active_scans for pass 1 and spawned pass 2, releases on both success and error paths - budget::calculate_budget_status_with_reservation subtracts reserved tokens so concurrent starts do not over-commit the daily budget - New engine_set_concurrency_limit Tauri command (clamped 1..=10) - EngineStatusResponse exposes running_tasks and concurrency_limit Frontend: - useQueueProcessor: removed single-task processingRef, now checks runningTasks.length + concurrencyLimit via engine_get_status before dequeuing. Chains processNext on task completion or enqueue - useScheduler: skips tick when at concurrency capacity instead of when any task is running - TaskDetailView: queues only when at capacity, starts immediately when a slot is available - TaskStatusBanner: checks runningTasks.some(t => t.taskId === id) for per-task working state - Deep scan listener invalidates every running task in the affected repo - useStartupRecovery syncs persisted concurrencyLimit to the Rust state at boot so the setting survives restarts Scan scope control: - Migration 21: scan_enabled column on agent_config (default 1) - AgentConfig.scanEnabled wired through DB layer and useUpdateAgentConfig - Scheduler and startup scan skip repos with scanEnabled=false - pr-import sets scanEnabled=false on imported repos; they never auto-scan again unless the user explicitly re-enables in project settings (new toggle in the Automation section) PR lifecycle parallelism: - prLifecycleTick now processes active PRs via Promise.allSettled instead of a sequential for-await loop, so multiple PRs can sync and address in parallel (bounded by backend concurrency limit) Settings: - GlobalSettings.concurrencyLimit (default 5), persisted in global_settings via migration 20 - General settings section: parallel-tasks selector (1-10) - useUpdateGlobalSetting propagates the limit to the Rust state on change so it takes effect immediately without restart
1 parent 7387bdd commit 729cf73

18 files changed

Lines changed: 441 additions & 120 deletions

src-tauri/src/engine/budget.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,24 @@ pub fn get_weekly_usage() -> Result<(i64, String), String> {
149149
/// usage earlier in the week from falsely exhausting the budget for remaining days —
150150
/// the actual subscription resets on rolling windows, not as a rigid weekly lump sum.
151151
pub fn calculate_budget_status(config: &BudgetConfig) -> BudgetStatus {
152+
calculate_budget_status_with_reservation(config, 0)
153+
}
154+
155+
/// Calculate budget status accounting for tokens already reserved by
156+
/// in-flight tasks. Used to prevent over-commit when multiple tasks
157+
/// start concurrently.
158+
pub fn calculate_budget_status_with_reservation(
159+
config: &BudgetConfig,
160+
tokens_reserved: i64,
161+
) -> BudgetStatus {
152162
let (tokens_today, _) = get_today_usage().unwrap_or((0, "unavailable".to_string()));
153163
let (tokens_week, source) = get_weekly_usage().unwrap_or((0, "unavailable".to_string()));
154164

155165
let daily_budget = config.weekly_token_budget / 7;
156166
let max_for_sustn = daily_budget * (config.max_usage_percent as i64) / 100;
157167
let reserve = daily_budget * (config.reserve_percent as i64) / 100;
158-
let available = (max_for_sustn - tokens_today - reserve).max(0);
168+
let available =
169+
(max_for_sustn - tokens_today - reserve - tokens_reserved).max(0);
159170

160171
BudgetStatus {
161172
weekly_token_budget: config.weekly_token_budget,

src-tauri/src/engine/mod.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,46 @@ pub mod worker;
88
pub mod worktree;
99

1010
use serde::{Deserialize, Serialize};
11-
use std::collections::HashSet;
11+
use std::collections::{HashMap, HashSet};
1212
use std::sync::Arc;
1313
use tauri::Emitter;
1414
use tokio::sync::{Mutex, RwLock};
1515

16+
/// Default number of tasks that can run concurrently.
17+
pub const DEFAULT_CONCURRENCY_LIMIT: usize = 5;
18+
1619
/// Global engine state shared across Tauri commands and the background scheduler.
1720
pub struct EngineState {
1821
/// Whether the engine scheduler loop is running.
1922
pub running: RwLock<bool>,
20-
/// The currently executing task (if any). Only one task runs at a time.
21-
pub current_task: Mutex<Option<CurrentTask>>,
23+
/// Currently executing tasks, keyed by task_id.
24+
pub running_tasks: Mutex<HashMap<String, CurrentTask>>,
2225
/// Handle to cancel the scheduler loop.
2326
pub cancel_token: Mutex<Option<tokio::sync::watch::Sender<bool>>>,
2427
/// Repository IDs that currently have a deep scan in progress.
2528
/// Task execution waits for the scan to finish before starting,
2629
/// preventing concurrent Claude CLI instances in the same repo.
2730
pub deep_scanning_repos: Mutex<HashSet<String>>,
31+
/// Maximum number of tasks that can run concurrently.
32+
pub concurrency_limit: RwLock<usize>,
33+
/// Total tokens reserved by in-flight tasks (prevents over-commit
34+
/// when multiple tasks start near-simultaneously).
35+
pub tokens_reserved: Mutex<i64>,
36+
/// Number of scans currently running. Counted against the concurrency
37+
/// limit so scans and tasks don't over-subscribe Claude CLI.
38+
pub active_scans: Mutex<usize>,
2839
}
2940

3041
impl EngineState {
3142
pub fn new() -> Arc<Self> {
3243
Arc::new(Self {
3344
running: RwLock::new(false),
34-
current_task: Mutex::new(None),
45+
running_tasks: Mutex::new(HashMap::new()),
3546
cancel_token: Mutex::new(None),
3647
deep_scanning_repos: Mutex::new(HashSet::new()),
48+
concurrency_limit: RwLock::new(DEFAULT_CONCURRENCY_LIMIT),
49+
tokens_reserved: Mutex::new(0),
50+
active_scans: Mutex::new(0),
3751
})
3852
}
3953
}

0 commit comments

Comments
 (0)