From d84c64fa0ea9290375aead1d00f0099ba441cc59 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 10 Apr 2026 08:03:25 -0700 Subject: [PATCH] feat: canonicalize ecc2 harness aliases --- ecc2/src/session/manager.rs | 91 +++++++++++++++++++++++++++++++++++-- ecc2/src/session/mod.rs | 21 +++++++++ ecc2/src/session/store.rs | 24 +++++++--- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 4ecd5569..e36e2318 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -2103,11 +2103,12 @@ async fn queue_session_in_dir_with_runner_program( grouping: SessionGrouping, ) -> Result { let profile = resolve_launch_profile(db, cfg, profile_name, inherited_profile_session_id)?; + let canonical_agent_type = HarnessKind::canonical_agent_type(agent_type); queue_session_with_resolved_profile_and_runner_program( db, cfg, task, - agent_type, + &canonical_agent_type, use_worktree, repo_root, runner_program, @@ -2132,10 +2133,11 @@ async fn queue_session_with_resolved_profile_and_runner_program( .as_ref() .and_then(|profile| profile.agent.as_deref()) .unwrap_or(agent_type); + let effective_agent_type = HarnessKind::canonical_agent_type(effective_agent_type); let session = build_session_record( db, task, - effective_agent_type, + &effective_agent_type, use_worktree, cfg, repo_root, @@ -2188,6 +2190,7 @@ fn build_session_record( repo_root: &Path, grouping: SessionGrouping, ) -> Result { + let canonical_agent_type = HarnessKind::canonical_agent_type(agent_type); let id = uuid::Uuid::new_v4().to_string()[..8].to_string(); let now = chrono::Utc::now(); @@ -2216,7 +2219,7 @@ fn build_session_record( task: task.to_string(), project, task_group, - agent_type: agent_type.to_string(), + agent_type: canonical_agent_type, working_dir, state: SessionState::Pending, pid: None, @@ -2341,13 +2344,18 @@ fn direct_delegate_sessions( lead_id: &str, agent_type: &str, ) -> Result> { + let target_harness = HarnessKind::from_agent_type(agent_type); let mut sessions = Vec::new(); for child_id in db.delegated_children(lead_id, 50)? { let Some(session) = db.get_session(&child_id)? else { continue; }; - if session.agent_type != agent_type { + if target_harness != HarnessKind::Unknown { + if HarnessKind::from_agent_type(&session.agent_type) != target_harness { + continue; + } + } else if session.agent_type != HarnessKind::canonical_agent_type(agent_type) { continue; } @@ -3575,6 +3583,81 @@ mod tests { ); } + #[test] + fn build_session_record_canonicalizes_known_agent_aliases() -> Result<()> { + let tempdir = TestDir::new("manager-canonical-agent-type")?; + let repo_root = tempdir.path().join("repo"); + init_git_repo(&repo_root)?; + + let cfg = build_config(tempdir.path()); + let db = StateStore::open(&cfg.db_path)?; + let session = build_session_record( + &db, + "Investigate auth callback", + "gemini-cli", + false, + &cfg, + &repo_root, + SessionGrouping::default(), + )?; + + assert_eq!(session.agent_type, "gemini"); + Ok(()) + } + + #[test] + fn direct_delegate_sessions_matches_harness_aliases_for_existing_rows() -> Result<()> { + let tempdir = TestDir::new("manager-delegate-alias-match")?; + let repo_root = tempdir.path().join("repo"); + init_git_repo(&repo_root)?; + + let cfg = build_config(tempdir.path()); + let db = StateStore::open(&cfg.db_path)?; + let now = Utc::now(); + + db.insert_session(&Session { + id: "lead".to_string(), + task: "Lead task".to_string(), + project: "workspace".to_string(), + task_group: "general".to_string(), + agent_type: "claude".to_string(), + working_dir: repo_root.clone(), + state: SessionState::Running, + pid: Some(42), + worktree: None, + created_at: now, + updated_at: now, + last_heartbeat_at: now, + metrics: SessionMetrics::default(), + })?; + db.insert_session(&Session { + id: "child".to_string(), + task: "Delegate task".to_string(), + project: "workspace".to_string(), + task_group: "general".to_string(), + agent_type: "claude-code".to_string(), + working_dir: repo_root.clone(), + state: SessionState::Idle, + pid: Some(7), + worktree: None, + created_at: now, + updated_at: now, + last_heartbeat_at: now, + metrics: SessionMetrics::default(), + })?; + db.send_message( + "lead", + "child", + "{\"task\":\"Delegate task\",\"context\":\"Delegated from lead\"}", + "task_handoff", + )?; + + let delegates = direct_delegate_sessions(&db, "lead", "claude")?; + assert_eq!(delegates.len(), 1); + assert_eq!(delegates[0].id, "child"); + Ok(()) + } + #[test] fn enforce_session_heartbeats_marks_overdue_running_sessions_stale() -> Result<()> { let tempdir = TestDir::new("manager-heartbeat-stale")?; diff --git a/ecc2/src/session/mod.rs b/ecc2/src/session/mod.rs index 0a1aa292..8ad6c54a 100644 --- a/ecc2/src/session/mod.rs +++ b/ecc2/src/session/mod.rs @@ -79,6 +79,13 @@ impl HarnessKind { } } + pub fn canonical_agent_type(agent_type: &str) -> String { + match Self::from_agent_type(agent_type) { + Self::Unknown => agent_type.trim().to_ascii_lowercase(), + harness => harness.as_str().to_string(), + } + } + fn project_markers(self) -> &'static [&'static str] { match self { Self::Claude => &[".claude"], @@ -505,4 +512,18 @@ mod tests { assert_eq!(harness.detected, vec![HarnessKind::Gemini]); Ok(()) } + + #[test] + fn canonical_agent_type_normalizes_known_aliases() { + assert_eq!(HarnessKind::canonical_agent_type("claude-code"), "claude"); + assert_eq!(HarnessKind::canonical_agent_type("gemini-cli"), "gemini"); + assert_eq!( + HarnessKind::canonical_agent_type("factory-droid"), + "factory_droid" + ); + assert_eq!( + HarnessKind::canonical_agent_type(" custom-runner "), + "custom-runner" + ); + } } diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index 5109bef8..fdebe58d 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -675,15 +675,23 @@ impl StateStore { .collect::, _>>()?; for (session_id, agent_type, working_dir) in updates { - let harness = SessionHarnessInfo::detect(&agent_type, Path::new(&working_dir)); + let canonical_agent_type = HarnessKind::canonical_agent_type(&agent_type); + let harness = + SessionHarnessInfo::detect(&canonical_agent_type, Path::new(&working_dir)); let detected_json = serde_json::to_string(&harness.detected).context("serialize detected harnesses")?; self.conn.execute( "UPDATE sessions - SET harness = ?2, - detected_harnesses_json = ?3 + SET agent_type = ?2, + harness = ?3, + detected_harnesses_json = ?4 WHERE id = ?1", - rusqlite::params![session_id, harness.primary.to_string(), detected_json], + rusqlite::params![ + session_id, + canonical_agent_type, + harness.primary.to_string(), + detected_json + ], )?; } @@ -3968,7 +3976,7 @@ mod tests { "Backfill harness metadata", "ecc", "legacy", - "claude", + "gemini-cli", repo_root.display().to_string(), now, ], @@ -3976,10 +3984,14 @@ mod tests { drop(conn); let db = StateStore::open(&db_path)?; + let session = db + .get_session("sess-legacy")? + .expect("legacy row should still exist"); + assert_eq!(session.agent_type, "gemini"); let harness = db .get_session_harness_info("sess-legacy")? .expect("legacy row should be backfilled"); - assert_eq!(harness.primary, HarnessKind::Claude); + assert_eq!(harness.primary, HarnessKind::Gemini); assert_eq!(harness.detected, vec![HarnessKind::Codex]); Ok(()) }