mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 19:03:28 +08:00
feat: auto-prune inactive ecc2 worktrees
This commit is contained in:
@@ -37,6 +37,10 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
tracing::error!("Worktree auto-merge pass failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = maybe_auto_prune_inactive_worktrees(&db).await {
|
||||
tracing::error!("Worktree auto-prune pass failed: {e}");
|
||||
}
|
||||
|
||||
time::sleep(heartbeat_interval).await;
|
||||
}
|
||||
}
|
||||
@@ -404,6 +408,46 @@ where
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
async fn maybe_auto_prune_inactive_worktrees(db: &StateStore) -> Result<usize> {
|
||||
maybe_auto_prune_inactive_worktrees_with_recorder(
|
||||
|| manager::prune_inactive_worktrees(db),
|
||||
|pruned, active| db.record_daemon_auto_prune_pass(pruned, active),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn maybe_auto_prune_inactive_worktrees_with<F, Fut>(prune: F) -> Result<usize>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<manager::WorktreePruneOutcome>>,
|
||||
{
|
||||
maybe_auto_prune_inactive_worktrees_with_recorder(prune, |_, _| Ok(())).await
|
||||
}
|
||||
|
||||
async fn maybe_auto_prune_inactive_worktrees_with_recorder<F, Fut, R>(
|
||||
prune: F,
|
||||
mut record: R,
|
||||
) -> Result<usize>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<manager::WorktreePruneOutcome>>,
|
||||
R: FnMut(usize, usize) -> Result<()>,
|
||||
{
|
||||
let outcome = prune().await?;
|
||||
let pruned = outcome.cleaned_session_ids.len();
|
||||
let active = outcome.active_with_worktree_ids.len();
|
||||
record(pruned, active)?;
|
||||
|
||||
if pruned > 0 {
|
||||
tracing::info!("Auto-pruned {pruned} inactive worktree(s)");
|
||||
}
|
||||
if active > 0 {
|
||||
tracing::info!("Skipped {active} active worktree(s) during auto-prune");
|
||||
}
|
||||
|
||||
Ok(pruned)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn pid_is_alive(pid: u32) -> bool {
|
||||
if pid == 0 {
|
||||
@@ -769,6 +813,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
let order = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let dispatch_order = order.clone();
|
||||
@@ -832,6 +879,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
let recorded = std::sync::Arc::new(std::sync::Mutex::new(None));
|
||||
let recorded_clone = recorded.clone();
|
||||
@@ -887,6 +937,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
@@ -943,6 +996,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let calls_clone = calls.clone();
|
||||
@@ -999,6 +1055,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
let rebalance_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let rebalance_calls_clone = rebalance_calls.clone();
|
||||
@@ -1199,4 +1258,28 @@ mod tests {
|
||||
assert_eq!(merged, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn maybe_auto_prune_inactive_worktrees_records_pruned_and_active_counts() -> Result<()> {
|
||||
let recorded = std::sync::Arc::new(std::sync::Mutex::new(None));
|
||||
let recorded_clone = recorded.clone();
|
||||
|
||||
let pruned = maybe_auto_prune_inactive_worktrees_with_recorder(
|
||||
|| async move {
|
||||
Ok(manager::WorktreePruneOutcome {
|
||||
cleaned_session_ids: vec!["stopped-a".to_string(), "stopped-b".to_string()],
|
||||
active_with_worktree_ids: vec!["running-a".to_string()],
|
||||
})
|
||||
},
|
||||
move |pruned, active| {
|
||||
*recorded_clone.lock().unwrap() = Some((pruned, active));
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(pruned, 2);
|
||||
assert_eq!(*recorded.lock().unwrap(), Some((2, 1)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,6 +1585,16 @@ impl fmt::Display for CoordinationStatus {
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(last_auto_prune_at) = self.daemon_activity.last_auto_prune_at.as_ref() {
|
||||
writeln!(
|
||||
f,
|
||||
"Last daemon auto-prune: {} pruned / {} active @ {}",
|
||||
self.daemon_activity.last_auto_prune_pruned,
|
||||
self.daemon_activity.last_auto_prune_active_skipped,
|
||||
last_auto_prune_at.to_rfc3339()
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1693,6 +1703,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: Some(now),
|
||||
last_auto_prune_pruned: 2,
|
||||
last_auto_prune_active_skipped: 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3171,6 +3184,7 @@ mod tests {
|
||||
assert!(rendered.contains(
|
||||
"Last daemon auto-merge: 1 merged / 1 active / 0 conflicted / 0 dirty / 0 failed"
|
||||
));
|
||||
assert!(rendered.contains("Last daemon auto-prune: 2 pruned / 1 active"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -33,6 +33,9 @@ pub struct DaemonActivity {
|
||||
pub last_auto_merge_conflicted_skipped: usize,
|
||||
pub last_auto_merge_dirty_skipped: usize,
|
||||
pub last_auto_merge_failed: usize,
|
||||
pub last_auto_prune_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub last_auto_prune_pruned: usize,
|
||||
pub last_auto_prune_active_skipped: usize,
|
||||
}
|
||||
|
||||
impl DaemonActivity {
|
||||
@@ -174,7 +177,10 @@ impl StateStore {
|
||||
last_auto_merge_active_skipped INTEGER NOT NULL DEFAULT 0,
|
||||
last_auto_merge_conflicted_skipped INTEGER NOT NULL DEFAULT 0,
|
||||
last_auto_merge_dirty_skipped INTEGER NOT NULL DEFAULT 0,
|
||||
last_auto_merge_failed INTEGER NOT NULL DEFAULT 0
|
||||
last_auto_merge_failed INTEGER NOT NULL DEFAULT 0,
|
||||
last_auto_prune_at TEXT,
|
||||
last_auto_prune_pruned INTEGER NOT NULL DEFAULT 0,
|
||||
last_auto_prune_active_skipped INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_state ON sessions(state);
|
||||
@@ -307,6 +313,33 @@ impl StateStore {
|
||||
.context("Failed to add last_auto_merge_failed column to daemon_activity table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("daemon_activity", "last_auto_prune_at")? {
|
||||
self.conn
|
||||
.execute(
|
||||
"ALTER TABLE daemon_activity ADD COLUMN last_auto_prune_at TEXT",
|
||||
[],
|
||||
)
|
||||
.context("Failed to add last_auto_prune_at column to daemon_activity table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("daemon_activity", "last_auto_prune_pruned")? {
|
||||
self.conn
|
||||
.execute(
|
||||
"ALTER TABLE daemon_activity ADD COLUMN last_auto_prune_pruned INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)
|
||||
.context("Failed to add last_auto_prune_pruned column to daemon_activity table")?;
|
||||
}
|
||||
|
||||
if !self.has_column("daemon_activity", "last_auto_prune_active_skipped")? {
|
||||
self.conn
|
||||
.execute(
|
||||
"ALTER TABLE daemon_activity ADD COLUMN last_auto_prune_active_skipped INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)
|
||||
.context("Failed to add last_auto_prune_active_skipped column to daemon_activity table")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -712,7 +745,8 @@ impl StateStore {
|
||||
last_rebalance_at, last_rebalance_rerouted, last_rebalance_leads,
|
||||
last_auto_merge_at, last_auto_merge_merged, last_auto_merge_active_skipped,
|
||||
last_auto_merge_conflicted_skipped, last_auto_merge_dirty_skipped,
|
||||
last_auto_merge_failed
|
||||
last_auto_merge_failed, last_auto_prune_at, last_auto_prune_pruned,
|
||||
last_auto_prune_active_skipped
|
||||
FROM daemon_activity
|
||||
WHERE id = 1",
|
||||
[],
|
||||
@@ -752,6 +786,9 @@ impl StateStore {
|
||||
last_auto_merge_conflicted_skipped: row.get::<_, i64>(14)? as usize,
|
||||
last_auto_merge_dirty_skipped: row.get::<_, i64>(15)? as usize,
|
||||
last_auto_merge_failed: row.get::<_, i64>(16)? as usize,
|
||||
last_auto_prune_at: parse_ts(row.get(17)?)?,
|
||||
last_auto_prune_pruned: row.get::<_, i64>(18)? as usize,
|
||||
last_auto_prune_active_skipped: row.get::<_, i64>(19)? as usize,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -847,6 +884,27 @@ impl StateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_daemon_auto_prune_pass(
|
||||
&self,
|
||||
pruned: usize,
|
||||
active_skipped: usize,
|
||||
) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE daemon_activity
|
||||
SET last_auto_prune_at = ?1,
|
||||
last_auto_prune_pruned = ?2,
|
||||
last_auto_prune_active_skipped = ?3
|
||||
WHERE id = 1",
|
||||
rusqlite::params![
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
pruned as i64,
|
||||
active_skipped as i64,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delegated_children(&self, session_id: &str, limit: usize) -> Result<Vec<String>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT to_session
|
||||
@@ -1223,6 +1281,7 @@ mod tests {
|
||||
db.record_daemon_recovery_dispatch_pass(2, 1)?;
|
||||
db.record_daemon_rebalance_pass(3, 1)?;
|
||||
db.record_daemon_auto_merge_pass(2, 1, 1, 1, 0)?;
|
||||
db.record_daemon_auto_prune_pass(3, 1)?;
|
||||
|
||||
let activity = db.daemon_activity()?;
|
||||
assert_eq!(activity.last_dispatch_routed, 4);
|
||||
@@ -1238,10 +1297,13 @@ mod tests {
|
||||
assert_eq!(activity.last_auto_merge_conflicted_skipped, 1);
|
||||
assert_eq!(activity.last_auto_merge_dirty_skipped, 1);
|
||||
assert_eq!(activity.last_auto_merge_failed, 0);
|
||||
assert_eq!(activity.last_auto_prune_pruned, 3);
|
||||
assert_eq!(activity.last_auto_prune_active_skipped, 1);
|
||||
assert!(activity.last_dispatch_at.is_some());
|
||||
assert!(activity.last_recovery_dispatch_at.is_some());
|
||||
assert!(activity.last_rebalance_at.is_some());
|
||||
assert!(activity.last_auto_merge_at.is_some());
|
||||
assert!(activity.last_auto_prune_at.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1274,6 +1336,9 @@ mod tests {
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
assert!(unresolved.prefers_rebalance_first());
|
||||
assert!(unresolved.dispatch_cooloff_active());
|
||||
|
||||
@@ -2029,6 +2029,15 @@ impl Dashboard {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(last_auto_prune_at) = self.daemon_activity.last_auto_prune_at.as_ref() {
|
||||
lines.push(format!(
|
||||
"Last daemon auto-prune {} pruned / {} active @ {}",
|
||||
self.daemon_activity.last_auto_prune_pruned,
|
||||
self.daemon_activity.last_auto_prune_active_skipped,
|
||||
self.short_timestamp(&last_auto_prune_at.to_rfc3339())
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(route_preview) = self.selected_route_preview.as_ref() {
|
||||
lines.push(format!("Next route {route_preview}"));
|
||||
}
|
||||
@@ -3041,6 +3050,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 1,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: Some(now + chrono::Duration::seconds(4)),
|
||||
last_auto_prune_pruned: 3,
|
||||
last_auto_prune_active_skipped: 1,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3052,6 +3064,7 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
assert!(text.contains(
|
||||
"Last daemon auto-merge 2 merged / 1 active / 1 conflicted / 0 dirty / 0 failed"
|
||||
));
|
||||
assert!(text.contains("Last daemon auto-prune 3 pruned / 1 active"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3085,6 +3098,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3122,6 +3138,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3161,6 +3180,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3201,6 +3223,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3256,6 +3281,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
@@ -3358,6 +3386,9 @@ diff --git a/src/next.rs b/src/next.rs
|
||||
last_auto_merge_conflicted_skipped: 0,
|
||||
last_auto_merge_dirty_skipped: 0,
|
||||
last_auto_merge_failed: 0,
|
||||
last_auto_prune_at: None,
|
||||
last_auto_prune_pruned: 0,
|
||||
last_auto_prune_active_skipped: 0,
|
||||
};
|
||||
|
||||
let text = dashboard.selected_session_metrics_text();
|
||||
|
||||
Reference in New Issue
Block a user