diff --git a/epiphany-core/src/bin/epiphany-mvp-status.rs b/epiphany-core/src/bin/epiphany-mvp-status.rs index e1a2c438..3513cfe2 100644 --- a/epiphany-core/src/bin/epiphany-mvp-status.rs +++ b/epiphany-core/src/bin/epiphany-mvp-status.rs @@ -210,16 +210,15 @@ fn run_native_status(args: &Args, include_auxiliary_status: bool) -> Result EpiphanyCurrentWorkProjection { EpiphanyCurrentWorkProjection { mind_projection_digest: String::new(), body_modeling: None, - body_modeling_action: None, research_continuation_action: None, frontier_planning_stage: RepoFrontierPlanningLifecycleStage::Unavailable, proposal_modeling: None, diff --git a/epiphany-core/src/current_work.rs b/epiphany-core/src/current_work.rs index 4f216ccb..4ab04a41 100644 --- a/epiphany-core/src/current_work.rs +++ b/epiphany-core/src/current_work.rs @@ -19,8 +19,7 @@ pub const BODY_MODELING_LAUNCH_BINDING_SCHEMA_VERSION: &str = #[serde(rename_all = "camelCase")] pub struct EpiphanyCurrentWorkProjection { pub mind_projection_digest: String, - pub body_modeling: Option, - pub body_modeling_action: Option, + pub body_modeling: Option, pub research_continuation_action: Option, pub frontier_planning_stage: RepoFrontierPlanningLifecycleStage, pub proposal_modeling: Option, @@ -99,6 +98,14 @@ pub struct EpiphanyBodyModelingWorkProjection { pub repo_model_basis: EpiphanyRepoModelBasis, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EpiphanyBodyModelingCurrentWorkProjection { + pub work: EpiphanyBodyModelingWorkProjection, + pub action: EpiphanyAgentPassContinuationAction, + pub job_id: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.runtime.body_modeling_launch.v1", @@ -331,7 +338,7 @@ pub fn project_current_work(store_path: impl AsRef) -> Result) -> Result { - let action = body_modeling_continuation_action(&cache, &work.work_id)?; - ( - (action == EpiphanyAgentPassContinuationAction::Launch).then_some(work), - Some(action), - ) + let (action, job_id) = + body_modeling_continuation_action(&cache, &work.work_id)?; + Some(EpiphanyBodyModelingCurrentWorkProjection { + work, + action, + job_id, + }) } - None => (None, None), + None => None, } } - (None, None) | (Some(_), None) => (None, None), + (None, None) | (Some(_), None) => None, (None, Some(_)) => { return Err(anyhow!( "current work has a RepoModel but no admitted repository Body observation" @@ -375,7 +384,6 @@ pub fn project_current_work(store_path: impl AsRef) -> Result Result { +) -> Result<(EpiphanyAgentPassContinuationAction, Option)> { let mut bindings = cache .get_all::()? .into_iter() @@ -699,12 +707,12 @@ fn body_modeling_continuation_action( .collect::>(); bindings.sort_by_key(|binding| binding.attempt_ordinal); let Some(binding) = bindings.last() else { - return Ok(EpiphanyAgentPassContinuationAction::Launch); + return Ok((EpiphanyAgentPassContinuationAction::Launch, None)); }; let job = cache .get::(&binding.job_id)? .ok_or_else(|| anyhow!("Body Modeling launch binding lost its runtime job"))?; - Ok(match job.status { + let action = match job.status { crate::EpiphanyRuntimeJobStatus::Failed | crate::EpiphanyRuntimeJobStatus::Cancelled => { EpiphanyAgentPassContinuationAction::Launch } @@ -719,7 +727,8 @@ fn body_modeling_continuation_action( } } _ => EpiphanyAgentPassContinuationAction::Wait, - }) + }; + Ok((action, Some(binding.job_id.clone()))) } fn current_proposal_modeling_work( @@ -845,34 +854,16 @@ pub fn body_modeling_continuation_action_for_job( { return Ok(None); } - Ok(Some(body_modeling_continuation_action( - &cache, - &binding.work_id, - )?)) + Ok(Some( + body_modeling_continuation_action(&cache, &binding.work_id)?.0, + )) } pub fn current_body_modeling_review_job_id(store_path: impl AsRef) -> Result> { - let store_path = store_path.as_ref(); - if crate::project_current_work(store_path)?.body_modeling_action - != Some(EpiphanyAgentPassContinuationAction::Review) - { - return Ok(None); - } - current_body_modeling_job_id(store_path) -} - -pub fn current_body_modeling_job_id(store_path: impl AsRef) -> Result> { - let store_path = store_path.as_ref(); - let mut cache = crate::runtime_spine_cache(store_path)?; - cache.pull_all_backing_stores()?; - let work = current_body_modeling_work(store_path)?; - let mut bindings = cache - .get_all::()? - .into_iter() - .filter(|binding| binding.work_id == work.work_id) - .collect::>(); - bindings.sort_by_key(|binding| binding.attempt_ordinal); - Ok(bindings.last().map(|binding| binding.job_id.clone())) + Ok(crate::project_current_work(store_path)? + .body_modeling + .filter(|work| work.action == EpiphanyAgentPassContinuationAction::Review) + .and_then(|work| work.job_id)) } pub fn proposal_modeling_continuation_action_for_job( @@ -2654,7 +2645,7 @@ pub fn launch_current_body_modeling_work( cache.get::(&work.work_id)?, )? .is_none() - || body_modeling_continuation_action(&cache, &work.work_id)? + || body_modeling_continuation_action(&cache, &work.work_id)?.0 != EpiphanyAgentPassContinuationAction::Launch { return Err(anyhow!("Mind has no launchable Body Modeling work")); @@ -3355,10 +3346,13 @@ mod tests { assert_eq!(projected_work.body_basis, body); assert_eq!(crate::repository_body_read_counters(), (0, 0)); let current_work = project_current_work(&store)?; - assert_eq!(current_work.body_modeling, Some(projected_work.clone())); assert_eq!( - current_work.body_modeling_action, - Some(EpiphanyAgentPassContinuationAction::Launch) + current_work.body_modeling, + Some(EpiphanyBodyModelingCurrentWorkProjection { + work: projected_work.clone(), + action: EpiphanyAgentPassContinuationAction::Launch, + job_id: None, + }) ); assert_eq!( current_work.mind_projection_digest, @@ -3369,11 +3363,15 @@ mod tests { .body_modeling .as_mut() .expect("fixture has Body Modeling work") + .work .work_id .push_str("-distinct-request"); assert_eq!( - distinct_launch.body_modeling_action, - current_work.body_modeling_action + distinct_launch + .body_modeling + .as_ref() + .map(|work| work.action), + current_work.body_modeling.as_ref().map(|work| work.action) ); assert_ne!( distinct_launch.projection_digest()?, @@ -3418,10 +3416,15 @@ mod tests { assert_eq!(scheduled.work_id, projected_work.work_id); assert_eq!(scheduled.attempt_ordinal, 0); let scheduled_work = project_current_work(&store)?; - assert!(scheduled_work.body_modeling.is_none()); assert_eq!( - scheduled_work.body_modeling_action, - Some(EpiphanyAgentPassContinuationAction::Wait) + scheduled_work + .body_modeling + .as_ref() + .map(|work| (work.action, work.job_id.as_deref())), + Some(( + EpiphanyAgentPassContinuationAction::Wait, + Some("body-scheduled-job") + )) ); let resident_policy = crate::ResidentSelfPolicy { workspace: temp.path().join("workspace"), @@ -3491,8 +3494,65 @@ mod tests { assert_eq!(crate::repository_body_read_counters(), (0, 0)); let mut cache = crate::runtime_spine_cache(&store)?; cache.pull_all_backing_stores()?; + let mut failed_job = cache + .get::("body-scheduled-job")? + .expect("scheduled Body job exists"); + failed_job.status = crate::EpiphanyRuntimeJobStatus::Failed; + failed_job.updated_at = "2026-08-17T00:00:03Z".into(); + cache.put(&failed_job.job_id, &failed_job)?; + let retry_work = project_current_work(&store)?; + assert_eq!( + retry_work + .body_modeling + .as_ref() + .map(|work| (work.action, work.job_id.as_deref())), + Some(( + EpiphanyAgentPassContinuationAction::Launch, + Some("body-scheduled-job") + )) + ); + assert_ne!( + retry_work.projection_digest()?, + current_work.projection_digest()?, + "a failed Body attempt must change exact current-work identity before retry" + ); + assert!(crate::ingest_resident_self_current_work_pressure( + &resident_store, + &store, + 4, + )?); + assert!( + !crate::ingest_resident_self_current_work_pressure(&resident_store, &store, 5)?, + "unchanged failed-attempt state must remain idempotent" + ); + let retry_grant = crate::heartbeat_issue_resident_self_grant( + &resident_store, + "current-work-retry-schedule", + "current-work-retry-action", + 6, + )? + .expect("failed Body attempt must receive a fresh exact-state grant"); + assert_eq!( + retry_grant.provenance_ref, + format!( + "{}{}{}", + crate::RESIDENT_SELF_CURRENT_WORK_PROVENANCE_PREFIX, + retry_work.projection_digest()?, + "/launchModeling" + ) + ); + let retried = launch_current_body_modeling_work( + &store, + EpiphanyBodyModelingLaunchOptions { + job_id: "body-retry-job".into(), + created_at: "2026-08-17T00:00:04Z".into(), + }, + )?; + assert_eq!(retried.attempt_ordinal, 1); + let mut cache = crate::runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; let launch = cache - .get::("body-scheduled-job")? + .get::("body-retry-job")? .unwrap(); crate::reset_repository_body_read_counters(); let reasoning_basis = crate::worker_reasoning_basis(&store, &launch)?; @@ -3565,7 +3625,10 @@ mod tests { job.updated_at = "2026-08-17T00:00:04Z".into(); cache.put(&job.job_id, &job)?; assert_eq!( - project_current_work(&store)?.body_modeling_action, + project_current_work(&store)? + .body_modeling + .as_ref() + .map(|work| work.action), Some(EpiphanyAgentPassContinuationAction::Review) ); assert_eq!( @@ -3582,7 +3645,6 @@ mod tests { ); let completed_work = project_current_work(&store)?; assert!(completed_work.body_modeling.is_none()); - assert!(completed_work.body_modeling_action.is_none()); assert!(crate::resident_self::resident_self_current_work_action(&store)?.is_none()); assert_eq!(crate::repository_body_read_counters(), (0, 0)); assert_current_work_reentry_is_read_only(&store, &completed_work)?; @@ -5479,7 +5541,7 @@ mod tests { assert_eq!(packet.decision_context_id, research_context.context_id); let after_research = project_current_work(&store)?; assert!(after_research.research_continuation_action.is_none()); - assert!(after_research.body_modeling_action.is_none()); + assert!(after_research.body_modeling.is_none()); assert!(after_research.proposal_modeling.is_none()); assert!(after_research.frontier_verdict_modeling.is_none()); assert_current_work_reentry_is_read_only(&store, &after_research)?; diff --git a/epiphany-core/src/surfaces/coordinator.rs b/epiphany-core/src/surfaces/coordinator.rs index a572c26c..f61ca0e4 100644 --- a/epiphany-core/src/surfaces/coordinator.rs +++ b/epiphany-core/src/surfaces/coordinator.rs @@ -67,16 +67,17 @@ pub fn coordinator_automation_action( mod tests { use super::*; use epiphany_core::{ - EpiphanyAgentPassContinuationAction, EpiphanyCoordinatorCrrcRecommendation, - EpiphanyCoordinatorInput, EpiphanyCrrcAction, RepoFrontierPlanningLifecycleStage, - RepoFrontierResearchContinuationAction, recommend_coordinator_action, + EpiphanyAgentPassContinuationAction, EpiphanyBodyModelingCurrentWorkProjection, + EpiphanyBodyModelingWorkProjection, EpiphanyCoordinatorCrrcRecommendation, + EpiphanyCoordinatorInput, EpiphanyCrrcAction, EpiphanyRepoModelBasis, + RepoFrontierPlanningLifecycleStage, RepoFrontierResearchContinuationAction, + RepositoryBodyObservationBasis, recommend_coordinator_action, }; fn current_work() -> epiphany_core::EpiphanyCurrentWorkProjection { epiphany_core::EpiphanyCurrentWorkProjection { mind_projection_digest: "sha256:mind".into(), body_modeling: None, - body_modeling_action: None, research_continuation_action: None, frontier_planning_stage: RepoFrontierPlanningLifecycleStage::Terminal, proposal_modeling: None, @@ -112,7 +113,31 @@ mod tests { #[test] fn exact_body_obligation_routes_modeling() { let mut input = input(); - input.current_work.body_modeling_action = Some(EpiphanyAgentPassContinuationAction::Launch); + input.current_work.body_modeling = Some(EpiphanyBodyModelingCurrentWorkProjection { + work: EpiphanyBodyModelingWorkProjection { + work_id: "body-work".into(), + runtime_id: "runtime".into(), + body_basis: RepositoryBodyObservationBasis { + schema_version: "body-basis".into(), + workspace_id: "workspace".into(), + swarm_id: "swarm".into(), + runtime_id: "runtime".into(), + scope: "git_worktree".into(), + body_binding_sha256: "sha256:body".into(), + observation_id: "observation".into(), + generation: 1, + manifest_root_sha256: "sha256:manifest".into(), + scan_started_at: "2026-08-22T00:00:00Z".into(), + scan_finished_at: "2026-08-22T00:00:01Z".into(), + }, + repo_model_basis: EpiphanyRepoModelBasis { + projection_digest: "sha256:model".into(), + source_documents: Vec::new(), + }, + }, + action: EpiphanyAgentPassContinuationAction::Launch, + job_id: None, + }); assert_eq!( recommend_coordinator_action(input).action, EpiphanyCoordinatorAction::LaunchModeling diff --git a/epiphany-core/src/surfaces/coordinator_decision.rs b/epiphany-core/src/surfaces/coordinator_decision.rs index 75e7c5e4..51cf2e3f 100644 --- a/epiphany-core/src/surfaces/coordinator_decision.rs +++ b/epiphany-core/src/surfaces/coordinator_decision.rs @@ -58,8 +58,8 @@ pub fn recommend_coordinator_action( if let Some(work) = input.current_work.proposal_modeling.as_ref() { return modeling_decision(work.action, "proposal Modeling"); } - if let Some(action) = input.current_work.body_modeling_action { - return modeling_decision(action, "Body Modeling"); + if let Some(work) = input.current_work.body_modeling.as_ref() { + return modeling_decision(work.action, "Body Modeling"); } if let Some(work) = input.current_work.frontier_verdict_modeling.as_ref() { return modeling_decision(work.action, "frontier-verdict Modeling"); diff --git a/epiphany-core/src/surfaces/role_board.rs b/epiphany-core/src/surfaces/role_board.rs index 6b7eb5e6..cfd63042 100644 --- a/epiphany-core/src/surfaces/role_board.rs +++ b/epiphany-core/src/surfaces/role_board.rs @@ -85,7 +85,11 @@ pub fn derive_role_board(input: EpiphanyRoleBoardInput) -> Vec