diff --git a/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs b/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs index 9d3436429a..275b1f2ca6 100644 --- a/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs +++ b/crates/tracedecay-automation-runtime/src/automation/memory_curator.rs @@ -20,8 +20,7 @@ use super::lifecycle::{ failed_backend_fallback_report, }; use super::run_ledger::{AutomationRunLedgerRecord, AutomationTrigger}; -use crate::ports::project_runtime::ProfileRuntime; -use crate::ports::project_runtime::TraceDecay; +use crate::ports::project_runtime::{AutomationProjectContext, ProfileRuntime}; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; use tracedecay_policy::{ @@ -81,14 +80,14 @@ pub struct MemoryCuratorAutomationRun { } pub async fn run_memory_curator_with_backend( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: MemoryCuratorAutomationOptions, run_control: &AutomationRunControl, ) -> AutomationRunResult { - let sessions_db = super::runner::project_automation_sessions(cg).await?; + let sessions_db = super::runner::project_automation_sessions(cg); run_memory_curator_for_store_with_publication( MemoryCuratorStore::Project { cg, sessions_db }, config, @@ -107,7 +106,7 @@ pub async fn run_memory_curator_with_backend( /// Runs one admitted retained Memory Curator effect without publishing its /// ledger terminal before the daemon accepts the outer application terminal. pub async fn run_memory_curator_with_backend_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -115,24 +114,20 @@ pub async fn run_memory_curator_with_backend_for_retained_settlement( run_control: &AutomationRunControl, ) -> RetainedAutomationRun { let settlement_guard = AutomationRunSettlementGuard::new(); - let result = match super::runner::project_automation_sessions(cg).await { - Ok(sessions_db) => { - run_memory_curator_for_store_with_publication( - MemoryCuratorStore::Project { cg, sessions_db }, - config, - configuration_revision_id, - backend, - options, - run_control, - AutomationRunPublication { - ledger: AutomationRunLedgerPublication::DeferredUntilApplicationSettlement, - settlement_guard: Some(&settlement_guard), - }, - ) - .await - } - Err(error) => Err(error.into()), - }; + let sessions_db = super::runner::project_automation_sessions(cg); + let result = run_memory_curator_for_store_with_publication( + MemoryCuratorStore::Project { cg, sessions_db }, + config, + configuration_revision_id, + backend, + options, + run_control, + AutomationRunPublication { + ledger: AutomationRunLedgerPublication::DeferredUntilApplicationSettlement, + settlement_guard: Some(&settlement_guard), + }, + ) + .await; RetainedAutomationRun::new(result, settlement_guard) } @@ -168,7 +163,7 @@ pub(crate) async fn run_user_memory_curator_with_backend( enum MemoryCuratorStore<'a> { Project { - cg: &'a TraceDecay, + cg: &'a AutomationProjectContext, sessions_db: RegisteredGlobalDbLeaseV1, }, User { @@ -181,7 +176,7 @@ enum MemoryCuratorStore<'a> { impl MemoryCuratorStore<'_> { fn dashboard_root(&self) -> std::path::PathBuf { match self { - Self::Project { cg, .. } => cg.store_layout().dashboard_root.clone(), + Self::Project { cg, .. } => cg.dashboard_root.clone(), Self::User { profile_root, .. } => super::runner::user_automation_root(profile_root), } } @@ -196,7 +191,9 @@ impl MemoryCuratorStore<'_> { fn owner(&self) -> Result { match self { - Self::Project { cg, .. } => cg.project_memory_owner(), + Self::Project { cg, .. } => Ok(FactOwnerV1::Project { + project_id: cg.project_id.clone(), + }), Self::User { .. } => Ok(FactOwnerV1::Profile), } } @@ -207,17 +204,7 @@ impl MemoryCuratorStore<'_> { ) -> Result { let actor_id = ActorId::new("automation:memory-curator").map_err(memory_contract_error)?; let (project_id, profile_id) = match self { - Self::Project { cg, .. } => { - let project_id = match cg.project_memory_owner()? { - FactOwnerV1::Project { project_id } => project_id, - FactOwnerV1::Profile => { - return Err(memory_validation_error( - "project memory curator is missing project authority", - )); - } - }; - (Some(project_id), cg.profile_id().clone()) - } + Self::Project { cg, .. } => (Some(cg.project_id.clone()), cg.profile_id.clone()), Self::User { runtime, .. } => (None, runtime.profile_id().clone()), }; Ok(CurationApplyAuthorityV1 { @@ -230,7 +217,7 @@ impl MemoryCuratorStore<'_> { async fn open_memory_database(&self) -> Result { match self { - Self::Project { cg, .. } => cg.open_project_store_db().await, + Self::Project { cg, .. } => Ok(cg.project_memory_database.clone()), Self::User { runtime, .. } => runtime.open_user_memory_db().await, } } diff --git a/crates/tracedecay-automation-runtime/src/automation/runner.rs b/crates/tracedecay-automation-runtime/src/automation/runner.rs index 2f8ed0e6cc..d15d3d6b10 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner.rs @@ -27,8 +27,7 @@ use super::skill_writer::{ activation_policy as skill_writer_activation_policy, validate_and_apply_skill_proposals, validate_skill_proposals, }; -use crate::ports::project_runtime::ProfileRuntime; -use crate::ports::project_runtime::TraceDecay; +use crate::ports::project_runtime::{AutomationProjectContext, ProfileRuntime}; use crate::ports::session_store::AutomationSessionStore; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::{RegisteredGlobalDb, RegisteredGlobalDbLeaseV1}; @@ -52,7 +51,7 @@ use evidence::{ SkillWriterEvidenceOutcome, build_session_reflector_evidence, build_skill_writer_evidence, canonical_evidence_hash, }; -use retrieval::{production_project_automation_retrieval, production_user_automation_retrieval}; +use retrieval::{production_user_automation_retrieval, unavailable_automation_retrieval}; use session_reflector::{ ProposedAgentOutput, SessionReflectorFinalization, build_session_reflector_prompt, finalize_session_reflector_success, validate_session_fact_candidates, @@ -101,35 +100,24 @@ pub fn user_automation_root(profile_root: &std::path::Path) -> PathBuf { profile_root.join(USER_AUTOMATION_DIR) } -pub(super) async fn project_automation_sessions( - cg: &TraceDecay, -) -> Result { - let FactOwnerV1::Project { project_id } = cg.project_memory_owner()? else { - return Err(TraceDecayError::Config { - message: "project automation requires authoritative project session scope".to_string(), - }); - }; - cg.project_sessions(project_id, vec![cg.store_layout().project_root.clone()]) - .await +pub(super) fn project_automation_sessions( + context: &AutomationProjectContext, +) -> RegisteredGlobalDbLeaseV1 { + context.project_sessions.clone() } fn project_curation_authority( - cg: &TraceDecay, + context: &AutomationProjectContext, actor: &'static str, configuration_revision_id: &ConfigurationRevisionId, ) -> Result { - let FactOwnerV1::Project { project_id } = cg.project_memory_owner()? else { - return Err(TraceDecayError::Config { - message: "project curation requires authoritative project scope".to_owned(), - }); - }; let actor_id = ActorId::new(actor).map_err(|error| TraceDecayError::Config { message: format!("invalid curation actor identity: {error}"), })?; Ok(CurationApplyAuthorityV1 { actor_id, - project_id: Some(project_id), - profile_id: cg.profile_id().clone(), + project_id: Some(context.project_id.clone()), + profile_id: context.profile_id.clone(), configuration_revision_id: configuration_revision_id.clone(), }) } @@ -423,14 +411,14 @@ impl RetainedCombinedReviewRun { /// `prompt_version` set to the combined contract's version. #[hotpath::measure(label = "automation.run.combined_review", future = true)] pub async fn run_combined_review_with_backend( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: CombinedReviewAutomationOptions, run_control: &AutomationRunControl, ) -> Result { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_combined_review_for_retrieval( cg, config, @@ -451,7 +439,7 @@ pub async fn run_combined_review_with_backend( } pub async fn run_combined_review_with_backend_and_retrieval( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -476,14 +464,14 @@ pub async fn run_combined_review_with_backend_and_retrieval( } pub async fn run_combined_review_with_backend_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: CombinedReviewAutomationOptions, run_control: &AutomationRunControl, ) -> RetainedCombinedReviewRun { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_combined_review_with_backend_and_retrieval_for_retained_settlement( cg, config, @@ -498,7 +486,7 @@ pub async fn run_combined_review_with_backend_for_retained_settlement( #[allow(clippy::too_many_arguments)] pub async fn run_combined_review_with_backend_and_retrieval_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -585,7 +573,7 @@ fn combined_skill_writer_evidence_or_not_combined( #[hotpath::measure(future = true, label = "automation.run.combined_review.inner")] async fn run_combined_review_for_retrieval( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, io: AutomationTaskIo<'_>, @@ -610,7 +598,7 @@ async fn run_combined_review_for_retrieval( /// rather than the inlined review state machine. #[allow(clippy::too_many_arguments)] fn run_combined_review_for_retrieval_inner<'a>( - cg: &'a TraceDecay, + cg: &'a AutomationProjectContext, config: &'a AutomationConfig, configuration_revision_id: &'a ConfigurationRevisionId, io: AutomationTaskIo<'a>, @@ -640,8 +628,8 @@ fn run_combined_review_for_retrieval_inner<'a>( reason: "combined_mode_disabled", }); } - let dashboard_root = cg.store_layout().dashboard_root.clone(); - let sessions_db = project_automation_sessions(cg).await?; + let dashboard_root = cg.dashboard_root.clone(); + let sessions_db = project_automation_sessions(cg); let _reflector_lock = match acquire_combined_task_lock( config, &dashboard_root, @@ -670,10 +658,11 @@ fn run_combined_review_for_retrieval_inner<'a>( Ok(lock) => lock, Err(dispatch) => return Ok(dispatch), }; - let project_memory_db = cg.open_project_store_db().await?; let memory = MemoryApplication::new( - cg.project_memory_owner()?, - DatabaseFactStore::new(&project_memory_db), + FactOwnerV1::Project { + project_id: cg.project_id.clone(), + }, + DatabaseFactStore::new(&cg.project_memory_database), ) .map_err(|error| TraceDecayError::Config { message: format!("could not initialize combined review memory authority: {error}"), @@ -689,8 +678,8 @@ fn run_combined_review_for_retrieval_inner<'a>( let skill_bundle = match combined_skill_writer_evidence_or_not_combined( build_skill_writer_evidence( retrieval, - Some(cg.project_root()), - Some(cg.profile_database().as_ref()), + Some(&cg.project_root), + Some(cg.profile_database.as_ref()), options.skill_writer, ) .await?, @@ -1039,10 +1028,10 @@ fn run_combined_review_for_retrieval_inner<'a>( let (skill_report, skill_record, skill_committed_receipt) = match finalize_skill_writer_success( - &cg.host_io(), + &cg.host_io, &skill_finalizer, &skill_bundle.profile_root, - Some(cg.store_layout().project_root.as_path()), + Some(&cg.project_root), config, &skill_authority, activation_policy, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs index 0736265c64..53ea0f8e20 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/retrieval.rs @@ -26,7 +26,6 @@ use tracedecay_domain::{ use tracedecay_store::{StoreShardIdV1, StoreShardScopeV1}; use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; -use crate::ports::project_runtime::TraceDecay; use crate::ports::session_evidence::LcmScope; use tracedecay_contracts::request_identity::{GlobalRequestSurface, mint_global_request_id}; use tracedecay_domain::errors::{Result, TraceDecayError}; @@ -768,13 +767,9 @@ pub async fn registered_project_automation_retrieval( Ok(registered_automation_retrieval_for_identity(database, identity).await) } -pub(super) async fn production_project_automation_retrieval( - _cg: &TraceDecay, +pub(super) fn unavailable_automation_retrieval( + reason: &'static str, ) -> Box { - unavailable_automation_retrieval("session_evidence_retrieval_unavailable") -} - -fn unavailable_automation_retrieval(reason: &'static str) -> Box { // The static fallback session id is a fixed, valid identifier. #[allow(clippy::expect_used)] Box::new(UnavailableAutomationSessionRetrieval { diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs index 04818b9b9c..b923557ed1 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/session_reflector.rs @@ -20,8 +20,9 @@ use crate::automation::lifecycle::{ }; use crate::automation::run_ledger::{AutomationRunLedgerRecord, AutomationTrigger}; use crate::automation::session_reflector::validate_fact_candidates; -use crate::ports::project_runtime::TraceDecay; +use crate::ports::project_runtime::AutomationProjectContext; use crate::ports::session_evidence::{LcmGrepSort, LcmScope}; +use tracedecay_domain::FactOwnerV1; use tracedecay_domain::configuration::ConfigurationRevisionId; use tracedecay_domain::errors::{Result, TraceDecayError}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; @@ -34,7 +35,7 @@ use super::evidence::{ SessionReflectorEvidenceBundle, SessionReflectorEvidenceOutcome, build_session_reflector_evidence, }; -use super::retrieval::{AutomationSessionRetrieval, production_project_automation_retrieval}; +use super::retrieval::{AutomationSessionRetrieval, unavailable_automation_retrieval}; mod privacy; use privacy::{ @@ -963,7 +964,7 @@ fn run_session_reflector_for_store_with_publication_inner<'a, A: ProjectMemoryFa } pub async fn run_session_reflector_with_backend_and_retrieval( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, run_control: &AutomationRunControl, configuration_revision_id: &ConfigurationRevisionId, @@ -987,7 +988,7 @@ pub async fn run_session_reflector_with_backend_and_retrieval( #[allow(clippy::too_many_arguments)] async fn run_session_reflector_with_backend_and_retrieval_publication( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, run_control: &AutomationRunControl, configuration_revision_id: &ConfigurationRevisionId, @@ -1002,11 +1003,12 @@ async fn run_session_reflector_with_backend_and_retrieval_publication( "automation:session-reflector", configuration_revision_id, )?; - let sessions_db = super::project_automation_sessions(cg).await?; - let project_memory_db = cg.open_project_store_db().await?; + let sessions_db = super::project_automation_sessions(cg); let memory = MemoryApplication::new( - cg.project_memory_owner()?, - DatabaseFactStore::new(&project_memory_db), + FactOwnerV1::Project { + project_id: cg.project_id.clone(), + }, + DatabaseFactStore::new(&cg.project_memory_database), ) .map_err(|error| TraceDecayError::Config { message: format!( @@ -1014,7 +1016,7 @@ async fn run_session_reflector_with_backend_and_retrieval_publication( ), })?; run_session_reflector_for_store_with_publication( - cg.store_layout().dashboard_root.clone(), + cg.dashboard_root.clone(), sessions_db, retrieval, &memory, @@ -1031,14 +1033,14 @@ async fn run_session_reflector_with_backend_and_retrieval_publication( } pub async fn run_session_reflector_with_backend( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, run_control: &AutomationRunControl, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: SessionReflectorAutomationOptions, ) -> AutomationRunResult { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_session_reflector_with_backend_and_retrieval( cg, config, @@ -1055,14 +1057,14 @@ pub async fn run_session_reflector_with_backend( /// its ledger terminal ahead of outer settlement. The retained settlement /// authority must bind and publish the returned exact record. pub async fn run_session_reflector_with_backend_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, run_control: &AutomationRunControl, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: SessionReflectorAutomationOptions, ) -> RetainedAutomationRun { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_session_reflector_with_backend_and_retrieval_for_retained_settlement( cg, config, @@ -1079,7 +1081,7 @@ pub async fn run_session_reflector_with_backend_for_retained_settlement( /// retrieval authority instead of silently reopening the production route. #[allow(clippy::too_many_arguments)] pub async fn run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, run_control: &AutomationRunControl, configuration_revision_id: &ConfigurationRevisionId, diff --git a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs index 14daf36600..861d74b329 100644 --- a/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs +++ b/crates/tracedecay-automation-runtime/src/automation/runner/skill_writer.rs @@ -120,13 +120,13 @@ pub(super) fn validate_skill_writer_decision(output: &Value, proposals: &[Value] } pub async fn run_skill_writer_with_backend( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: SkillWriterAutomationOptions, ) -> AutomationRunResult { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_skill_writer_with_backend_and_retrieval( cg, config, @@ -142,13 +142,13 @@ pub async fn run_skill_writer_with_backend( /// its ledger terminal ahead of outer settlement. The retained settlement /// authority must bind and publish the returned exact record. pub async fn run_skill_writer_with_backend_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, options: SkillWriterAutomationOptions, ) -> RetainedAutomationRun { - let retrieval = production_project_automation_retrieval(cg).await; + let retrieval = unavailable_automation_retrieval("session_evidence_retrieval_unavailable"); run_skill_writer_with_backend_and_retrieval_for_retained_settlement( cg, config, @@ -163,7 +163,7 @@ pub async fn run_skill_writer_with_backend_for_retained_settlement( /// Retained-settlement variant that preserves the caller's canonical session /// retrieval authority instead of silently reopening the production route. pub async fn run_skill_writer_with_backend_and_retrieval_for_retained_settlement( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -188,7 +188,7 @@ pub async fn run_skill_writer_with_backend_and_retrieval_for_retained_settlement } pub async fn run_skill_writer_with_backend_and_retrieval( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -211,7 +211,7 @@ pub async fn run_skill_writer_with_backend_and_retrieval( } async fn run_skill_writer_with_backend_and_retrieval_publication( - cg: &TraceDecay, + cg: &AutomationProjectContext, config: &AutomationConfig, configuration_revision_id: &ConfigurationRevisionId, backend: &dyn AgentTaskBackend, @@ -221,14 +221,14 @@ async fn run_skill_writer_with_backend_and_retrieval_publication( ) -> AutomationRunResult { let authority = project_curation_authority(cg, "automation:skill-writer", configuration_revision_id)?; - let sessions_db = project_automation_sessions(cg).await?; + let sessions_db = project_automation_sessions(cg); run_skill_writer_for_store_with_publication( SkillWriterStoreRuntime { - host_io: cg.host_io(), - dashboard_root: cg.store_layout().dashboard_root.clone(), + host_io: cg.host_io, + dashboard_root: cg.dashboard_root.clone(), sessions_db, - analytics_project_root: Some(cg.project_root()), - analytics_db: Some(cg.profile_database().as_ref()), + analytics_project_root: Some(&cg.project_root), + analytics_db: Some(cg.profile_database.as_ref()), authority, }, retrieval, diff --git a/crates/tracedecay-automation-runtime/src/ports/project_runtime.rs b/crates/tracedecay-automation-runtime/src/ports/project_runtime.rs index 9665e2fdd6..a50e42e339 100644 --- a/crates/tracedecay-automation-runtime/src/ports/project_runtime.rs +++ b/crates/tracedecay-automation-runtime/src/ports/project_runtime.rs @@ -5,35 +5,38 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use tracedecay_domain::errors::Result; -use tracedecay_domain::{FactOwnerV1, ProjectId, UserProfileId}; +use tracedecay_domain::{ProjectId, UserProfileId}; use tracedecay_global_db::RegisteredGlobalDbLeaseV1; use tracedecay_runtime_core::db::Database; -use tracedecay_runtime_core::storage::StoreLayout; use crate::automation::host_io::HostIo; pub type RuntimeFuture<'a, T> = Pin> + Send + 'a>>; -/// Project runtime needed by automation. -pub trait ProjectRuntime: Send + Sync { - fn project_root(&self) -> &Path; - /// The host-install I/O the composition root built for managed-skill - /// deployment; automation never reaches host-owned files another way. - fn host_io(&self) -> HostIo; - fn db(&self) -> &Database; - fn store_layout(&self) -> &StoreLayout; - fn project_memory_owner(&self) -> Result; - fn profile_id(&self) -> &UserProfileId; - fn profile_database(&self) -> &RegisteredGlobalDbLeaseV1; - fn project_sessions( - &self, - project_id: ProjectId, - roots: Vec, - ) -> RuntimeFuture<'_, RegisteredGlobalDbLeaseV1>; - fn open_project_store_db(&self) -> RuntimeFuture<'_, Database>; +/// Immutable project values captured by the composition root for one +/// automation run. +pub struct AutomationProjectContext { + pub project_root: PathBuf, + pub dashboard_root: PathBuf, + pub host_io: HostIo, + pub project_id: ProjectId, + pub profile_id: UserProfileId, + pub profile_database: RegisteredGlobalDbLeaseV1, + pub project_sessions: RegisteredGlobalDbLeaseV1, + pub project_memory_database: Database, } -pub type TraceDecay = dyn ProjectRuntime; +impl AutomationProjectContext { + #[must_use] + pub fn project_id(&self) -> &ProjectId { + &self.project_id + } + + #[must_use] + pub fn project_root(&self) -> &Path { + &self.project_root + } +} /// Profile runtime needed by projectless automation. pub trait ProfileRuntime: Send + Sync { diff --git a/crates/tracedecay-dashboard-api/src/lib.rs b/crates/tracedecay-dashboard-api/src/lib.rs index 5b9eb21ad9..f0fbe625e8 100644 --- a/crates/tracedecay-dashboard-api/src/lib.rs +++ b/crates/tracedecay-dashboard-api/src/lib.rs @@ -29,7 +29,7 @@ pub use application_surface::{ DashboardConfigurationApplyFuture, DashboardDaemonReadUnavailableV1, DashboardNativeIntegrationStatusFuture, DashboardScopeSetReadFuture, }; -pub use tracedecay::DashboardProjectRuntime; +pub use tracedecay::DashboardProjectContext; /// Installs the registered global/session schema into the kernel's fail-closed /// port for this crate's test process. @@ -206,7 +206,6 @@ use tower::ServiceExt; use tracedecay_api::{WorkOperation, WorkflowOperation}; -use crate::tracedecay::TraceDecay; use tracedecay_automation_runtime::automation::backend; use tracedecay_automation_runtime::automation::config::{AutomationBackend, AutomationHostMode}; use tracedecay_automation_runtime::automation::host_io::HostIo; @@ -412,7 +411,7 @@ pub struct DashboardState { /// Exact project graph retained by the daemon for this dashboard state. /// Absent for lightweight/profile-only states that cannot run project /// automation. - pub project_graph: Option>, + pub project_graph: Option>, /// Resolves other registered projects only when their graph is already /// mounted by the daemon. pub project_graph_resolver: Option, @@ -640,16 +639,17 @@ impl DashboardHostAdmissionTestAuthorityV1 { #[cfg(feature = "test-transport")] #[derive(Clone, Default)] pub struct DashboardTestProjectGraphsV1 { - graphs: Arc>>>, + graphs: + Arc>>>, } #[cfg(feature = "test-transport")] impl DashboardTestProjectGraphsV1 { - pub fn register(&self, graph: Arc) { + pub fn register(&self, graph: Arc) { self.graphs .write() .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(graph.project_root().to_path_buf(), graph); + .insert(graph.store_layout.project_root.clone(), graph); } fn resolver(&self) -> crate::project_graph::RetainedProjectGraphResolver { @@ -697,10 +697,10 @@ pub struct LcmStoreSelection { } pub async fn resolve_lcm_store( - cg: &TraceDecay, + cg: &DashboardProjectContext, registered_project_session_db: Option, ) -> LcmStoreSelection { - resolve_lcm_store_for_layout(cg.store_layout(), registered_project_session_db) + resolve_lcm_store_for_layout(&cg.store_layout, registered_project_session_db) } fn resolve_lcm_store_for_layout( @@ -728,10 +728,10 @@ pub fn storage_mode_label(mode: &StorageMode) -> &'static str { } } -pub fn resolve_project_memory_store(cg: &TraceDecay) -> (String, Arc) { +pub fn resolve_project_memory_store(cg: &DashboardProjectContext) -> (String, Arc) { ( - cg.dashboard_db_path().display().to_string(), - cg.dashboard_database_guard(), + cg.dashboard_db_path.display().to_string(), + Arc::clone(&cg.dashboard_database), ) } @@ -739,8 +739,8 @@ pub fn resolve_project_memory_store(cg: &TraceDecay) -> (String, Arc) /// /// Dashboard routes must never infer ownership from a path, label, or /// optional display field after construction. -pub fn project_memory_owner(cg: &TraceDecay) -> Result { - project_memory_owner_for_layout(cg.store_layout()) +pub fn project_memory_owner(cg: &DashboardProjectContext) -> Result { + project_memory_owner_for_layout(&cg.store_layout) } fn project_memory_owner_for_layout(layout: &StoreLayout) -> Result { @@ -756,8 +756,8 @@ fn project_memory_owner_for_layout(layout: &StoreLayout) -> Result } async fn build_state_inner( - cg: &TraceDecay, - project_graph: Option>, + cg: &DashboardProjectContext, + project_graph: Option>, warm_token_counts: bool, composition: DashboardStateCompositionV1, ) -> Result { @@ -789,10 +789,10 @@ async fn build_state_inner( let (mem_db_path, mem_db) = resolve_project_memory_store(cg); let memory_owner = project_memory_owner(cg)?; let lcm = resolve_lcm_store(cg, registered_project_session_db).await; - let dashboard_root = cg.store_layout().dashboard_root.clone(); - let store_root = cg.store_layout().data_root.clone(); - let config_path = cg.store_layout().config_path.clone(); - let storage_mode = storage_mode_label(&cg.store_layout().storage_mode).to_string(); + let dashboard_root = cg.store_layout.dashboard_root.clone(); + let store_root = cg.store_layout.data_root.clone(); + let config_path = cg.store_layout.config_path.clone(); + let storage_mode = storage_mode_label(&cg.store_layout.storage_mode).to_string(); let code_diagnostics_authority = match ( code_diagnostics_broker, code_graph_read_admission.as_ref(), @@ -800,7 +800,7 @@ async fn build_state_inner( ) { (Some(broker), Some(graph_admission), Some(graph_projection)) => Some( crate::application::dashboard_diagnostics::DashboardDiagnosticsAuthorityV1::new( - cg.project_root().to_path_buf(), + cg.store_layout.project_root.clone(), dashboard_root.clone(), Arc::clone(graph_admission), Arc::clone(graph_projection), @@ -822,11 +822,11 @@ async fn build_state_inner( ); let mut state = DashboardState { build_version, - host_io: cg.automation_runtime().host_io(), - project_id: cg.store_layout().identity.project_id.clone(), + host_io: cg.host_io, + project_id: cg.store_layout.identity.project_id.clone(), resolved_scope: scope::resolve_dashboard_scope( - cg.project_root(), - cg.store_layout().identity.project_id.as_deref(), + &cg.store_layout.project_root, + cg.store_layout.identity.project_id.as_deref(), ), code_graph_read_admission, code_graph_projection_read_port, @@ -835,11 +835,8 @@ async fn build_state_inner( memory_owner, graph_conn: mem_db.read_connection(), _database_guards: vec![mem_db.clone()], - graph_telemetry_handle: cg - .dashboard_database_guard() - .storage_telemetry_handle() - .ok(), - graph_db_path: cg.dashboard_db_path().display().to_string(), + graph_telemetry_handle: cg.dashboard_database.storage_telemetry_handle().ok(), + graph_db_path: cg.dashboard_db_path.display().to_string(), mem_db, mem_db_path, lcm_db: lcm.lcm_db, @@ -850,7 +847,7 @@ async fn build_state_inner( delivery_read_authority, savings_db: registered_savings_db, savings_db_path, - project_root: cg.project_root().to_path_buf(), + project_root: cg.store_layout.project_root.clone(), code_index_freshness_reader, explorer_semantic_reader, feedback_status_reader, @@ -859,8 +856,8 @@ async fn build_state_inner( store_root, config_path, dashboard_root, - retention_config: cg.retention_config(), - user_settings: cg.user_settings_client(), + retention_config: cg.retention_config.clone(), + user_settings: Arc::clone(&cg.user_settings_client), profile_code_index_worker_settings, token_counts: Arc::new(token_count::TokenCountCache::new()), derived_snapshots: Arc::new(snapshot_cache::DerivedSnapshotCaches::new()), @@ -888,7 +885,7 @@ async fn build_state_inner( } pub async fn build_state_with_automation_reconciler( - cg: Arc, + cg: Arc, composition: DashboardStateCompositionV1, ) -> Result { build_state_inner(cg.as_ref(), Some(Arc::clone(&cg)), true, composition).await @@ -898,7 +895,7 @@ pub async fn build_state_with_automation_reconciler( /// dashboard project picker. Automation authority is inherited from the active /// dashboard state so daemon-selected projects cannot fall back to direct open. pub async fn build_selected_project_state( - cg: Arc, + cg: Arc, active: &DashboardState, ) -> Result { build_state_inner( @@ -978,7 +975,7 @@ pub struct DashboardTestEndpointV1<'a> { #[doc(hidden)] #[cfg(feature = "test-transport")] pub async fn run_until_shutdown_for_tests_with_host_admission( - cg: Arc, + cg: Arc, authority: DashboardHostAdmissionTestAuthorityV1, project_graphs: DashboardTestProjectGraphsV1, endpoint: DashboardTestEndpointV1<'_>, @@ -1016,12 +1013,12 @@ struct DashboardRunRequest<'a> { spa_routes: Router, test_authority: Option<&'a DashboardHostAdmissionTestAuthorityV1>, test_project_graph_resolver: Option, - test_project_graph: Option>, + test_project_graph: Option>, } #[cfg(feature = "test-transport")] async fn run_until_shutdown_inner( - cg: &TraceDecay, + cg: &DashboardProjectContext, request: DashboardRunRequest<'_>, shutdown: F, ) -> Result<()> @@ -1043,8 +1040,8 @@ where // entry point started the dashboard. let code_diagnostics_broker = crate::application::dashboard_diagnostics::open_diagnostic_broker( - cg.project_root().to_path_buf(), - &cg.store_layout().dashboard_root, + cg.store_layout.project_root.clone(), + &cg.store_layout.dashboard_root, ) .await; let state = build_state_inner( @@ -1100,7 +1097,7 @@ where let url = format!("http://{addr}/"); // Stable, parseable line for wrappers (the Hermes plugin reads this). println!("tracedecay dashboard listening on {url}"); - eprintln!("Serving project {}", cg.project_root().display()); + eprintln!("Serving project {}", cg.store_layout.project_root.display()); axum::serve(listener, app) .with_graceful_shutdown(shutdown) @@ -1362,7 +1359,7 @@ struct ActiveProjectApplicationRoutes { impl ActiveProjectApplicationRoutes { fn for_active_project( - cg: &TraceDecay, + cg: &DashboardProjectContext, executor: Option>, ) -> Result { let executor = executor @@ -1400,7 +1397,7 @@ impl ActiveProjectApplicationRoutes { /// panics on overlapping paths. Pass `Router::new()` to serve the JSON API /// with no UI. pub async fn router( - cg: &TraceDecay, + cg: &DashboardProjectContext, mut state: DashboardState, spa_routes: Router, ) -> Result { @@ -1794,7 +1791,7 @@ async fn project_scoped_api_gateway( .active_state() .application_invocation_executor .as_ref(), - project_graph.project_root(), + &project_graph.store_layout.project_root, ) { Ok(application_runtime) => application_runtime, Err(err) => { diff --git a/crates/tracedecay-dashboard-api/src/project_graph.rs b/crates/tracedecay-dashboard-api/src/project_graph.rs index ddaa1d8fd1..b4068d1b90 100644 --- a/crates/tracedecay-dashboard-api/src/project_graph.rs +++ b/crates/tracedecay-dashboard-api/src/project_graph.rs @@ -11,12 +11,13 @@ use std::path::PathBuf; use std::sync::Arc; -use crate::tracedecay::TraceDecay; +use crate::tracedecay::DashboardProjectContext; pub type RetainedProjectGraphFuture = std::pin::Pin< Box< - dyn std::future::Future>>> - + Send + dyn std::future::Future< + Output = tracedecay_domain::errors::Result>>, + > + Send + 'static, >, >; diff --git a/crates/tracedecay-dashboard-api/src/projects.rs b/crates/tracedecay-dashboard-api/src/projects.rs index 5d111db213..9a65f1b0be 100644 --- a/crates/tracedecay-dashboard-api/src/projects.rs +++ b/crates/tracedecay-dashboard-api/src/projects.rs @@ -95,7 +95,7 @@ impl DashboardRuntime { "registered project graph is not mounted: {project_id}" )) })?; - if cg.store_layout().identity.project_id.as_deref() != Some(project_id) { + if cg.store_layout.identity.project_id.as_deref() != Some(project_id) { return Err(config_error(format!( "registered project id mismatch for {project_id}: {}", project_root.display() diff --git a/crates/tracedecay-dashboard-api/src/tracedecay.rs b/crates/tracedecay-dashboard-api/src/tracedecay.rs index 3a700a9e49..ba87f48e39 100644 --- a/crates/tracedecay-dashboard-api/src/tracedecay.rs +++ b/crates/tracedecay-dashboard-api/src/tracedecay.rs @@ -1,30 +1,28 @@ //! Dashboard-facing graph and memory runtime seams. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; -use tracedecay_automation_runtime::ports::project_runtime::ProjectRuntime; +use tracedecay_automation_runtime::automation::host_io::HostIo; pub use tracedecay_code_index::is_test_file; use tracedecay_configuration::UserSettingsDaemonClient; -use tracedecay_domain::errors::Result; -use tracedecay_runtime_core::db::{Database, DatabaseStorageTelemetryHandle}; +use tracedecay_runtime_core::db::Database; use tracedecay_runtime_core::storage::StoreLayout; use crate::config::RetentionConfig; -pub trait DashboardProjectRuntime: Send + Sync { - fn project_root(&self) -> &Path; - fn store_layout(&self) -> &StoreLayout; - fn automation_runtime(&self) -> &(dyn ProjectRuntime + 'static); - fn dashboard_db_path(&self) -> PathBuf; - fn dashboard_database_guard(&self) -> Arc; - fn storage_telemetry_handle(&self) -> Result; - fn retention_config(&self) -> RetentionConfig; - fn user_settings_client(&self) -> Arc; +/// Immutable project values captured by the composition root for dashboard +/// state construction. +#[derive(Clone)] +pub struct DashboardProjectContext { + pub store_layout: StoreLayout, + pub dashboard_db_path: PathBuf, + pub dashboard_database: Arc, + pub retention_config: RetentionConfig, + pub host_io: HostIo, + pub user_settings_client: Arc, } -pub type TraceDecay = dyn DashboardProjectRuntime; - pub mod facts { // The shared resolvers live in `tracedecay_session_memory::memory` — the crate // that owns `MemoryApplication`/`MemoryApplicationError` — rather than a diff --git a/crates/tracedecay/src/daemon/automation_effect/journal/tests.rs b/crates/tracedecay/src/daemon/automation_effect/journal/tests.rs index d2e924e890..9e2d090eb8 100644 --- a/crates/tracedecay/src/daemon/automation_effect/journal/tests.rs +++ b/crates/tracedecay/src/daemon/automation_effect/journal/tests.rs @@ -556,8 +556,11 @@ async fn retained_repeated_memory_curator_run( }; let run_control = AutomationRunControl::from_interrupted(Arc::new(|| false)); + let project_context = cg + .automation_project_context() + .expect("compose project automation context"); run_memory_curator_with_backend_for_retained_settlement( - cg, + &project_context, config, configuration_revision, &NeverAutomationBackend, @@ -3161,6 +3164,9 @@ async fn reused_scheduler_skip_abandons_current_effect_before_observing_exact_pr ) .await .expect("initialize fixed-task automation project"); + let project_context = cg + .automation_project_context() + .expect("compose project automation context"); let dashboard_root = &cg.store_layout().dashboard_root; let config = AutomationConfig { enabled: true, @@ -3176,7 +3182,7 @@ async fn reused_scheduler_skip_abandons_current_effect_before_observing_exact_pr let current_run_id = "run.reused-scheduler-skip.current"; let prior = run_memory_curator_with_backend_for_retained_settlement( - &cg, + &project_context, &config, &configuration_revision, &NeverAutomationBackend, diff --git a/crates/tracedecay/src/daemon/dashboard_automation.rs b/crates/tracedecay/src/daemon/dashboard_automation.rs index c6b1eafc4e..a75995d72b 100644 --- a/crates/tracedecay/src/daemon/dashboard_automation.rs +++ b/crates/tracedecay/src/daemon/dashboard_automation.rs @@ -21,7 +21,6 @@ use tracedecay_automation_runtime::automation::run_ledger::{ AutomationRunLedgerRecord, AutomationTrigger, }; use tracedecay_automation_runtime::automation::skill_writer::deploy_managed_skills_to_project; -use tracedecay_automation_runtime::ports::project_runtime::ProjectRuntime as _; use tracedecay_contracts::now_micros; #[cfg(feature = "test-transport")] use tracedecay_daemon_identity::authority; @@ -242,7 +241,7 @@ fn dashboard_managed_skill_command_port( execute_serialized_dashboard_automation(&writer, move || async move { let cg = project_resolver(invocation.project_root.clone()).await?; execute_dashboard_managed_skill_command( - &cg.host_io(), + &tracedecay_agent_hosts::host_io(), &profile_root, cg.project_root(), invocation.command, diff --git a/crates/tracedecay/src/daemon/dashboard_automation/retained_curator.rs b/crates/tracedecay/src/daemon/dashboard_automation/retained_curator.rs index a69833df55..b53605775c 100644 --- a/crates/tracedecay/src/daemon/dashboard_automation/retained_curator.rs +++ b/crates/tracedecay/src/daemon/dashboard_automation/retained_curator.rs @@ -60,11 +60,16 @@ pub(crate) async fn execute_retained_memory_curator( .automation_request(context.request_context.request_id()) .map_err(|_| RetainedSurfaceExecutionErrorV1::InvalidRequest)?; let run_id = automation_request.run_id.as_str().to_owned(); + let automation_context = cg.automation_project_context().map_err(|error| { + RetainedSurfaceExecutionErrorV1::unavailable(format!( + "the automation project context could not be composed: {error}" + )) + })?; let admission = crate::daemon::automation_effect::AutomationEffectAuthority::prepare( invocation_service, cg, - cg.project_root(), - &cg.store_layout().dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, context.request_context.request_id().clone(), context.request_context.deadline().clone(), context.cancellation_signal, @@ -101,15 +106,15 @@ pub(crate) async fn execute_retained_memory_curator( })); let observation_producer = crate::daemon::project_automation_observation_producer( invocation_service, - cg.project_root(), + automation_context.project_root(), ) .await; - let project_root = cg.project_root().to_path_buf(); + let project_root = automation_context.project_root().to_path_buf(); let observer = observation_producer.map(|producer| { super::automation_run_observer(producer, project_root, "fact_store_curate") }); let retained_run = run_memory_curator_with_backend_for_retained_settlement( - cg, + &automation_context, &config, pinned.revision_id(), &backend, @@ -157,3 +162,107 @@ fn automation_problem( ) -> RetainedSurfaceExecutionErrorV1 { RetainedSurfaceExecutionErrorV1::ApplicationProblem(problem.problem.problem.source().clone()) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use tracedecay_contracts::{ + CancellationContext, CancellationSignal, CapabilityGrantId, CapabilityGrantSnapshot, + Deadline, DisclosureClass, RequestContext, RequestId, ResolvedScope, + RetainedSurfaceExecutionContextV1, RetainedSurfaceOperation, + retained_surface_application_operation, + }; + use tracedecay_domain::{ + ActorId, ProjectId, RepositoryId, UtcMicros, WorktreeId, canonical_sha256, + }; + + use super::{ + DaemonInvocationService, FactStoreCurateRequestV1, RetainedSurfaceExecutionErrorV1, + }; + + #[tokio::test] + async fn context_failure_precedes_retained_curator_admission() { + let directory = tempfile::tempdir().expect("temporary project"); + let project_root = directory.path().join("project"); + let profile_root = directory.path().join("profile"); + std::fs::create_dir_all(project_root.join("src")).expect("project source directory"); + std::fs::write(project_root.join("src/lib.rs"), "pub fn fixture() {}\n") + .expect("project source"); + let options = crate::tracedecay::TraceDecayOpenOptions { + profile_root: Some(profile_root.clone()), + global_db_path: Some(profile_root.join("global.db")), + }; + let writable = + crate::tracedecay::TraceDecay::init_with_options(&project_root, options.clone()) + .await + .expect("initialize retained curator project"); + let dashboard_root = writable.store_layout().dashboard_root.clone(); + writable.close(); + let read_only = + crate::tracedecay::TraceDecay::open_read_only_with_options(&project_root, options) + .await + .expect("open read-only retained curator project"); + let operation = + retained_surface_application_operation(RetainedSurfaceOperation::FactStoreCurate) + .expect("retained operation"); + let actor = ActorId::new("actor.retained-context-failure").expect("actor"); + let scope = ResolvedScope::new( + ProjectId::new("project.retained-context-failure").expect("project id"), + RepositoryId::new("repository.retained-context-failure").expect("repository id"), + WorktreeId::new("worktree.retained-context-failure").expect("worktree id"), + None, + ) + .expect("scope"); + let request_id = RequestId::new("request.retained-context-failure").expect("request id"); + let cancellation_id = "cancel.retained-context-failure".to_owned(); + let grant = CapabilityGrantSnapshot::new( + CapabilityGrantId::new("grant.retained-context-failure").expect("grant id"), + 1, + canonical_sha256(&"retained-context-failure").expect("grant digest"), + actor.clone(), + UtcMicros(1), + UtcMicros(i64::MAX - 1), + scope.clone(), + BTreeSet::from([operation.capability_id().clone()]), + BTreeSet::from([operation.use_case_id().clone()]), + DisclosureClass::Evidence, + ) + .expect("grant"); + let request_context = RequestContext::new( + actor, + scope, + grant, + request_id, + Deadline::new(UtcMicros(i64::MAX)).expect("deadline"), + CancellationContext::active(cancellation_id.clone()).expect("cancellation context"), + ) + .expect("request context"); + let cancellation = + CancellationSignal::active(cancellation_id).expect("cancellation signal"); + let execution = RetainedSurfaceExecutionContextV1 { + request_context: &request_context, + cancellation_signal: &cancellation, + operation: &operation, + observed_at: UtcMicros(2), + }; + + let error = super::execute_retained_memory_curator( + &read_only, + &DaemonInvocationService::default(), + &execution, + &FactStoreCurateRequestV1::default(), + ) + .await + .expect_err("read-only automation context must fail before admission"); + + let RetainedSurfaceExecutionErrorV1::Unavailable { detail } = error else { + panic!("context failure must win over admission: {error:?}"); + }; + assert!(detail.contains("open read-only")); + assert!( + !dashboard_root.join("automation_effects").exists(), + "context failure must not leave a durable automation reservation" + ); + } +} diff --git a/crates/tracedecay/src/daemon/scheduler/combined_effect.rs b/crates/tracedecay/src/daemon/scheduler/combined_effect.rs index b09ddc62fe..d49b2cf9a9 100644 --- a/crates/tracedecay/src/daemon/scheduler/combined_effect.rs +++ b/crates/tracedecay/src/daemon/scheduler/combined_effect.rs @@ -13,6 +13,7 @@ use tracedecay_automation_runtime::automation::runner::{ run_session_reflector_with_backend_and_retrieval_for_retained_settlement, run_skill_writer_with_backend_and_retrieval_for_retained_settlement, }; +use tracedecay_automation_runtime::ports::project_runtime::AutomationProjectContext; use super::scheduler_automation_effect; use crate::daemon::DaemonEngine; @@ -360,9 +361,7 @@ where pub(super) async fn run_combined_scheduler_effect( admission: CombinedEffectAdmission, engine: &DaemonEngine, - memory: &TraceDecay, - project_id: &tracedecay_domain::ProjectId, - project_path: &Path, + automation_context: &AutomationProjectContext, config: &tracedecay_automation_runtime::automation::config::AutomationConfig, configuration_revision_id: &tracedecay_domain::configuration::ConfigurationRevisionId, backend: &dyn tracedecay_automation_runtime::automation::backend::AgentTaskBackend, @@ -373,9 +372,7 @@ pub(super) async fn run_combined_scheduler_effect( run_combined_scheduler_effect_inner( admission, engine, - memory, - project_id, - project_path, + automation_context, config, configuration_revision_id, backend, @@ -394,9 +391,7 @@ pub(super) async fn run_combined_scheduler_effect( fn run_combined_scheduler_effect_inner<'a>( admission: CombinedEffectAdmission, engine: &'a DaemonEngine, - memory: &'a TraceDecay, - project_id: &'a tracedecay_domain::ProjectId, - project_path: &'a Path, + automation_context: &'a AutomationProjectContext, config: &'a tracedecay_automation_runtime::automation::config::AutomationConfig, configuration_revision_id: &'a tracedecay_domain::configuration::ConfigurationRevisionId, backend: &'a dyn tracedecay_automation_runtime::automation::backend::AgentTaskBackend, @@ -408,7 +403,7 @@ fn run_combined_scheduler_effect_inner<'a>( let outcome = match admission { CombinedEffectAdmission::Conflict => { super::log_scheduler_admission_conflict( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::CombinedReview, ); CombinedEffectOutcome::Handled @@ -416,7 +411,7 @@ fn run_combined_scheduler_effect_inner<'a>( CombinedEffectAdmission::PreAdmissionProblem(problems) => { for problem in problems { super::log_scheduler_pre_admission_problem( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::CombinedReview, &problem, ); @@ -425,12 +420,12 @@ fn run_combined_scheduler_effect_inner<'a>( } CombinedEffectAdmission::Replay { reflector, skill } => { super::log_scheduler_automation_replay( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::SessionReflector, &reflector, ); super::log_scheduler_automation_replay( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::SkillWriter, &skill, ); @@ -447,7 +442,7 @@ fn run_combined_scheduler_effect_inner<'a>( skill, } => { super::log_scheduler_automation_replay( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::SessionReflector, &reflector, ); @@ -456,7 +451,7 @@ fn run_combined_scheduler_effect_inner<'a>( skill_options.trigger = options.trigger; let replay_completed = reflector.is_completed(); let retained = run_skill_writer_with_backend_and_retrieval_for_retained_settlement( - memory, + automation_context, config, configuration_revision_id, backend, @@ -466,8 +461,8 @@ fn run_combined_scheduler_effect_inner<'a>( .await; settle_single_replay_leg( engine, - project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), first_error, replay_completed, tracedecay_automation_runtime::automation::backend::AgentTaskKind::SkillWriter, @@ -484,7 +479,7 @@ fn run_combined_scheduler_effect_inner<'a>( skill, } => { super::log_scheduler_automation_replay( - project_path, + automation_context.project_root(), tracedecay_automation_runtime::automation::backend::AgentTaskKind::SkillWriter, &skill, ); @@ -494,7 +489,7 @@ fn run_combined_scheduler_effect_inner<'a>( let replay_completed = skill.is_completed(); let retained = run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - memory, + automation_context, config, &reflector_control, configuration_revision_id, @@ -505,8 +500,8 @@ fn run_combined_scheduler_effect_inner<'a>( .await; settle_single_replay_leg( engine, - project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), first_error, replay_completed, tracedecay_automation_runtime::automation::backend::AgentTaskKind::SessionReflector, @@ -528,9 +523,7 @@ fn run_combined_scheduler_effect_inner<'a>( *reflector, *skill, engine, - memory, - project_id, - project_path, + automation_context, config, configuration_revision_id, backend, @@ -566,9 +559,7 @@ fn run_execute_pair<'a>( reflector: AutomationEffectAuthority, skill: AutomationEffectAuthority, engine: &'a DaemonEngine, - memory: &'a TraceDecay, - project_id: &'a tracedecay_domain::ProjectId, - project_path: &'a Path, + automation_context: &'a AutomationProjectContext, config: &'a tracedecay_automation_runtime::automation::config::AutomationConfig, configuration_revision_id: &'a tracedecay_domain::configuration::ConfigurationRevisionId, backend: &'a dyn tracedecay_automation_runtime::automation::backend::AgentTaskBackend, @@ -577,8 +568,10 @@ fn run_execute_pair<'a>( first_error: &'a mut Option, ) -> Pin + Send + 'a>> { Box::pin(async move { + let project_id = automation_context.project_id(); + let project_path = automation_context.project_root(); let retained = run_combined_review_with_backend_and_retrieval_for_retained_settlement( - memory, + automation_context, config, configuration_revision_id, backend, @@ -1551,10 +1544,14 @@ mod tests { let options = CombinedReviewAutomationOptions::default(); assert_eq!(options.trigger, AutomationTrigger::Scheduler); assert_eq!(options.skill_writer.trigger, AutomationTrigger::ManualCli); + let automation_context = fixture + .memory + .automation_project_context() + .expect("compose project automation context"); let prior_skill_run_id = "combined-partial-replay-prior-skill"; let prior_skill = run_skill_writer_with_backend_and_retrieval( - fixture.memory.as_ref(), + &automation_context, &config, &fixture.configuration_revision_id, &backend, @@ -1603,7 +1600,7 @@ mod tests { reflector_options.run_id = Some(combined_run_id.to_owned()); let reflector_run = run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - fixture.memory.as_ref(), + &automation_context, &config, &parent_control, &fixture.configuration_revision_id, @@ -1671,9 +1668,7 @@ mod tests { let mut effect = Box::pin(run_combined_scheduler_effect( admission, &fixture.engine, - fixture.memory.as_ref(), - &fixture.project_id, - &fixture.project_root, + &automation_context, &config, &fixture.configuration_revision_id, &backend, diff --git a/crates/tracedecay/src/daemon/scheduler/effect_admission.rs b/crates/tracedecay/src/daemon/scheduler/effect_admission.rs index 58cafa990b..168ec57b53 100644 --- a/crates/tracedecay/src/daemon/scheduler/effect_admission.rs +++ b/crates/tracedecay/src/daemon/scheduler/effect_admission.rs @@ -338,6 +338,7 @@ fn run_automation_scheduler_tick_inner<'a>( ); return Ok(()); } + let automation_context = cg.automation_project_context()?; if let Ok(profile_database) = engine .store_administration .registered_profile_database() @@ -351,24 +352,12 @@ fn run_automation_scheduler_tick_inner<'a>( .await; } let backend = CodexAppServerBackend::from_automation_config(config); - let authoritative_project_id = cg - .store_layout() - .identity - .project_id - .as_deref() - .ok_or_else(|| TraceDecayError::Config { - message: "automation scheduler requires an authoritative project identity" - .to_string(), - })?; - let project_id = tracedecay_domain::ProjectId::new(authoritative_project_id.to_string()) - .map_err(|error| TraceDecayError::Config { - message: format!( - "automation scheduler has an invalid authoritative project identity: {error}" - ), - })?; let session_database = engine .store_administration - .registered_project_session_database(project_path, cg.store_layout()) + .registered_project_session_database( + automation_context.project_root(), + cg.store_layout(), + ) .await?; let schedule_activity = tracedecay_automation_runtime::automation::scheduler::load_session_activity( @@ -404,7 +393,7 @@ fn run_automation_scheduler_tick_inner<'a>( let retrieval = registered_project_automation_retrieval( session_database, &profile_identity, - &project_id, + automation_context.project_id(), ) .await?; let mut first_error: Option = None; @@ -421,8 +410,8 @@ fn run_automation_scheduler_tick_inner<'a>( engine, cg, run_control, - project_path, - &cg.store_layout().dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, None, configuration.configuration_digest.clone(), |run_id| { @@ -457,7 +446,7 @@ fn run_automation_scheduler_tick_inner<'a>( let mut options = memory_curator_options; options.run_id = Some(run_id); let retained_run = run_memory_curator_with_backend_for_retained_settlement( - cg, + &automation_context, config, &configuration.configuration_revision_id, &backend, @@ -467,8 +456,8 @@ fn run_automation_scheduler_tick_inner<'a>( .await; if let Some(error) = settle_scheduler_retained_automation( engine, - &project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), AgentTaskKind::MemoryCurator, &effect_run_control, *effect, @@ -508,8 +497,8 @@ fn run_automation_scheduler_tick_inner<'a>( engine, cg, run_control, - project_path, - &cg.store_layout().dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, None, configuration.configuration_digest.clone(), &combined_options, @@ -520,9 +509,7 @@ fn run_automation_scheduler_tick_inner<'a>( combined_handled = super::combined_effect::run_combined_scheduler_effect( admission, engine, - cg, - &project_id, - project_path, + &automation_context, config, &configuration.configuration_revision_id, &backend, @@ -552,8 +539,8 @@ fn run_automation_scheduler_tick_inner<'a>( engine, cg, run_control, - project_path, - &cg.store_layout().dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, None, configuration.configuration_digest.clone(), |run_id| { @@ -600,7 +587,7 @@ fn run_automation_scheduler_tick_inner<'a>( )) => { let retained_run = run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - cg, + &automation_context, config, &effect_run_control, &configuration.configuration_revision_id, @@ -614,8 +601,8 @@ fn run_automation_scheduler_tick_inner<'a>( .await; if let Some(error) = settle_scheduler_retained_automation( engine, - &project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), AgentTaskKind::SessionReflector, &effect_run_control, *effect, @@ -642,8 +629,8 @@ fn run_automation_scheduler_tick_inner<'a>( engine, cg, run_control, - project_path, - &cg.store_layout().dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, None, configuration.configuration_digest.clone(), |run_id| { @@ -681,7 +668,7 @@ fn run_automation_scheduler_tick_inner<'a>( options.run_id = Some(run_id); let retained_run = run_skill_writer_with_backend_and_retrieval_for_retained_settlement( - cg, + &automation_context, config, &configuration.configuration_revision_id, &backend, @@ -691,8 +678,8 @@ fn run_automation_scheduler_tick_inner<'a>( .await; if let Some(error) = settle_scheduler_retained_automation( engine, - &project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), AgentTaskKind::SkillWriter, &effect_run_control, *effect, @@ -710,8 +697,8 @@ fn run_automation_scheduler_tick_inner<'a>( run_user_jobs_scheduler_pass( engine, run_control, - &project_id, - project_path, + automation_context.project_id(), + automation_context.project_root(), &handshake.client_identity.profile_root, cg, configuration.configuration_digest.clone(), diff --git a/crates/tracedecay/src/daemon/scheduler/host_receipt_review.rs b/crates/tracedecay/src/daemon/scheduler/host_receipt_review.rs index 72c8624808..cb85b57333 100644 --- a/crates/tracedecay/src/daemon/scheduler/host_receipt_review.rs +++ b/crates/tracedecay/src/daemon/scheduler/host_receipt_review.rs @@ -115,22 +115,14 @@ async fn run_one_host_receipt_review( } let configuration = effective_automation_config_for_project(cg).await?; let config = &configuration.settings; + let automation_context = cg.automation_project_context()?; let session_id = pending .route .as_ref() .and_then(|route| route.session_id.clone()); - let Some(authoritative_project_id) = cg.store_layout().identity.project_id.as_deref() else { - return Ok(HostReceiptReviewProgress::Deferred); - }; - let project_id = tracedecay_domain::ProjectId::new(authoritative_project_id.to_string()) - .map_err(|error| TraceDecayError::Config { - message: format!( - "host receipt review has an invalid authoritative project identity: {error}" - ), - })?; let session_database = engine .store_administration - .registered_project_session_database(project_path, cg.store_layout()) + .registered_project_session_database(automation_context.project_root(), cg.store_layout()) .await?; let watermark_durable = { @@ -167,9 +159,12 @@ async fn run_one_host_receipt_review( return Ok(HostReceiptReviewProgress::Deferred); } let profile_identity = engine.store_administration.profile_identity()?.clone(); - let retrieval = - registered_project_automation_retrieval(session_database, &profile_identity, &project_id) - .await?; + let retrieval = registered_project_automation_retrieval( + session_database, + &profile_identity, + automation_context.project_id(), + ) + .await?; let backend = CodexAppServerBackend::from_automation_config(config); let host_run_id = format!("host_receipt_{}", pending.generation); let combined_options = CombinedReviewAutomationOptions { @@ -192,8 +187,8 @@ async fn run_one_host_receipt_review( engine, cg, run_control, - project_path, - &dashboard_root, + automation_context.project_root(), + &automation_context.dashboard_root, Some(&host_run_id), configuration.configuration_digest.clone(), &combined_options, @@ -203,9 +198,7 @@ async fn run_one_host_receipt_review( let outcome = Box::pin(super::combined_effect::run_combined_scheduler_effect( admission, engine, - cg, - &project_id, - project_path, + &automation_context, config, &configuration.configuration_revision_id, &backend, @@ -245,8 +238,13 @@ mod tests { use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; + use tracedecay_automation_runtime::automation::AutomationRunControl; + use tracedecay_daemon_protocol::{DaemonClientIdentity, DaemonHandshake, MovedStoreAdoption}; + use tracedecay_hooks::{HookRouteMetadata, HookTerminalReceipt}; + use super::{ - HOST_RECEIPT_REVIEW_BATCH_LIMIT, HostReceiptReviewProgress, drain_ready_host_receipts, + DaemonEngine, HOST_RECEIPT_REVIEW_BATCH_LIMIT, HostReceiptReviewProgress, + drain_ready_host_receipts, run_one_host_receipt_review, }; #[tokio::test] @@ -293,4 +291,90 @@ mod tests { HOST_RECEIPT_REVIEW_BATCH_LIMIT ); } + + #[tokio::test] + async fn context_failure_precedes_host_receipt_admission() { + let directory = tempfile::tempdir().expect("temporary project"); + let project_root = directory.path().join("project"); + let profile_root = directory.path().join("profile"); + std::fs::create_dir_all(project_root.join("src")).expect("project source directory"); + std::fs::write(project_root.join("src/lib.rs"), "pub fn fixture() {}\n") + .expect("project source"); + let options = crate::tracedecay::TraceDecayOpenOptions { + profile_root: Some(profile_root.clone()), + global_db_path: Some(profile_root.join("global.db")), + }; + let writable = + crate::tracedecay::TraceDecay::init_with_options(&project_root, options.clone()) + .await + .expect("initialize host receipt project"); + let dashboard_root = writable.store_layout().dashboard_root.clone(); + let route = Some(HookRouteMetadata { + session_id: Some("session.context-failure".to_owned()), + thread_id: None, + cwd: None, + worktree: None, + branch: None, + }); + tracedecay_automation_runtime::automation::host_receipts::record( + &dashboard_root, + route.clone(), + HookTerminalReceipt { + tool_call_id: Some("call.context-failure".to_owned()), + turn_id: Some("turn.context-failure".to_owned()), + status: Some("success".to_owned()), + duration_ms: Some(1), + transcript_watermark: Some("message.context-failure".to_owned()), + }, + ) + .await + .expect("record host receipt"); + tracedecay_automation_runtime::automation::host_receipts::mark_turn_ingested( + &dashboard_root, + route, + "message.context-failure", + ) + .await + .expect("mark host receipt ready"); + writable.close(); + let read_only = + crate::tracedecay::TraceDecay::open_read_only_with_options(&project_root, options) + .await + .expect("open read-only host receipt project"); + let handshake = DaemonHandshake { + project_path: Some(project_root.clone()), + scope_prefix: None, + timings: false, + allow_init: false, + allow_initialize_root_routing: false, + client_identity: DaemonClientIdentity::new( + profile_root.clone(), + profile_root.join("global.db"), + ), + client_version: env!("CARGO_PKG_VERSION").to_owned(), + client_instance_id: "client.context-failure".to_owned(), + tool_list_changed_capable: false, + catalog_version: String::new(), + moved_store_adoption: MovedStoreAdoption::Never, + }; + + let error = run_one_host_receipt_review( + &project_root, + &read_only, + &handshake, + &DaemonEngine::default(), + &AutomationRunControl::from_interrupted(Arc::new(|| false)), + ) + .await + .expect_err("read-only automation context must fail before admission"); + + assert!( + error.to_string().contains("open read-only"), + "context failure must win over admission: {error}" + ); + assert!( + !dashboard_root.join("automation_effects").exists(), + "context failure must not leave a durable automation reservation" + ); + } } diff --git a/crates/tracedecay/src/dashboard.rs b/crates/tracedecay/src/dashboard.rs index e492c3840d..660ec20d71 100644 --- a/crates/tracedecay/src/dashboard.rs +++ b/crates/tracedecay/src/dashboard.rs @@ -9,6 +9,8 @@ //! runtime ([`crate::product_runtime`]). The canonical API crate owns the //! resulting HTTP router and transport policy. +use tracedecay_dashboard_api::DashboardProjectContext; + #[cfg(feature = "test-transport")] use tracedecay_daemon_service::DaemonInvocationService; #[cfg(feature = "test-transport")] @@ -28,8 +30,7 @@ pub use tracedecay_dashboard_api::contract_schema; #[cfg(feature = "test-transport")] #[doc(hidden)] pub use tracedecay_dashboard_api::{ - DashboardHostAdmissionTestAuthorityV1, DashboardTestEndpointV1, DashboardTestProjectGraphsV1, - run_until_shutdown_for_tests_with_host_admission, + DashboardHostAdmissionTestAuthorityV1, DashboardTestEndpointV1, }; /// Canonical observation-capture seeding for dashboard integration fixtures. @@ -56,6 +57,68 @@ pub fn register_test_schema_installer() { REGISTER.call_once(tracedecay_store_runtime::register_registered_schema_installer); } +pub(crate) fn dashboard_project_context( + graph: &crate::tracedecay::TraceDecay, +) -> DashboardProjectContext { + DashboardProjectContext { + store_layout: graph.store_layout().clone(), + dashboard_db_path: graph.dashboard_db_path(), + dashboard_database: graph.dashboard_database_guard(), + retention_config: tracedecay_dashboard_api::config::RetentionConfig { + store_soft_budgets_bytes: graph + .get_config() + .sync + .retention + .store_soft_budgets_bytes + .clone(), + }, + host_io: tracedecay_agent_hosts::host_io(), + user_settings_client: graph.configuration_runtime().user_settings_client(), + } +} + +#[cfg(feature = "test-transport")] +#[doc(hidden)] +#[derive(Clone, Default)] +pub struct DashboardTestProjectGraphsV1 { + contexts: tracedecay_dashboard_api::DashboardTestProjectGraphsV1, +} + +#[cfg(feature = "test-transport")] +impl DashboardTestProjectGraphsV1 { + pub fn register(&self, graph: std::sync::Arc) { + self.contexts + .register(std::sync::Arc::new(dashboard_project_context(&graph))); + } +} + +#[cfg(feature = "test-transport")] +#[doc(hidden)] +#[allow(clippy::too_many_arguments)] +pub async fn run_until_shutdown_for_tests_with_host_admission( + graph: std::sync::Arc, + authority: DashboardHostAdmissionTestAuthorityV1, + project_graphs: DashboardTestProjectGraphsV1, + endpoint: DashboardTestEndpointV1<'_>, + build_version: &'static str, + spa_routes: axum::Router, + shutdown: F, +) -> tracedecay_domain::errors::Result<()> +where + F: std::future::Future + Send + 'static, +{ + tracedecay_dashboard_api::run_until_shutdown_for_tests_with_host_admission( + std::sync::Arc::new(dashboard_project_context(&graph)), + authority, + project_graphs.contexts, + endpoint, + build_version, + spa_routes, + shutdown, + ) + .await +} + /// Composes the production dashboard automation authority over one retained /// integration-test graph. The returned writer is the same serialization /// authority captured by managed-skill mutation/materialization and must be diff --git a/crates/tracedecay/src/mcp/server/construction.rs b/crates/tracedecay/src/mcp/server/construction.rs index 24642cc329..91e85038ad 100644 --- a/crates/tracedecay/src/mcp/server/construction.rs +++ b/crates/tracedecay/src/mcp/server/construction.rs @@ -99,8 +99,7 @@ pub(crate) fn dashboard_retained_project_graph_resolver( } None => None, }; - Ok(graph - .map(|graph| graph as Arc)) + Ok(graph.map(|graph| Arc::new(crate::dashboard::dashboard_project_context(&graph)))) }) }) } diff --git a/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs b/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs index d310004024..dcf6d618ab 100644 --- a/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs +++ b/crates/tracedecay/src/mcp/tools/handlers/dashboard.rs @@ -824,8 +824,8 @@ pub(super) async fn handle_dashboard( "retained dashboard project server resolved a different root", )); } - let retained_cg: Arc = - retained_graph; + let retained_cg = + Arc::new(crate::dashboard::dashboard_project_context(&retained_graph)); let dashboard_project_graph_resolver = retained_project_server_resolver .clone() .zip(daemon_user_profile_id.clone()) @@ -872,7 +872,7 @@ pub(super) async fn handle_dashboard( compose_dashboard_profile_code_index_worker_settings( database, profile_id, - retained_cg.project_root().to_path_buf(), + retained_cg.store_layout.project_root.clone(), &service, ) }); @@ -908,7 +908,7 @@ pub(super) async fn handle_dashboard( let delivery_read_authority = daemon_invocation_service.map(|service| { let adapter = super::dashboard_delivery::DashboardDeliveryReadAdapter::new( service, - retained_cg.project_root().to_path_buf(), + retained_cg.store_layout.project_root.clone(), ); Arc::new(adapter) as Arc }); diff --git a/crates/tracedecay/src/tracedecay.rs b/crates/tracedecay/src/tracedecay.rs index 73715baa96..2807514f67 100644 --- a/crates/tracedecay/src/tracedecay.rs +++ b/crates/tracedecay/src/tracedecay.rs @@ -16,6 +16,7 @@ use tracedecay_runtime_core::db::{Database, DatabaseStorageTelemetryHandle}; use tracedecay_runtime_core::storage::{self, StoreLayout}; use tracedecay_store_runtime::DaemonSessionRuntimeRegistryV1; +mod automation_context; #[cfg(test)] mod concrete_runtime_tests; mod diagnostics; @@ -23,8 +24,8 @@ mod edits; pub(crate) mod facts; mod lifecycle; mod move_symbol; -mod project_runtime_port; pub(crate) mod queries; +mod source_edit_runtime; pub use diagnostics::{BranchDiagnostics, TrackedBranchDiagnostic}; pub use lifecycle::MovedStoreAdoption; diff --git a/crates/tracedecay/src/tracedecay/automation_context.rs b/crates/tracedecay/src/tracedecay/automation_context.rs new file mode 100644 index 0000000000..653b8f7f3d --- /dev/null +++ b/crates/tracedecay/src/tracedecay/automation_context.rs @@ -0,0 +1,36 @@ +use tracedecay_automation_runtime::ports::project_runtime::AutomationProjectContext; +use tracedecay_domain::FactOwnerV1; +use tracedecay_domain::errors::{Result, TraceDecayError}; + +use super::TraceDecay; + +impl TraceDecay { + /// Captures the immutable authorities required by one project automation run. + /// + /// Read-only graphs are rejected because every project automation task may + /// publish memory, ledger, or managed-skill effects. + pub fn automation_project_context(&self) -> Result { + if self.is_read_only() { + return Err(TraceDecayError::Config { + message: + "cannot open project store for writing: active TraceDecay store is open read-only" + .to_owned(), + }); + } + let FactOwnerV1::Project { project_id } = self.project_memory_owner()? else { + return Err(TraceDecayError::Config { + message: "project automation requires authoritative project scope".to_owned(), + }); + }; + Ok(AutomationProjectContext { + project_root: self.project_root().to_path_buf(), + dashboard_root: self.store_layout().dashboard_root.clone(), + host_io: tracedecay_agent_hosts::host_io(), + project_id, + profile_id: self.project_store_runtime().profile_id().clone(), + profile_database: self.profile_database().clone(), + project_sessions: self.configuration_runtime().registered_database(), + project_memory_database: self.retained_project_store_db()?, + }) + } +} diff --git a/crates/tracedecay/src/tracedecay/queries/graph.rs b/crates/tracedecay/src/tracedecay/queries/graph.rs index c3f7d3606b..5a5052404d 100644 --- a/crates/tracedecay/src/tracedecay/queries/graph.rs +++ b/crates/tracedecay/src/tracedecay/queries/graph.rs @@ -7,6 +7,19 @@ use tracedecay_graph_query::{ open_verified_graph_query, }; +use crate::tracedecay::TraceDecay; + +impl TraceDecay { + pub(crate) fn source_read_context(&self) -> Option { + Some(SourceReadContext::new( + self.project_root().to_path_buf(), + self.db().clone(), + self.is_read_only(), + self.store_layout().identity.project_id.clone()?, + )) + } +} + struct BoundCodeGraphSourceAuthority { source: SourceReadContext, } diff --git a/crates/tracedecay/src/tracedecay/project_runtime_port.rs b/crates/tracedecay/src/tracedecay/source_edit_runtime.rs similarity index 52% rename from crates/tracedecay/src/tracedecay/project_runtime_port.rs rename to crates/tracedecay/src/tracedecay/source_edit_runtime.rs index 62af4836df..926d63a85f 100644 --- a/crates/tracedecay/src/tracedecay/project_runtime_port.rs +++ b/crates/tracedecay/src/tracedecay/source_edit_runtime.rs @@ -1,21 +1,10 @@ -//! Root-owned adapters for automation and dashboard project runtimes. +use std::path::Path; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use tracedecay_automation_runtime::automation::host_io::HostIo; -use tracedecay_automation_runtime::ports::project_runtime::{ProjectRuntime, RuntimeFuture}; -use tracedecay_configuration::UserSettingsDaemonClient; use tracedecay_contracts::source_edit::{ AstGrepResult, EditResult, InsertResult, MoveResult, MultiEditResult, RenameResult, RenameSymbolBindingV1, }; -use tracedecay_dashboard_api::DashboardProjectRuntime; -use tracedecay_domain::errors::{Result, TraceDecayError}; -use tracedecay_domain::{FactOwnerV1, ProjectId, UserProfileId}; -use tracedecay_global_db::RegisteredGlobalDbLeaseV1; -use tracedecay_graph_query::SourceReadContext; -use tracedecay_runtime_core::db::{Database, DatabaseStorageTelemetryHandle}; +use tracedecay_domain::errors::TraceDecayError; use tracedecay_runtime_core::storage::StoreLayout; use tracedecay_source_edit::{ EditDiagnosticRecord, SourceEditFuture, SourceEditGraphReadV1, SourceEditRuntimePort, @@ -23,92 +12,6 @@ use tracedecay_source_edit::{ use super::TraceDecay; -impl ProjectRuntime for TraceDecay { - fn project_root(&self) -> &Path { - TraceDecay::project_root(self) - } - - fn host_io(&self) -> HostIo { - tracedecay_agent_hosts::host_io() - } - - fn db(&self) -> &Database { - TraceDecay::db(self) - } - - fn store_layout(&self) -> &StoreLayout { - TraceDecay::store_layout(self) - } - - fn project_memory_owner(&self) -> Result { - TraceDecay::project_memory_owner(self) - } - - fn profile_id(&self) -> &UserProfileId { - self.project_store_runtime().profile_id() - } - - fn profile_database(&self) -> &RegisteredGlobalDbLeaseV1 { - TraceDecay::profile_database(self) - } - - fn project_sessions( - &self, - project_id: ProjectId, - roots: Vec, - ) -> RuntimeFuture<'_, RegisteredGlobalDbLeaseV1> { - Box::pin(async move { - TraceDecay::project_store_runtime(self) - .project_sessions(project_id, roots) - .await - }) - } - - fn open_project_store_db(&self) -> RuntimeFuture<'_, Database> { - Box::pin(TraceDecay::open_project_store_db(self)) - } -} - -impl DashboardProjectRuntime for TraceDecay { - fn project_root(&self) -> &Path { - TraceDecay::project_root(self) - } - - fn store_layout(&self) -> &StoreLayout { - TraceDecay::store_layout(self) - } - - fn automation_runtime(&self) -> &(dyn ProjectRuntime + 'static) { - self - } - - fn dashboard_db_path(&self) -> PathBuf { - TraceDecay::dashboard_db_path(self) - } - - fn dashboard_database_guard(&self) -> Arc { - TraceDecay::dashboard_database_guard(self) - } - - fn storage_telemetry_handle(&self) -> Result { - TraceDecay::storage_telemetry_handle(self) - } - - fn retention_config(&self) -> tracedecay_dashboard_api::config::RetentionConfig { - tracedecay_dashboard_api::config::RetentionConfig { - store_soft_budgets_bytes: TraceDecay::get_config(self) - .sync - .retention - .store_soft_budgets_bytes - .clone(), - } - } - - fn user_settings_client(&self) -> Arc { - TraceDecay::configuration_runtime(self).user_settings_client() - } -} - impl SourceEditRuntimePort for TraceDecay { fn project_root(&self) -> &Path { TraceDecay::project_root(self) @@ -237,14 +140,3 @@ impl SourceEditRuntimePort for TraceDecay { )) } } - -impl TraceDecay { - pub(crate) fn source_read_context(&self) -> Option { - Some(SourceReadContext::new( - TraceDecay::project_root(self).to_path_buf(), - TraceDecay::db(self).clone(), - TraceDecay::is_read_only(self), - self.store_layout().identity.project_id.clone()?, - )) - } -} diff --git a/crates/tracedecay/tests/automation_runner_test/combined_review.rs b/crates/tracedecay/tests/automation_runner_test/combined_review.rs index 43f77f17d0..7ca0308686 100644 --- a/crates/tracedecay/tests/automation_runner_test/combined_review.rs +++ b/crates/tracedecay/tests/automation_runner_test/combined_review.rs @@ -153,7 +153,7 @@ async fn combined_review_runner_records_both_tasks_from_one_backend_call() { let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -259,7 +259,7 @@ async fn retained_combined_review_defers_both_ledgers_and_holds_both_task_locks( let retrieval = FixtureAutomationSessionRetrieval::new(&cg); let retained = run_combined_review_with_backend_and_retrieval_for_retained_settlement( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -320,7 +320,7 @@ async fn retained_combined_review_defers_recorded_failures_until_settlement() { let retrieval = FixtureAutomationSessionRetrieval::new(&cg); let retained = run_combined_review_with_backend_and_retrieval_for_retained_settlement( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -434,7 +434,7 @@ async fn combined_review_not_dispatched_when_only_one_task_is_due() { let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -483,7 +483,7 @@ async fn combined_review_not_dispatched_when_skill_writer_is_not_due() { let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -523,7 +523,7 @@ async fn combined_review_task_configuration_skips_before_retrieval_or_backend() let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -576,7 +576,7 @@ async fn combined_review_active_task_locks_skip_before_retrieval_or_backend() { let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -617,7 +617,7 @@ async fn combined_review_respects_escape_hatch_flag() { let retrieval = CountingAutomationSessionRetrieval::new(&cg); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -658,7 +658,7 @@ async fn combined_review_falls_back_when_evidence_is_unavailable() { let dispatch = tracedecay_automation_runtime::automation::runner::run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -696,7 +696,7 @@ async fn combined_review_terminal_evidence_matrix_has_zero_effects() { let dispatch = tracedecay_automation_runtime::automation::runner::run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -733,7 +733,7 @@ async fn combined_review_terminal_evidence_matrix_has_zero_effects() { let retrieval = EmptyAutomationSessionRetrieval::new(); let dispatch = tracedecay_automation_runtime::automation::runner::run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -773,7 +773,7 @@ async fn combined_review_preserves_reflector_budget_stage_for_fallback() { ); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -817,7 +817,7 @@ async fn combined_review_preserves_skill_budget_stage_for_fallback() { ); let dispatch = run_combined_review_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, diff --git a/crates/tracedecay/tests/automation_runner_test/memory_curator.rs b/crates/tracedecay/tests/automation_runner_test/memory_curator.rs index 63380f6cb1..eca55034fd 100644 --- a/crates/tracedecay/tests/automation_runner_test/memory_curator.rs +++ b/crates/tracedecay/tests/automation_runner_test/memory_curator.rs @@ -75,7 +75,7 @@ async fn memory_curator_empty_store_skips_without_a_backend_attempt_and_releases let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -159,7 +159,7 @@ async fn memory_curator_repairs_then_applies_validated_ops_and_records_ledger() let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -653,7 +653,7 @@ async fn memory_curator_persists_transient_transient_success_retry_receipt() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -700,7 +700,7 @@ async fn scheduler_memory_curator_applies_validated_ops_automatically() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -746,7 +746,7 @@ async fn memory_curator_runner_artifacts_block_handoff_without_validation_exampl let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -837,7 +837,7 @@ async fn memory_curator_runner_artifacts_mark_handoff_ready_for_accepted_only_ex let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -927,7 +927,7 @@ async fn memory_curator_runner_applies_validated_ops_under_apply_policy() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -990,7 +990,7 @@ async fn memory_curator_quarantines_legacy_output_after_bounded_repair_exhaustio let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let error = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -1062,7 +1062,7 @@ async fn memory_curator_runner_auto_applies_validated_operations() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -1157,7 +1157,7 @@ async fn memory_curator_stops_before_backend_or_apply_when_caller_is_interrupted let run_control = test_automation_run_control(Arc::clone(&interrupted)); let error = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, diff --git a/crates/tracedecay/tests/automation_runner_test/memory_curator/manual_trigger.rs b/crates/tracedecay/tests/automation_runner_test/memory_curator/manual_trigger.rs index 5310d56be1..8090ea8122 100644 --- a/crates/tracedecay/tests/automation_runner_test/memory_curator/manual_trigger.rs +++ b/crates/tracedecay/tests/automation_runner_test/memory_curator/manual_trigger.rs @@ -22,7 +22,7 @@ async fn manual_memory_curator_runs_when_scheduling_and_task_are_disabled() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -59,7 +59,7 @@ async fn manual_memory_curator_skips_when_backend_is_disabled() { let run_control = test_automation_run_control(Arc::new(AtomicBool::new(false))); let run = tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, diff --git a/crates/tracedecay/tests/automation_runner_test/memory_curator/pagination.rs b/crates/tracedecay/tests/automation_runner_test/memory_curator/pagination.rs index 78e90eff5a..c6b9fc472f 100644 --- a/crates/tracedecay/tests/automation_runner_test/memory_curator/pagination.rs +++ b/crates/tracedecay/tests/automation_runner_test/memory_curator/pagination.rs @@ -64,7 +64,7 @@ async fn memory_curator_resumes_from_the_durable_next_page_cursor() { for _ in 0..2 { tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, diff --git a/crates/tracedecay/tests/automation_runner_test/session_reflector.rs b/crates/tracedecay/tests/automation_runner_test/session_reflector.rs index d7f83e029d..4c0c6da6c9 100644 --- a/crates/tracedecay/tests/automation_runner_test/session_reflector.rs +++ b/crates/tracedecay/tests/automation_runner_test/session_reflector.rs @@ -67,7 +67,7 @@ async fn retained_session_reflector_preserves_retrieval_and_defers_ledger_public let retrieval = FixtureAutomationSessionRetrieval::new(&cg); let backend = SessionJsonBackend::new(json!({"facts": []})); let retained = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval_for_retained_settlement( - &cg, + &automation_project_context(&cg), &scheduler_config(Some(3600), None), &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -229,7 +229,7 @@ async fn session_reflector_fails_closed_on_stale_temporal_evidence() { }; let run = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -299,7 +299,7 @@ async fn project_runners_keep_distinct_budget_stages_in_terminal_reports_and_led ); let reflector = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -310,7 +310,7 @@ async fn project_runners_keep_distinct_budget_stages_in_terminal_reports_and_led .await .unwrap(); let skill = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &skill_backend, @@ -393,7 +393,7 @@ async fn project_reflector_and_skill_writer_terminal_evidence_matrix_has_zero_wr let reflector = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -404,7 +404,7 @@ async fn project_reflector_and_skill_writer_terminal_evidence_matrix_has_zero_wr .await .unwrap(); let skill = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &skill_backend, @@ -460,7 +460,7 @@ async fn project_reflector_and_skill_writer_terminal_evidence_matrix_has_zero_wr }; let reflector = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -472,7 +472,7 @@ async fn project_reflector_and_skill_writer_terminal_evidence_matrix_has_zero_wr .unwrap(); let skill = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &skill_backend, @@ -1364,7 +1364,7 @@ async fn session_reflector_replays_recent_sessions_without_keyword_matches() { }; let run = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -1463,7 +1463,7 @@ async fn session_reflector_skips_when_replay_disabled_and_no_grep_hits() { }; let run = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), @@ -1581,7 +1581,7 @@ async fn session_reflector_replay_respects_include_summaries_false() { ); let run = tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_automation_run_control(Arc::new(AtomicBool::new(false))), &test_configuration_revision(), diff --git a/crates/tracedecay/tests/automation_runner_test/skill_writer.rs b/crates/tracedecay/tests/automation_runner_test/skill_writer.rs index d7362fe489..2192b5e7ca 100644 --- a/crates/tracedecay/tests/automation_runner_test/skill_writer.rs +++ b/crates/tracedecay/tests/automation_runner_test/skill_writer.rs @@ -15,7 +15,7 @@ async fn retained_skill_writer_preserves_retrieval_and_defers_ledger_publication "insufficient_repeated_evidence", )); let retained = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval_for_retained_settlement( - &cg, + &automation_project_context(&cg), &enabled_skill_writer_config(), &test_configuration_revision(), &backend, @@ -112,7 +112,7 @@ async fn skill_writer_fails_closed_on_denied_temporal_evidence() { let run = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &enabled_skill_writer_config(), &test_configuration_revision(), &backend, @@ -233,7 +233,7 @@ async fn skill_writer_replays_recent_sessions_without_keyword_matches() { let run = tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -264,7 +264,7 @@ async fn skill_writer_skips_when_replay_disabled_and_no_grep_hits() { let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -706,7 +706,7 @@ async fn skill_writer_evidence_imports_project_skill_usage_analytics_before_summ let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, @@ -742,7 +742,7 @@ async fn skill_writer_evidence_includes_underused_tool_family_summary() { let config = enabled_skill_writer_config(); let run = run_skill_writer_with_backend_and_retrieval( - &cg, + &automation_project_context(&cg), &config, &test_configuration_revision(), &backend, diff --git a/crates/tracedecay/tests/automation_runner_test/support.rs b/crates/tracedecay/tests/automation_runner_test/support.rs index 31f155cefb..1197c79602 100644 --- a/crates/tracedecay/tests/automation_runner_test/support.rs +++ b/crates/tracedecay/tests/automation_runner_test/support.rs @@ -60,6 +60,13 @@ pub(crate) use tracedecay_sessions::runtime::{SessionMessageRecord, SessionRecor pub(crate) static ENV_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); +pub(crate) fn automation_project_context( + cg: &TraceDecay, +) -> tracedecay_automation_runtime::ports::project_runtime::AutomationProjectContext { + cg.automation_project_context() + .expect("automation project context") +} + pub(crate) fn test_automation_run_control(interrupted: Arc) -> AutomationRunControl { let observed = Arc::clone(&interrupted); AutomationRunControl::from_interrupted(Arc::new(move || observed.load(Ordering::Acquire))) @@ -284,8 +291,9 @@ pub(crate) async fn run_session_reflector_with_backend( AutomationRunError, > { let retrieval = FixtureAutomationSessionRetrieval::new(cg); + let context = automation_project_context(cg); tracedecay_automation_runtime::automation::runner::run_session_reflector_with_backend_and_retrieval( - cg, + &context, config, run_control, &test_configuration_revision(), @@ -306,8 +314,9 @@ pub(crate) async fn run_skill_writer_with_backend( AutomationRunError, > { let retrieval = FixtureAutomationSessionRetrieval::new(cg); + let context = automation_project_context(cg); tracedecay_automation_runtime::automation::runner::run_skill_writer_with_backend_and_retrieval( - cg, + &context, config, &test_configuration_revision(), backend, @@ -325,8 +334,9 @@ pub(crate) async fn run_combined_review_with_backend( options: CombinedReviewAutomationOptions, ) -> tracedecay_domain::errors::Result { let retrieval = FixtureAutomationSessionRetrieval::new(cg); + let context = automation_project_context(cg); tracedecay_automation_runtime::automation::runner::run_combined_review_with_backend_and_retrieval( - cg, + &context, config, &test_configuration_revision(), backend, @@ -347,8 +357,9 @@ pub(crate) async fn run_memory_curator_with_backend( tracedecay_automation_runtime::automation::runner::MemoryCuratorAutomationRun, AutomationRunError, > { + let context = automation_project_context(cg); tracedecay_automation_runtime::automation::runner::run_memory_curator_with_backend( - cg, + &context, config, &test_configuration_revision(), backend, diff --git a/crates/tracedecay/tests/automation_runner_test/support/fixtures.rs b/crates/tracedecay/tests/automation_runner_test/support/fixtures.rs index 7530a897dd..ace53d22ef 100644 --- a/crates/tracedecay/tests/automation_runner_test/support/fixtures.rs +++ b/crates/tracedecay/tests/automation_runner_test/support/fixtures.rs @@ -10,7 +10,6 @@ use tracedecay_automation_runtime::automation::automatic_facts::record_session_a use tracedecay_automation_runtime::automation::run_ledger::{ AutomationRunLedgerRecord, read_run_artifact_payload, }; -use tracedecay_automation_runtime::ports::project_runtime::ProjectRuntime; use tracedecay_domain::FactOwnerV1; use tracedecay_global_db::ParseOffset; use tracedecay_sessions::admission::HostAdmissionScope; @@ -72,10 +71,11 @@ pub(crate) async fn seed_project_session_activity_at(cg: &TraceDecay, timestamp: let FactOwnerV1::Project { project_id } = project_memory_owner(cg) else { panic!("combined-review fixtures require an authoritative project owner"); }; - let sessions = cg - .project_sessions(project_id, vec![cg.project_root().to_path_buf()]) - .await - .expect("project sessions mount"); + let context = cg + .automation_project_context() + .expect("automation project context"); + assert_eq!(context.project_id, project_id); + let sessions = &context.project_sessions; let session = SessionRecord { provider: "cursor".to_string(), session_id: format!("combined-activity-{timestamp}"),