diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 973f8858..f811fd60 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -2394,6 +2394,15 @@ pub(crate) mod tests { ); let result = consideration_result(&request, "backend-consideration")?; put_runtime_role_worker_result(&store, &result)?; + let candidate = result + .imagination_consideration_candidate()? + .expect("typed consideration candidate"); + let cache = coordinator_acceptance_cache(&store)?; + assert_eq!( + cache.get::(&candidate.candidate_id)?, + Some(candidate) + ); + put_runtime_role_worker_result(&store, &result)?; let mut cargo = consideration_result(&request, "backend-consideration")?; cargo.result_id = "cargo-result".into(); cargo.state_patch_msgpack = Some(vec![0]); @@ -2401,6 +2410,45 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn consideration_result_refuses_orphan_candidate_companion() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, request) = consideration_launch_fixture(root.path(), "orphan")?; + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-consideration".into(), + "backend-consideration".into(), + )?; + commit_coordinator_job_launch( + &store, + &request.thread_id, + &state, + &launch, + &plan, + "2026-07-18T00:00:01Z".into(), + )?; + let result = consideration_result(&request, "backend-consideration")?; + let candidate = result + .imagination_consideration_candidate()? + .expect("typed consideration candidate"); + let mut cache = coordinator_acceptance_cache(&store)?; + cache.put(&candidate.candidate_id, &candidate)?; + assert!(put_runtime_role_worker_result(&store, &result).is_err()); + let cache = coordinator_acceptance_cache(&store)?; + assert!( + cache + .get::(&result.job_id)? + .is_none() + ); + assert_eq!( + cache.get::(&candidate.candidate_id)?, + Some(candidate) + ); + Ok(()) + } + #[test] fn consideration_relaunch_requires_exact_terminal_process_and_preserves_attempt_history( ) -> Result<()> { diff --git a/epiphany-core/src/resident_self.rs b/epiphany-core/src/resident_self.rs index 758fbc9e..f3e63f29 100644 --- a/epiphany-core/src/resident_self.rs +++ b/epiphany-core/src/resident_self.rs @@ -4315,16 +4315,6 @@ mod tests { source_visibility: request.source_visibility.clone(), data_classification: request.data_classification.clone(), }; - let (candidate_entry, _) = runtime.prepare_entry(&candidate.candidate_id, &candidate)?; - SingleFileMessagePackBackingStore::new(&runtime_store).push(&candidate_entry)?; - assert_eq!( - verify_resident_self_grant_fulfillment( - &resident_store, - &runtime_store, - &grant.grant_id, - )?, - ResidentSelfGrantFulfillment::Pending - ); admit_test_imagination_candidate( &runtime_store, &request, diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index d9bf2b21..79515eee 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -3967,6 +3967,7 @@ pub fn put_runtime_role_worker_result( } model_direction_companion = Some(direction_result); } + let mut imagination_candidate_companion = None; let has_consideration_echo = result.imagination_consideration_request_id.is_some(); let has_consideration_candidate = result.imagination_consideration_candidate_msgpack.is_some(); if has_consideration_echo != has_consideration_candidate { @@ -4051,6 +4052,7 @@ pub fn put_runtime_role_worker_result( "consideration result substituted request, launch, or context" )); } + imagination_candidate_companion = Some(candidate); } let has_mind_echo = result.frontier_plan_mind_request_id.is_some(); let has_mind_decision = result.frontier_plan_mind_decision_msgpack.is_some(); @@ -4154,14 +4156,24 @@ pub fn put_runtime_role_worker_result( )); } if let Some(companion) = model_direction_companion.as_ref() { - return match cache + match cache .get::(&companion.result_id)? { - Some(existing) if existing == *companion => Ok(()), - _ => Err(anyhow!( + Some(existing) if existing == *companion => {} + _ => return Err(anyhow!( "model direction worker result lost its exact typed companion" )), - }; + } + } + if let Some(companion) = imagination_candidate_companion.as_ref() { + match cache + .get::(&companion.candidate_id)? + { + Some(existing) if existing == *companion => {} + _ => return Err(anyhow!( + "Imagination worker result lost its exact typed candidate companion" + )), + } } return Ok(()); } @@ -4196,6 +4208,18 @@ pub fn put_runtime_role_worker_result( let (companion_envelope, _) = cache.prepare_entry(&companion.result_id, companion)?; writes.push(companion_envelope); } + if let Some(companion) = imagination_candidate_companion.as_ref() { + if cache + .get::(&companion.candidate_id)? + .is_some() + { + return Err(anyhow!( + "Imagination candidate identity already exists without its worker result" + )); + } + let (companion_envelope, _) = cache.prepare_entry(&companion.candidate_id, companion)?; + writes.push(companion_envelope); + } let backing = SingleFileMessagePackBackingStore::new(store_path); if backing.compare_and_swap_batch(&expected, writes)? { return Ok(()); @@ -4205,12 +4229,19 @@ pub fn put_runtime_role_worker_result( let worker_matches = reloaded .get::(&result.job_id)? .is_some_and(|existing| existing == *result); - let companion_matches = match model_direction_companion.as_ref() { + let model_direction_companion_matches = match model_direction_companion.as_ref() { Some(companion) => reloaded .get::(&companion.result_id)? .is_some_and(|existing| existing == *companion), None => true, }; + let imagination_candidate_companion_matches = + match imagination_candidate_companion.as_ref() { + Some(companion) => reloaded + .get::(&companion.candidate_id)? + .is_some_and(|existing| existing == *companion), + None => true, + }; let claim_matches = match process_claim.as_ref() { Some(_) => reloaded .get::(&process_claim_id)? @@ -4220,7 +4251,11 @@ pub fn put_runtime_role_worker_result( }), None => true, }; - if worker_matches && companion_matches && claim_matches { + if worker_matches + && model_direction_companion_matches + && imagination_candidate_companion_matches + && claim_matches + { Ok(()) } else { Err(anyhow!(