diff --git a/epiphany-core/src/current_work.rs b/epiphany-core/src/current_work.rs index 2d96e33a..15513bc2 100644 --- a/epiphany-core/src/current_work.rs +++ b/epiphany-core/src/current_work.rs @@ -12,6 +12,8 @@ use crate::{ pub const BODY_MODELING_DECISION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind.body_modeling_decision.v1"; +pub const BODY_MODELING_LAUNCH_BINDING_SCHEMA_VERSION: &str = + "epiphany.runtime.body_modeling_launch.v1"; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct EpiphanyCurrentWorkProjection { @@ -31,6 +33,36 @@ pub struct EpiphanyBodyModelingWorkProjection { pub repo_model_basis: EpiphanyRepoModelBasis, } +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.runtime.body_modeling_launch.v1", + schema = "EpiphanyBodyModelingLaunchBinding" +)] +pub struct EpiphanyBodyModelingLaunchBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_record_id: String, + #[cultcache(key = 2)] + pub work_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub attempt_ordinal: u64, + #[cultcache(key = 5)] + pub body_document_sha256: String, + #[cultcache(key = 6)] + pub observed_repo_model_projection_digest: String, + #[cultcache(key = 7)] + pub launched_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EpiphanyBodyModelingLaunchOptions { + pub job_id: String, + pub created_at: String, +} + #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.mind.body_modeling_decision.v1", @@ -136,6 +168,7 @@ impl EpiphanyBodyModelingDecisionReceipt { pub(crate) fn register_current_work_types(cache: &mut CultCache) -> Result<()> { cache.register_entry_type::()?; + cache.register_entry_type::()?; Ok(()) } @@ -222,9 +255,7 @@ pub fn unresolved_body_modeling_work( resolve_body_modeling_work(work, decision) } -pub fn project_current_work( - store_path: impl AsRef, -) -> Result { +pub fn project_current_work(store_path: impl AsRef) -> Result { let store_path = store_path.as_ref(); let mind = crate::assemble_mind_view(store_path)?; let body_modeling = match ( @@ -239,10 +270,14 @@ pub fn project_current_work( )?; let mut cache = crate::runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; - resolve_body_modeling_work( + let unresolved = resolve_body_modeling_work( work.clone(), cache.get::(&work.work_id)?, - )? + )?; + match unresolved { + Some(work) if !body_modeling_work_is_covered(&cache, &work.work_id)? => Some(work), + _ => None, + } } (None, None) | (Some(_), None) => None, (None, Some(_)) => { @@ -257,12 +292,196 @@ pub fn project_current_work( research_continuation_action: crate::runtime_repo_frontier_research_lifecycle(store_path)? .continuation_action(), frontier_planning_stage: crate::runtime_repo_frontier_planning_lifecycle(store_path)?.stage, - proposal_modeling_request: - crate::runtime_pending_repo_frontier_proposal_modeling_request(store_path)?, + proposal_modeling_request: crate::runtime_pending_repo_frontier_proposal_modeling_request( + store_path, + )?, hands_frontier_ready: crate::runtime_has_actionable_hands_frontier(store_path)?, }) } +fn body_modeling_work_is_covered(cache: &CultCache, work_id: &str) -> Result { + for binding in cache.get_all::()? { + if binding.work_id != work_id { + continue; + } + let job = cache + .get::(&binding.job_id)? + .ok_or_else(|| anyhow!("Body Modeling launch binding lost its runtime job"))?; + if !matches!( + job.status, + crate::EpiphanyRuntimeJobStatus::Failed | crate::EpiphanyRuntimeJobStatus::Cancelled + ) { + return Ok(true); + } + } + Ok(false) +} + +pub fn launch_current_body_modeling_work( + store_path: impl AsRef, + options: EpiphanyBodyModelingLaunchOptions, +) -> Result { + let store_path = store_path.as_ref(); + if options.job_id.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&options.created_at).is_err() + { + return Err(anyhow!("Body Modeling launch options are invalid")); + } + let current_work = project_current_work(store_path)?; + let work = current_work + .body_modeling + .ok_or_else(|| anyhow!("Mind has no unresolved Body Modeling work"))?; + let mut cache = crate::runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let mind = crate::assemble_mind_view(store_path)?; + let body_source = mind + .source_documents + .iter() + .find(|source| { + source.document_type == crate::EpiphanyMindRepositoryBodyObservationDocument::TYPE + && source.document_key == work.body_basis.observation_id + }) + .cloned() + .ok_or_else(|| anyhow!("Body Modeling launch lost its admitted Body source"))?; + let attempt_ordinal = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.work_id == work.work_id) + .map(|binding| binding.attempt_ordinal) + .max() + .map_or(0, |ordinal| ordinal + 1); + let binding_record_id = format!( + "body-modeling-launch-{}-attempt-{attempt_ordinal}", + work.work_id + ); + let launch_document = + crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument { + thread_id: binding_record_id.clone(), + role_id: "modeling".into(), + state_revision: 0, + objective: None, + dynamic_prompt_context: None, + repository_body_observation_basis: Some(work.body_basis.clone()), + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, + frontier_research_context: None, + frontier_plan_mind_context: None, + imagination_consideration_context: None, + admitted_model_direction_consideration_context: None, + active_subgoal_id: None, + active_subgoals: Vec::new(), + active_graph_node_ids: Vec::new(), + investigation_checkpoint: None, + scratch: None, + invariants: Vec::new(), + graphs: None, + recent_evidence: Vec::new(), + recent_observations: Vec::new(), + graph_frontier: None, + graph_checkpoint: None, + planning: None, + churn: None, + }); + let authority_scope = "epiphany.role.modeling".to_string(); + let output_contract_id = launch_document.output_contract_id().to_string(); + let instruction = "Act as Epiphany Modeling. Model the admitted repository Body against the exact keyed Mind projection and return the typed role result contract.".to_string(); + let prepared = crate::prepare_runtime_spine_heartbeat_job( + &cache, + crate::RuntimeSpineHeartbeatJobOptions { + runtime_id: work.runtime_id.clone(), + display_name: "Epiphany Local".into(), + session_id: crate::EPIPHANY_RUNTIME_ROOT_SESSION_ID.into(), + objective: "Model the admitted repository Body".into(), + coordinator_note: "Keyed Mind current-work launch transaction opened this session." + .into(), + job_id: options.job_id.clone(), + role: crate::EPIPHANY_MODELING_OWNER_ROLE.into(), + binding_id: crate::EPIPHANY_MODELING_ROLE_BINDING_ID.into(), + authority_scope: authority_scope.clone(), + instruction: instruction.clone(), + launch_document: launch_document.clone(), + output_contract_id: output_contract_id.clone(), + organ_launch_contract: crate::default_launch_organ_contract( + &authority_scope, + launch_document.document_kind(), + &output_contract_id, + ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, + imagination_consideration_request_id: None, + admitted_model_direction_consideration_request_id: None, + repo_frontier_modeling_request_id: None, + repo_frontier_research_request_id: None, + repo_frontier_verdict_modeling_authority: None, + created_at: options.created_at.clone(), + }, + )?; + let binding = EpiphanyBodyModelingLaunchBinding { + schema_version: BODY_MODELING_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_record_id, + work_id: work.work_id, + job_id: options.job_id, + attempt_ordinal, + body_document_sha256: body_source.payload_sha256.clone(), + observed_repo_model_projection_digest: work.repo_model_basis.projection_digest.clone(), + launched_at: options.created_at.clone(), + }; + let grant = crate::substrate_gate::substrate_gate_repo_access_grant_for_worker( + format!("substrate-grant-{}", binding.job_id), + binding.job_id.clone(), + crate::EPIPHANY_MODELING_ROLE_BINDING_ID.into(), + crate::EPIPHANY_MODELING_OWNER_ROLE.into(), + authority_scope, + false, + options.created_at, + ); + let snapshot = cache.snapshot_envelopes(); + let mut expected = Vec::new(); + for source in [&body_source] { + let envelope = snapshot + .iter() + .find(|value| value.r#type == source.document_type && value.key == source.document_key) + .ok_or_else(|| anyhow!("Body Modeling launch lost a strong Mind source"))?; + if EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", envelope)? != *source { + return Err(anyhow!("Body Modeling launch strong Mind source changed")); + } + if !expected.contains(envelope) { + expected.push(envelope.clone()); + } + } + let mut replacements = prepared.envelopes; + for replacement in &replacements { + if let Some(existing) = snapshot + .iter() + .find(|value| value.r#type == replacement.r#type && value.key == replacement.key) + { + if !expected.contains(existing) { + expected.push(existing.clone()); + } + } + } + replacements.push(cache.prepare_entry(&binding.binding_record_id, &binding)?.0); + replacements.push(cache.prepare_entry(&grant.receipt_id, &grant)?.0); + for envelope in &expected { + if !replacements.iter().any(|replacement| { + replacement.r#type == envelope.r#type && replacement.key == envelope.key + }) { + replacements.push(envelope.clone()); + } + } + if !crate::runtime_store_backend::runtime_spine_backing_store(store_path)? + .compare_and_swap_batch(&expected, replacements)? + { + return Err(anyhow!( + "Body Modeling launch lost its exact Mind/runtime CAS" + )); + } + Ok(binding) +} + fn resolve_body_modeling_work( work: EpiphanyBodyModelingWorkProjection, receipt: Option, @@ -452,6 +671,46 @@ mod tests { crate::assemble_mind_view(&store)?.projection_digest ); assert_eq!(crate::repository_body_read_counters(), (0, 0)); + let scheduled = launch_current_body_modeling_work( + &store, + EpiphanyBodyModelingLaunchOptions { + job_id: "body-scheduled-job".into(), + created_at: "2026-08-17T00:00:02Z".into(), + }, + )?; + assert_eq!(scheduled.work_id, projected_work.work_id); + assert_eq!(scheduled.attempt_ordinal, 0); + assert!(project_current_work(&store)?.body_modeling.is_none()); + let mut scheduled_cache = crate::runtime_spine_cache(&store)?; + scheduled_cache.pull_all_backing_stores()?; + assert!( + scheduled_cache + .get::(crate::THREAD_STATE_KEY)? + .is_none() + ); + assert!( + scheduled_cache + .get::("body-scheduled-job")? + .is_some() + ); + assert!( + scheduled_cache + .get::( + "substrate-grant-body-scheduled-job" + )? + .is_some() + ); + assert!( + launch_current_body_modeling_work( + &store, + EpiphanyBodyModelingLaunchOptions { + job_id: "body-scheduled-job-duplicate".into(), + created_at: "2026-08-17T00:00:03Z".into(), + }, + ) + .is_err() + ); + assert_eq!(crate::repository_body_read_counters(), (0, 0)); let document = crate::EpiphanyWorkerLaunchDocument::Role(crate::EpiphanyRoleWorkerLaunchDocument { thread_id: "creation-thread".into(), diff --git a/epiphany-core/src/substrate_gate.rs b/epiphany-core/src/substrate_gate.rs index 33b748cb..68752751 100644 --- a/epiphany-core/src/substrate_gate.rs +++ b/epiphany-core/src/substrate_gate.rs @@ -144,21 +144,41 @@ pub fn substrate_gate_repo_access_grant_for_launch( runtime_job_id: String, request: &EpiphanyJobLaunchRequest, granted_at: String, +) -> SubstrateGateRepoAccessGrantReceipt { + substrate_gate_repo_access_grant_for_worker( + receipt_id, + runtime_job_id, + request.binding_id.clone(), + request.owner_role.clone(), + request.authority_scope.clone(), + request.binding_id == crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID, + granted_at, + ) +} + +pub fn substrate_gate_repo_access_grant_for_worker( + receipt_id: String, + runtime_job_id: String, + binding_id: String, + role: String, + authority_scope: String, + allow_public_source_read: bool, + granted_at: String, ) -> SubstrateGateRepoAccessGrantReceipt { let mut granted_operations = vec![ SUBSTRATE_GATE_SOURCE_READ_OPERATION.to_string(), "snapshot".to_string(), ]; - if request.binding_id == crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID { + if allow_public_source_read { granted_operations.push(SUBSTRATE_GATE_PUBLIC_SOURCE_READ_OPERATION.to_string()); } SubstrateGateRepoAccessGrantReceipt { schema_version: SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION.to_string(), receipt_id, runtime_job_id, - binding_id: request.binding_id.clone(), - role: request.owner_role.clone(), - authority_scope: request.authority_scope.clone(), + binding_id, + role, + authority_scope, granted_operations, granted_paths: vec![".".to_string()], granted_at,