diff --git a/crates/tracedecay-migrate/src/hermes.rs b/crates/tracedecay-migrate/src/hermes.rs index e327d9b92..fa84e5500 100644 --- a/crates/tracedecay-migrate/src/hermes.rs +++ b/crates/tracedecay-migrate/src/hermes.rs @@ -83,6 +83,12 @@ pub struct LegacyHermesMigrationReport { pub failed: Vec, } +struct HermesMigrationRuntime<'a, R, F, H> { + registry: &'a R, + read_pinned_project_root: &'a F, + state_importer: &'a H, +} + /// Migrates historical stores below the standard user Hermes integration into /// the normal `TraceDecay` user profile. No environment or working-directory /// override can redirect discovery. @@ -146,6 +152,37 @@ where .await } +/// Migrates historical Hermes stores while the caller retains exclusive +/// lifecycle authority for the destination profile. +pub async fn migrate_legacy_hermes_stores_to_with_runtime_under_lease( + user_home: &Path, + tracedecay_profile_root: &Path, + lifecycle: &crate::lifecycle_lease::LifecycleLease, + registry: &R, + read_pinned_project_root: &F, + state_importer: &H, +) -> LegacyHermesMigrationReport +where + R: RegistryRuntime, + F: Fn(&Path) -> Option, + H: HermesStateImporter, +{ + let runtime = HermesMigrationRuntime { + registry, + read_pinned_project_root, + state_importer, + }; + migrate_legacy_hermes_stores_with_lease( + user_home, + tracedecay_profile_root, + &[user_home.join(".hermes")], + None, + lifecycle, + &runtime, + ) + .await +} + #[doc(hidden)] pub async fn migrate_legacy_hermes_stores_inner( user_home: &Path, @@ -161,6 +198,11 @@ where F: Fn(&Path) -> Option, H: HermesStateImporter, { + let runtime = HermesMigrationRuntime { + registry, + read_pinned_project_root, + state_importer, + }; let lifecycle = match crate::lifecycle_lease::acquire_exclusive_for_profile( tracedecay_profile_root, "legacy Hermes store migration", @@ -170,8 +212,32 @@ where return migration_authority_failure(tracedecay_profile_root, error.to_string()); } }; - let _database_scope = match crate::db::enter_maintenance_database_scope( + migrate_legacy_hermes_stores_with_lease( + user_home, + tracedecay_profile_root, + hermes_homes, + fail_after_table, &lifecycle, + &runtime, + ) + .await +} + +async fn migrate_legacy_hermes_stores_with_lease( + user_home: &Path, + tracedecay_profile_root: &Path, + hermes_homes: &[PathBuf], + fail_after_table: Option<&str>, + lifecycle: &crate::lifecycle_lease::LifecycleLease, + runtime: &HermesMigrationRuntime<'_, R, F, H>, +) -> LegacyHermesMigrationReport +where + R: RegistryRuntime, + F: Fn(&Path) -> Option, + H: HermesStateImporter, +{ + let _database_scope = match crate::db::enter_maintenance_database_scope( + lifecycle, tracedecay_profile_root, "legacy Hermes store migration", ) { @@ -190,9 +256,9 @@ where &candidate, tracedecay_profile_root, fail_after_table, - registry, - read_pinned_project_root, - state_importer, + runtime.registry, + runtime.read_pinned_project_root, + runtime.state_importer, ) .await { @@ -201,7 +267,7 @@ where tracedecay_profile_root, candidate.legacy_registry_project_id.as_deref(), &candidate.profile_dir, - registry, + runtime.registry, ) .await { @@ -218,7 +284,7 @@ where tracedecay_profile_root, candidate.legacy_registry_project_id.as_deref(), &candidate.profile_dir, - registry, + runtime.registry, ) .await { @@ -245,7 +311,7 @@ where for profile_dir in profile_dirs { let state_db = profile_dir.join("state.db"); if !state_db.is_file() - || read_pinned_project_root(&profile_dir.join("config.yaml")).is_none() + || (runtime.read_pinned_project_root)(&profile_dir.join("config.yaml")).is_none() { continue; } @@ -254,9 +320,9 @@ where hermes_homes, &profile_dir, tracedecay_profile_root, - registry, - read_pinned_project_root, - state_importer, + runtime.registry, + runtime.read_pinned_project_root, + runtime.state_importer, ) .await { diff --git a/src/agent_cmd.rs b/src/agent_cmd.rs index 80b4dacdb..4d5e7c2bf 100644 --- a/src/agent_cmd.rs +++ b/src/agent_cmd.rs @@ -180,8 +180,17 @@ pub(crate) async fn migrate_legacy_hermes_data(home: &Path) -> tracedecay::error /// stores whose project evidence no longer resolves. Genuine read, integrity, /// or copy failures still block the reinstall; an explicit Hermes install /// continues to use the strict cutover above. -async fn migrate_legacy_hermes_data_for_reinstall(home: &Path) -> tracedecay::errors::Result<()> { - let report = tracedecay::migrate::hermes::migrate_legacy_hermes_stores(home).await; +async fn migrate_legacy_hermes_data_for_reinstall( + home: &Path, + lifecycle: Option<&tracedecay::lifecycle_lease::LifecycleLease>, +) -> tracedecay::errors::Result<()> { + let report = match lifecycle { + Some(lifecycle) => { + tracedecay::migrate::hermes::migrate_legacy_hermes_stores_under_lease(home, lifecycle) + .await + } + None => tracedecay::migrate::hermes::migrate_legacy_hermes_stores(home).await, + }; finish_legacy_hermes_reinstall_migration(report) } @@ -455,11 +464,29 @@ pub(crate) async fn reinstall_agent_integrations( agent_ids: &[String], home: &Path, tracedecay_bin: &str, +) -> Vec<(String, tracedecay::errors::Result<()>)> { + reinstall_agent_integrations_with_lease(agent_ids, home, tracedecay_bin, None).await +} + +pub(crate) async fn reinstall_agent_integrations_under_lease( + agent_ids: &[String], + home: &Path, + tracedecay_bin: &str, + lifecycle: &tracedecay::lifecycle_lease::LifecycleLease, +) -> Vec<(String, tracedecay::errors::Result<()>)> { + reinstall_agent_integrations_with_lease(agent_ids, home, tracedecay_bin, Some(lifecycle)).await +} + +async fn reinstall_agent_integrations_with_lease( + agent_ids: &[String], + home: &Path, + tracedecay_bin: &str, + lifecycle: Option<&tracedecay::lifecycle_lease::LifecycleLease>, ) -> Vec<(String, tracedecay::errors::Result<()>)> { let project_path = std::env::current_dir().ok(); let mut results = Vec::new(); let hermes_migration_error = if agent_ids.iter().any(|id| id == "hermes") { - migrate_legacy_hermes_data_for_reinstall(home) + migrate_legacy_hermes_data_for_reinstall(home, lifecycle) .await .err() .map(|error| error.to_string()) diff --git a/src/migrate/hermes/tests.rs b/src/migrate/hermes/tests.rs index 5e13f42b6..b50d3d179 100644 --- a/src/migrate/hermes/tests.rs +++ b/src/migrate/hermes/tests.rs @@ -256,6 +256,23 @@ async fn migrates_standard_profile_store_once() { assert_eq!(count(source_after.conn(), "sessions").await, 1); } +#[tokio::test] +async fn post_update_migration_reuses_its_exclusive_profile_lease() { + let temp = tempfile::tempdir().unwrap(); + let user_home = temp.path().join("home"); + let profile_root = temp.path().join("tracedecay-profile"); + let lifecycle = crate::lifecycle_lease::acquire_exclusive_for_profile( + &profile_root, + "post-update maintenance", + ) + .unwrap(); + + let report = + migrate_legacy_hermes_stores_to_under_lease(&user_home, &profile_root, &lifecycle).await; + + assert!(report.failed.is_empty(), "{report:?}"); +} + #[tokio::test] async fn migration_marker_remerges_when_a_target_row_is_missing() { let temp = tempfile::tempdir().unwrap(); diff --git a/src/migrate/mod.rs b/src/migrate/mod.rs index 928d60709..038eb78b0 100644 --- a/src/migrate/mod.rs +++ b/src/migrate/mod.rs @@ -276,6 +276,38 @@ pub mod hermes { .await } + pub async fn migrate_legacy_hermes_stores_under_lease( + user_home: &Path, + lifecycle: &crate::lifecycle_lease::LifecycleLease, + ) -> LegacyHermesMigrationReport { + let Ok(profile_root) = crate::storage::default_profile_root() else { + return LegacyHermesMigrationReport { + failed: vec![LegacyHermesMigrationIssue { + source_db: user_home.join(".hermes/.tracedecay/sessions.db"), + reason: "could not resolve the TraceDecay user-profile store".to_string(), + }], + ..LegacyHermesMigrationReport::default() + }; + }; + migrate_legacy_hermes_stores_to_under_lease(user_home, &profile_root, lifecycle).await + } + + pub async fn migrate_legacy_hermes_stores_to_under_lease( + user_home: &Path, + tracedecay_profile_root: &Path, + lifecycle: &crate::lifecycle_lease::LifecycleLease, + ) -> LegacyHermesMigrationReport { + tracedecay_migrate::hermes::migrate_legacy_hermes_stores_to_with_runtime_under_lease( + user_home, + tracedecay_profile_root, + lifecycle, + &RootRegistry, + &crate::agents::hermes::read_config_pinned_project_root, + &RootHermesStateImporter, + ) + .await + } + fn code_project(project: CodeProjectRecord) -> registry_adapter::CodeProjectRecord { registry_adapter::CodeProjectRecord { project_id: project.project_id, diff --git a/src/update_cmd.rs b/src/update_cmd.rs index 883854ff2..354d206bc 100644 --- a/src/update_cmd.rs +++ b/src/update_cmd.rs @@ -472,6 +472,20 @@ pub(crate) fn partition_reinstall_results( /// resolved, no install runs and a descriptive failure is reported so the /// version markers stay put. pub(crate) async fn reinstall_tracked_agents(user_config: &UserConfig) -> ReinstallOutcome { + reinstall_tracked_agents_with_lease(user_config, None).await +} + +async fn reinstall_tracked_agents_under_lease( + user_config: &UserConfig, + lifecycle_lease: &tracedecay::lifecycle_lease::LifecycleLease, +) -> ReinstallOutcome { + reinstall_tracked_agents_with_lease(user_config, Some(lifecycle_lease)).await +} + +async fn reinstall_tracked_agents_with_lease( + user_config: &UserConfig, + lifecycle_lease: Option<&tracedecay::lifecycle_lease::LifecycleLease>, +) -> ReinstallOutcome { let (Some(home), Some(bin)) = ( tracedecay::agents::home_dir(), tracedecay::agents::which_tracedecay(), @@ -483,9 +497,25 @@ pub(crate) async fn reinstall_tracked_agents(user_config: &UserConfig) -> Reinst ], }; }; - let results = - crate::agent_cmd::reinstall_agent_integrations(&user_config.installed_agents, &home, &bin) - .await; + let results = match lifecycle_lease { + Some(lifecycle_lease) => { + crate::agent_cmd::reinstall_agent_integrations_under_lease( + &user_config.installed_agents, + &home, + &bin, + lifecycle_lease, + ) + .await + } + None => { + crate::agent_cmd::reinstall_agent_integrations( + &user_config.installed_agents, + &home, + &bin, + ) + .await + } + }; partition_reinstall_results(results) } @@ -569,7 +599,7 @@ async fn run_post_update_mutations( config.installed_agents.join(", ") ); } - match reinstall_tracked_agents(&config).await { + match reinstall_tracked_agents_under_lease(&config, lifecycle_lease).await { ReinstallOutcome::AllOk => { if config.mark_version_installed(env!("CARGO_PKG_VERSION")) { if let Err(err) = config.save() {