From 6cbe47313c3a517e44cd4f96fc6d1c9ddcc02a64 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 01:03:59 +0100 Subject: [PATCH] Bind frontier planning launches atomically --- epiphany-core/src/agent_launch.rs | 3 + epiphany-core/src/coordinator_launch.rs | 412 +++++++++++++++++- .../src/coordinator_launch_context.rs | 1 + epiphany-core/src/lib.rs | 3 + epiphany-core/src/repo_model_gateway.rs | 40 +- epiphany-core/src/runtime_spine.rs | 145 +++++- epiphany-core/src/substrate_gate.rs | 2 + epiphany-core/src/surfaces/mod.rs | 3 + epiphany-core/src/surfaces/worker_launch.rs | 46 ++ notes/fresh-workspace-handoff.md | 17 + notes/repo-frontier-planning-authority-map.md | 28 +- state/ledgers.msgpack | Bin 492332 -> 492623 bytes state/map.yaml | 2 +- 13 files changed, 675 insertions(+), 27 deletions(-) diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index ac0153d7..b15bfcf4 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -603,6 +603,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( dynamic_prompt_context, proposal_modeling_context: None, claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: state.active_subgoal_id.clone(), active_subgoals: state .subgoals @@ -645,6 +646,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( max_runtime_seconds, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, }) } @@ -749,6 +751,7 @@ pub fn build_epiphany_job_launch_request( max_runtime_seconds, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, } } diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 9c1d452d..be78e454 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -111,6 +111,33 @@ fn commit_coordinator_job_launch_in_cache( } else { None }; + let frontier_planning_launch = + if let Some(request_id) = request.frontier_planning_request_id.as_deref() { + let (planning, identity) = + validate_frontier_planning_launch(cache, current_state, request, request_id)?; + let projection = RepoFrontierPlanningContextProjection::from_request(&planning); + match &mut effective_launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + if document.proposal_modeling_context.is_some() + || document.claim_repair_context.is_some() + { + return Err(anyhow!( + "frontier planning context is exclusive of Modeling authority contexts" + )); + } + document.frontier_planning_context = Some(projection); + } + EpiphanyWorkerLaunchDocument::Reorient(_) => { + return Err(anyhow!( + "reorient launch cannot carry frontier planning context" + )); + } + } + let bytes = rmp_serde::to_vec_named(&effective_launch_document)?; + Some((planning, identity, format!("{:x}", Sha256::digest(bytes)))) + } else { + None + }; let prepared = prepare_runtime_spine_heartbeat_job( &cache, RuntimeSpineHeartbeatJobOptions { @@ -135,6 +162,7 @@ fn commit_coordinator_job_launch_in_cache( organ_launch_contract: request.organ_launch_contract.clone(), proposal_modeling_request_id: request.proposal_modeling_request_id.clone(), claim_repair_request_id: request.claim_repair_request_id.clone(), + frontier_planning_request_id: request.frontier_planning_request_id.clone(), created_at: created_at.clone(), }, )?; @@ -208,6 +236,33 @@ fn commit_coordinator_job_launch_in_cache( .0, ); } + if let Some((planning, identity, worker_launch_document_sha256)) = frontier_planning_launch { + let launch_binding = RepoFrontierPlanningLaunchBinding { + schema_version: REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_record_id: format!("repo-frontier-planning-launch-{}", planning.request_id), + planning_request_id: planning.request_id, + job_id: plan.backend_job_id.clone(), + binding_id: request.binding_id.clone(), + runtime_id: identity.runtime_id, + thread_id: planning.thread_id, + launched_at: created_at.clone(), + worker_launch_document_sha256, + contract: REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT.into(), + }; + if cache + .get::(&launch_binding.binding_record_id)? + .is_some() + { + return Err(anyhow!( + "frontier planning request is already bound to a launch" + )); + } + batch.push( + cache + .prepare_entry(&launch_binding.binding_record_id, &launch_binding)? + .0, + ); + } if request.binding_id == EPIPHANY_RESEARCH_ROLE_BINDING_ID { let grant = substrate_gate_repo_access_grant_for_launch( format!("substrate-grant-{}", plan.backend_job_id), @@ -240,13 +295,15 @@ pub fn plan_coordinator_job_launch( launcher_job_id: String, backend_job_id: String, ) -> Result { - let (caller_proposal_projection, caller_repair_projection) = match &request.launch_document { - EpiphanyWorkerLaunchDocument::Role(document) => ( - document.proposal_modeling_context.as_ref(), - document.claim_repair_context.as_ref(), - ), - EpiphanyWorkerLaunchDocument::Reorient(_) => (None, None), - }; + let (caller_proposal_projection, caller_repair_projection, caller_planning_projection) = + match &request.launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => ( + document.proposal_modeling_context.as_ref(), + document.claim_repair_context.as_ref(), + document.frontier_planning_context.as_ref(), + ), + EpiphanyWorkerLaunchDocument::Reorient(_) => (None, None, None), + }; if caller_proposal_projection.is_some() { return Err(anyhow!( "caller-prepopulated proposal Modeling context is forbidden; coordinator commit owns projection" @@ -257,9 +314,23 @@ pub fn plan_coordinator_job_launch( "caller-prepopulated claim repair context is forbidden; coordinator commit owns projection" )); } - if request.proposal_modeling_request_id.is_some() && request.claim_repair_request_id.is_some() { + if caller_planning_projection.is_some() { return Err(anyhow!( - "proposal Modeling and claim repair launches are mutually exclusive" + "caller-prepopulated frontier planning context is forbidden; coordinator commit owns projection" + )); + } + if [ + request.proposal_modeling_request_id.is_some(), + request.claim_repair_request_id.is_some(), + request.frontier_planning_request_id.is_some(), + ] + .into_iter() + .filter(|present| *present) + .count() + > 1 + { + return Err(anyhow!( + "proposal Modeling, claim repair, and frontier planning launches are mutually exclusive" )); } if let Some(expected) = request.expected_revision @@ -278,6 +349,10 @@ pub fn plan_coordinator_job_launch( let mut cache = runtime_spine_cache(runtime_store)?; cache.pull_all_backing_stores()?; validate_claim_repair_launch(&cache, state, request, request_id)?; + } else if let Some(request_id) = request.frontier_planning_request_id.as_deref() { + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + validate_frontier_planning_launch(&cache, state, request, request_id)?; } else if request.owner_role == EPIPHANY_MODELING_OWNER_ROLE { // Ordinary Modeling launches remain valid, but carry no proposal authority. } @@ -335,6 +410,60 @@ pub fn plan_coordinator_job_launch( }) } +fn validate_frontier_planning_launch( + cache: &CultCache, + state: &EpiphanyThreadState, + launch: &EpiphanyJobLaunchRequest, + request_id: &str, +) -> Result<(RepoFrontierPlanningRequest, EpiphanyRuntimeIdentity)> { + if match &launch.launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + document.frontier_planning_context.is_some() + } + EpiphanyWorkerLaunchDocument::Reorient(_) => false, + } { + return Err(anyhow!("caller cannot author frontier planning context")); + } + if launch.owner_role != EPIPHANY_IMAGINATION_OWNER_ROLE + || launch.binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID + { + return Err(anyhow!( + "frontier planning may only be carried by the Imagination role launch" + )); + } + let planning = cache + .get::(request_id)? + .ok_or_else(|| anyhow!("frontier planning request {request_id:?} does not exist"))?; + crate::runtime_spine::validate_current_repo_frontier_planning_request(cache, &planning)?; + let identity = cache + .get::(RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("frontier planning launch requires runtime identity"))?; + let persisted_state = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("frontier planning launch requires authoritative thread state"))?; + let persisted_state_value = persisted_state.state()?; + if planning.request_id != request_id + || planning.runtime_id != identity.runtime_id + || planning.thread_id != persisted_state.thread_id + || persisted_state_value != *state + || launch.launch_document.thread_id() != planning.thread_id + { + return Err(anyhow!( + "frontier planning launch provenance binding mismatch" + )); + } + if cache + .get_all::()? + .iter() + .any(|binding| binding.planning_request_id == request_id) + { + return Err(anyhow!( + "frontier planning request is already bound to a launch" + )); + } + Ok((planning, identity)) +} + fn validate_claim_repair_launch( cache: &CultCache, state: &EpiphanyThreadState, @@ -666,6 +795,49 @@ pub(crate) mod tests { Ok((store, state, launch, repair)) } + fn frontier_planning_launch_fixture( + root: &Path, + suffix: &str, + ) -> Result<( + std::path::PathBuf, + EpiphanyThreadState, + EpiphanyJobLaunchRequest, + RepoFrontierPlanningRequest, + )> { + let (store, _) = + crate::runtime_spine::tests::claim_challenge_fixture(root, suffix, "Imagination")?; + let planning = crate::select_and_commit_repo_frontier_planning_request( + &store, + "2026-07-15T09:00:02Z", + )?; + let mut cache = coordinator_acceptance_cache(&store)?; + cache.pull_all_backing_stores()?; + let mut state = cache + .get_required::(THREAD_STATE_KEY)? + .state()?; + for link in &mut state.runtime_links { + if link.binding_id == EPIPHANY_IMAGINATION_ROLE_BINDING_ID + && link.runtime_result_id.is_none() + { + link.runtime_result_id = Some(format!("prior-imagination-result-{suffix}")); + } + } + cache.put( + THREAD_STATE_KEY, + &EpiphanyThreadStateEntry::from_state(&planning.thread_id, &state)?, + )?; + let mut launch = build_epiphany_role_launch_request( + &planning.thread_id, + EpiphanyRoleResultRoleId::Imagination, + Some(state.revision), + Some(60), + &state, + ) + .map_err(|error| anyhow!(error))?; + launch.frontier_planning_request_id = Some(planning.request_id.clone()); + Ok((store, state, launch, planning)) + } + fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest { build_epiphany_role_launch_request( "thread-1", @@ -1016,6 +1188,228 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn frontier_planning_launch_is_coordinator_owned_exact_and_single_use() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), "exact")?; + for mutation in 0..5 { + let mut forged = launch.clone(); + match mutation { + 0 => forged.owner_role = "Modeling".into(), + 1 => forged.binding_id = "wrong-binding".into(), + 2 => { + if let EpiphanyWorkerLaunchDocument::Role(document) = + &mut forged.launch_document + { + document.frontier_planning_context = Some( + RepoFrontierPlanningContextProjection::from_request(&planning), + ); + } + } + 3 => forged.claim_repair_request_id = Some("dual-repair".into()), + _ => forged.proposal_modeling_request_id = Some("dual-proposal".into()), + } + let before = std::fs::read(&store)?; + assert!( + plan_coordinator_job_launch( + &state, + &forged, + &store, + format!("launcher-forged-{mutation}"), + format!("backend-forged-{mutation}"), + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + } + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-planning".into(), + "backend-planning".into(), + )?; + let committed = commit_coordinator_job_launch( + &store, + &planning.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:03Z".into(), + )?; + let persisted = runtime_worker_launch_request(&store, "backend-planning")? + .expect("planning worker launch"); + let projection = match persisted.launch_document()? { + EpiphanyWorkerLaunchDocument::Role(document) => document + .frontier_planning_context + .expect("coordinator-owned planning context"), + EpiphanyWorkerLaunchDocument::Reorient(_) => panic!("planning cannot reorient"), + }; + assert_eq!(projection.request_id, planning.request_id); + assert_eq!(projection.runtime_id, planning.runtime_id); + assert_eq!(projection.thread_id, planning.thread_id); + let cache = coordinator_acceptance_cache(&store)?; + let binding_key = format!("repo-frontier-planning-launch-{}", planning.request_id); + let binding = cache + .get::(&binding_key)? + .expect("request-keyed planning launch binding"); + assert_eq!(binding.job_id, "backend-planning"); + assert_eq!(binding.runtime_id, planning.runtime_id); + assert_eq!(binding.thread_id, planning.thread_id); + assert_eq!( + binding.worker_launch_document_sha256, + format!("{:x}", Sha256::digest(&persisted.launch_document_msgpack)) + ); + let mut second = build_epiphany_role_launch_request( + &planning.thread_id, + EpiphanyRoleResultRoleId::Imagination, + Some(committed.epiphany_state.revision), + Some(60), + &committed.epiphany_state, + ) + .map_err(|error| anyhow!(error))?; + second.frontier_planning_request_id = Some(planning.request_id); + let before = std::fs::read(&store)?; + assert!( + plan_coordinator_job_launch( + &committed.epiphany_state, + &second, + &store, + "launcher-second".into(), + "backend-second".into(), + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + Ok(()) + } + + #[test] + fn frontier_planning_launch_refuses_swapped_request_bytes_without_writes() -> Result<()> { + let root = tempfile::tempdir()?; + for mutation in 0..8 { + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), &format!("causal-{mutation}"))?; + let mut corrupt = planning.clone(); + match mutation { + 0 => corrupt.model_hash = "swapped-model".into(), + 1 => corrupt.admission_receipt_id = "swapped-admission".into(), + 2 => corrupt.frontier_item_id = "swapped-frontier".into(), + 3 => corrupt.frontier_item_hash = "swapped-frontier-hash".into(), + 4 => corrupt.selected_organ = "Hands".into(), + 5 => corrupt.source_scope.push("outside/scope".into()), + 6 => corrupt.runtime_id = "swapped-runtime".into(), + _ => corrupt.thread_id = "swapped-thread".into(), + } + let mut cache = coordinator_acceptance_cache(&store)?; + cache.put(&corrupt.request_id, &corrupt)?; + let before = std::fs::read(&store)?; + assert!( + plan_coordinator_job_launch( + &state, + &launch, + &store, + format!("launcher-causal-{mutation}"), + format!("backend-causal-{mutation}"), + ) + .is_err(), + "planning causal mutation {mutation} must be refused" + ); + assert_eq!(std::fs::read(&store)?, before); + } + Ok(()) + } + + #[test] + fn concurrent_frontier_planning_launches_leave_only_winner_artifacts() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), "race")?; + let left_plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-left".into(), + "backend-left".into(), + )?; + let right_plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-right".into(), + "backend-right".into(), + )?; + let spawn = |store: std::path::PathBuf, + state: EpiphanyThreadState, + launch: EpiphanyJobLaunchRequest, + plan: EpiphanyCoordinatorJobLaunchPlan, + thread: String, + at: &'static str| { + std::thread::spawn(move || { + commit_coordinator_job_launch(&store, &thread, &state, &launch, &plan, at.into()) + }) + }; + let left = spawn( + store.clone(), + state.clone(), + launch.clone(), + left_plan, + planning.thread_id.clone(), + "2026-07-15T09:00:03Z", + ); + let right = spawn( + store.clone(), + state, + launch, + right_plan, + planning.thread_id, + "2026-07-15T09:00:04Z", + ); + let outcomes = [left.join().unwrap(), right.join().unwrap()]; + assert_eq!(outcomes.iter().filter(|outcome| outcome.is_ok()).count(), 1); + let mut cache = coordinator_acceptance_cache(&store)?; + cache.pull_all_backing_stores()?; + let bindings = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.planning_request_id == planning.request_id) + .collect::>(); + assert_eq!(bindings.len(), 1); + let winner = bindings[0].job_id.as_str(); + let loser = if winner == "backend-left" { + "backend-right" + } else { + assert_eq!(winner, "backend-right"); + "backend-left" + }; + for job_id in [winner] { + assert!(cache.get::(job_id)?.is_some()); + assert!( + cache + .get::(job_id)? + .is_some() + ); + assert!( + cache + .get::(&format!("event-job-opened-{job_id}"))? + .is_some() + ); + } + assert!(cache.get::(loser)?.is_none()); + assert!( + cache + .get::(loser)? + .is_none() + ); + assert!( + cache + .get::(&format!("event-job-opened-{loser}"))? + .is_none() + ); + Ok(()) + } + #[test] fn claim_repair_launch_refuses_every_swapped_causal_field_without_new_writes() -> Result<()> { let root = tempfile::tempdir()?; diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index f2756258..6911fe94 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -1133,6 +1133,7 @@ mod tests { organ_launch_contract: launch_request.organ_launch_contract.clone(), proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: "2026-06-02T00:00:00Z".to_string(), }, )?; diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index cde37925..466c3b9b 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -976,11 +976,14 @@ pub use surfaces::EpiphanyTokenUsageSnapshot; pub use surfaces::EpiphanyViewLens; pub use surfaces::EpiphanyWorkerLaunchDocument; pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT; +pub use surfaces::REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION; pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT; pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION; pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::RepoFrontierPlanningContextProjection; pub use surfaces::RepoFrontierProposalModelingContextProjection; pub use surfaces::RepoModelClaimRepairContextProjection; pub use surfaces::build_reorient_acceptance_bundle; diff --git a/epiphany-core/src/repo_model_gateway.rs b/epiphany-core/src/repo_model_gateway.rs index 2624b806..65a19c1d 100644 --- a/epiphany-core/src/repo_model_gateway.rs +++ b/epiphany-core/src/repo_model_gateway.rs @@ -30,12 +30,12 @@ pub const REPO_FRONTIER_MODELING_REQUEST_CONTRACT: &str = pub const REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION: &str = "epiphany.repo_frontier_work_proposal.v0"; pub const REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION: &str = - "epiphany.self.repo_frontier_planning_request.v0"; + "epiphany.self.repo_frontier_planning_request.v1"; pub const REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION: &str = "epiphany.imagination.repo_frontier_plan_candidate.v0"; pub const REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION: &str = "epiphany.mind.repo_frontier_plan_adoption.v0"; -pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v0"; +pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v1"; pub const REPO_FRONTIER_WORK_PROPOSAL_CONTRACT: &str = "epiphany.repo_frontier_work_proposal.inert.v0"; pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION: &str = @@ -57,6 +57,10 @@ pub const REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_SCHEMA_VERSION: &str = "epiphany.coordinator.repo_model_claim_repair_launch_binding.v0"; pub const REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_CONTRACT: &str = "epiphany.repo_model_claim_repair_launch_binding.v0"; +pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION: &str = + "epiphany.coordinator.repo_frontier_planning_launch_binding.v0"; +pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT: &str = + "epiphany.repo_frontier_planning_launch_binding.v0"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -355,6 +359,38 @@ pub struct RepoFrontierPlanningRequest { pub requested_at: String, #[cultcache(key = 10)] pub contract: String, + #[cultcache(key = 11)] + pub runtime_id: String, + #[cultcache(key = 12)] + pub thread_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.coordinator.repo_frontier_planning_launch_binding", + schema = "RepoFrontierPlanningLaunchBinding" +)] +pub struct RepoFrontierPlanningLaunchBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_record_id: String, + #[cultcache(key = 2)] + pub planning_request_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub binding_id: String, + #[cultcache(key = 5)] + pub runtime_id: String, + #[cultcache(key = 6)] + pub thread_id: String, + #[cultcache(key = 7)] + pub launched_at: String, + #[cultcache(key = 8)] + pub worker_launch_document_sha256: String, + #[cultcache(key = 9)] + pub contract: String, } #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 375d3541..96addcb8 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -1,5 +1,8 @@ use crate::EpiphanyWorkerLaunchDocument; -use crate::agent_launch::{EPIPHANY_MODELING_OWNER_ROLE, EPIPHANY_MODELING_ROLE_BINDING_ID}; +use crate::agent_launch::{ + EPIPHANY_IMAGINATION_OWNER_ROLE, EPIPHANY_IMAGINATION_ROLE_BINDING_ID, + EPIPHANY_MODELING_OWNER_ROLE, EPIPHANY_MODELING_ROLE_BINDING_ID, +}; use crate::agent_memory::AGENT_MEMORY_TYPE; use crate::continuity_gateway::ContinuityRecoveryReceipt; use crate::continuity_gateway::*; @@ -59,7 +62,7 @@ use crate::repo_model_gateway::{ REPO_MODEL_MIGRATION_CONTRACT, REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION, REPO_MODEL_MIGRATION_RECEIPT_TYPE, RepoFrontierHandsAuthority, RepoFrontierModelingRequest, RepoFrontierNextOrgan, RepoFrontierPlanAdoption, RepoFrontierPlanCandidate, - RepoFrontierPlanDecision, RepoFrontierPlanningRequest, + RepoFrontierPlanDecision, RepoFrontierPlanningLaunchBinding, RepoFrontierPlanningRequest, RepoFrontierProposalModelingLaunchBinding, RepoFrontierProposalModelingRequest, RepoFrontierRoute, RepoFrontierVerdictDisposition, RepoFrontierWorkProposal, RepoModelAdmissionReceipt, RepoModelAdmissionReview, RepoModelClaimChallenge, @@ -328,6 +331,8 @@ pub struct EpiphanyRuntimeWorkerLaunchRequest { pub proposal_modeling_request_id: Option, #[cultcache(key = 12, default)] pub claim_repair_request_id: Option, + #[cultcache(key = 13, default)] + pub frontier_planning_request_id: Option, } impl EpiphanyRuntimeWorkerLaunchRequest { @@ -683,6 +688,7 @@ pub struct RuntimeSpineHeartbeatJobOptions { pub organ_launch_contract: EpiphanyLaunchOrganContract, pub proposal_modeling_request_id: Option, pub claim_repair_request_id: Option, + pub frontier_planning_request_id: Option, pub created_at: String, } @@ -732,6 +738,7 @@ pub struct EpiphanyJobLaunchRequest { pub max_runtime_seconds: Option, pub proposal_modeling_request_id: Option, pub claim_repair_request_id: Option, + pub frontier_planning_request_id: Option, } #[derive(Debug, Clone, PartialEq)] @@ -786,6 +793,7 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1054,6 +1062,12 @@ pub fn open_runtime_spine_heartbeat_job( options.claim_repair_request_id.as_deref(), &options.launch_document, )?; + validate_frontier_planning_launch_carrier( + &options.role, + &options.binding_id, + options.frontier_planning_request_id.as_deref(), + &options.launch_document, + )?; validate_non_empty(&options.binding_id, "binding id")?; validate_non_empty(&options.authority_scope, "authority scope")?; validate_non_empty(&options.instruction, "instruction")?; @@ -1139,6 +1153,7 @@ pub fn open_runtime_spine_heartbeat_job( organ_launch_contract, proposal_modeling_request_id: options.proposal_modeling_request_id, claim_repair_request_id: options.claim_repair_request_id, + frontier_planning_request_id: options.frontier_planning_request_id, }; cache.put(&job_id, &request)?; Ok(job) @@ -1166,6 +1181,12 @@ pub fn prepare_runtime_spine_heartbeat_job( options.claim_repair_request_id.as_deref(), &options.launch_document, )?; + validate_frontier_planning_launch_carrier( + &options.role, + &options.binding_id, + options.frontier_planning_request_id.as_deref(), + &options.launch_document, + )?; validate_non_empty(&options.binding_id, "binding id")?; validate_non_empty(&options.authority_scope, "authority scope")?; validate_non_empty(&options.instruction, "instruction")?; @@ -1277,6 +1298,7 @@ pub fn prepare_runtime_spine_heartbeat_job( organ_launch_contract: options.organ_launch_contract, proposal_modeling_request_id: options.proposal_modeling_request_id, claim_repair_request_id: options.claim_repair_request_id, + frontier_planning_request_id: options.frontier_planning_request_id, }; let envelopes = vec![ cache.prepare_entry(RUNTIME_IDENTITY_KEY, &identity)?.0, @@ -1354,6 +1376,39 @@ fn validate_claim_repair_launch_carrier( Ok(()) } +fn validate_frontier_planning_launch_carrier( + role: &str, + binding_id: &str, + planning_request_id: Option<&str>, + launch_document: &EpiphanyWorkerLaunchDocument, +) -> Result<()> { + let projection = match launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => document.frontier_planning_context.as_ref(), + EpiphanyWorkerLaunchDocument::Reorient(_) => None, + }; + let Some(request_id) = planning_request_id else { + if projection.is_some() { + return Err(anyhow!( + "frontier planning context requires its typed request id" + )); + } + return Ok(()); + }; + validate_non_empty(request_id, "frontier planning request id")?; + if role != EPIPHANY_IMAGINATION_OWNER_ROLE || binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID + { + return Err(anyhow!( + "frontier planning request may only be transported by the Imagination role launch" + )); + } + let projection = projection + .ok_or_else(|| anyhow!("frontier planning request requires its typed context"))?; + if projection.request_id != request_id { + return Err(anyhow!("frontier planning context/request mismatch")); + } + Ok(()) +} + pub fn runtime_job_snapshot( store_path: impl AsRef, job_id: &str, @@ -2748,7 +2803,11 @@ pub fn select_and_commit_repo_frontier_planning_request( let runtime_store = runtime_store.as_ref(); let mut cache = runtime_spine_cache(runtime_store)?; cache.pull_all_backing_stores()?; - require_identity(&cache)?; + let identity = require_identity(&cache)?; + let thread = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("planning request requires authoritative thread state"))?; + thread.state()?; let backing = SingleFileMessagePackBackingStore::new(runtime_store); let model_envelope = backing .pull_all()? @@ -2792,7 +2851,13 @@ pub fn select_and_commit_repo_frontier_planning_request( } let request_id = format!( "repo-frontier-planning-{:x}", - Sha256::digest(format!("{model_hash}:{}:{item_hash}", item.id).as_bytes()) + Sha256::digest( + format!( + "{}:{}:{model_hash}:{}:{item_hash}", + identity.runtime_id, thread.thread_id, item.id + ) + .as_bytes(), + ) ); let request = RepoFrontierPlanningRequest { schema_version: REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION.into(), @@ -2806,6 +2871,8 @@ pub fn select_and_commit_repo_frontier_planning_request( source_scope: item.source_scope.clone(), requested_at: at.into(), contract: REPO_FRONTIER_PLANNING_CONTRACT.into(), + runtime_id: identity.runtime_id, + thread_id: thread.thread_id, }; if let Some(existing) = cache.get::(&request_id)? { let mut retry = request.clone(); @@ -2834,6 +2901,72 @@ pub fn select_and_commit_repo_frontier_planning_request( Ok(request) } +pub(crate) fn validate_current_repo_frontier_planning_request( + cache: &CultCache, + request: &RepoFrontierPlanningRequest, +) -> Result<()> { + chrono::DateTime::parse_from_rfc3339(&request.requested_at) + .map_err(|_| anyhow!("planning request timestamp must be RFC3339"))?; + let identity = require_identity(cache)?; + let thread = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("planning request requires authoritative thread state"))?; + thread.state()?; + let entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("planning request requires canonical model"))?; + crate::validate_memory_graph_entry(&entry)?; + let model = entry.snapshot()?; + let model_hash = crate::memory_graph_model_hash(&model)?; + let receipts = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.schema_version == REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + && receipt.contract == REPO_MODEL_ADMISSION_CONTRACT + && receipt.admitted_revision == model.model_revision + && receipt.admitted_hash == model_hash + }) + .collect::>(); + if receipts.len() != 1 { + return Err(anyhow!( + "planning request requires exactly one current admission receipt" + )); + } + let challenges = current_repo_model_claim_challenges(cache, &model, &model_hash)?; + let item = actionable_imagination_frontier_item(&model, &challenges) + .ok_or_else(|| anyhow!("planning request frontier is no longer actionable"))?; + let item_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)); + let expected_request_id = format!( + "repo-frontier-planning-{:x}", + Sha256::digest( + format!( + "{}:{}:{model_hash}:{}:{item_hash}", + identity.runtime_id, thread.thread_id, item.id + ) + .as_bytes(), + ) + ); + if request.schema_version != REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION + || request.contract != REPO_FRONTIER_PLANNING_CONTRACT + || request.request_id != expected_request_id + || request.model_revision != model.model_revision + || request.model_hash != model_hash + || request.admission_receipt_id != receipts[0].receipt_id + || request.frontier_item_id != item.id + || request.frontier_item_hash != item_hash + || request.selected_organ != "Imagination" + || request.source_scope != item.source_scope + || request.runtime_id != identity.runtime_id + || request.thread_id != thread.thread_id + { + return Err(anyhow!( + "planning request does not exactly bind current model, frontier, runtime, and thread" + )); + } + Ok(()) +} + fn actionable_imagination_frontier_item<'a>( model: &'a crate::EpiphanyMemoryGraphSnapshot, challenges: &[RepoModelClaimChallenge], @@ -9853,6 +9986,7 @@ pub(crate) mod tests { dynamic_prompt_context: None, proposal_modeling_context: None, claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["node-model".to_string()], @@ -9876,6 +10010,7 @@ pub(crate) mod tests { ), proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: "2026-05-06T00:02:00Z".to_string(), }, )?; @@ -9946,6 +10081,7 @@ pub(crate) mod tests { dynamic_prompt_context: None, proposal_modeling_context: None, claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["runtime-spine".to_string()], @@ -10032,6 +10168,7 @@ pub(crate) mod tests { dynamic_prompt_context: None, proposal_modeling_context: None, claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), diff --git a/epiphany-core/src/substrate_gate.rs b/epiphany-core/src/substrate_gate.rs index ed241995..c6dbecb8 100644 --- a/epiphany-core/src/substrate_gate.rs +++ b/epiphany-core/src/substrate_gate.rs @@ -251,6 +251,7 @@ mod tests { dynamic_prompt_context: None, proposal_modeling_context: None, claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -275,6 +276,7 @@ mod tests { max_runtime_seconds: None, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, }; let grant = substrate_gate_repo_access_grant_for_launch( "grant-1".to_string(), diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index 4e93f319..a5b4c781 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -144,11 +144,14 @@ pub use worker_launch::EpiphanyReorientWorkerLaunchDocument; pub use worker_launch::EpiphanyRoleWorkerLaunchDocument; pub use worker_launch::EpiphanyWorkerLaunchDocument; pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT; +pub use worker_launch::REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION; pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT; pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION; pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::RepoFrontierPlanningContextProjection; pub use worker_launch::RepoFrontierProposalModelingContextProjection; pub use worker_launch::RepoModelClaimRepairContextProjection; pub use worker_launch::build_reorient_job_launch_request; diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs index 347b70b3..c00b2e73 100644 --- a/epiphany-core/src/surfaces/worker_launch.rs +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -34,6 +34,10 @@ pub const REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION: &str = "epiphany.worker.repo_model_claim_repair_context.v0"; pub const REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT: &str = "epiphany.repo_model_claim_repair_context.v0"; +pub const REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION: &str = + "epiphany.worker.repo_frontier_planning_context.v0"; +pub const REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT: &str = + "epiphany.repo_frontier_planning_context.v0"; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "documentKind")] @@ -89,6 +93,8 @@ pub struct EpiphanyRoleWorkerLaunchDocument { #[serde(default, skip_serializing_if = "Option::is_none")] pub claim_repair_context: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub frontier_planning_context: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub active_subgoal_id: Option, #[serde(default)] pub active_subgoals: Vec, @@ -256,6 +262,45 @@ impl RepoModelClaimRepairContextProjection { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepoFrontierPlanningContextProjection { + pub schema_version: String, + pub contract: String, + pub request_id: String, + pub model_revision: u64, + pub model_hash: String, + pub admission_receipt_id: String, + pub frontier_item_id: String, + pub frontier_item_hash: String, + pub selected_organ: String, + #[serde(default)] + pub source_scope: Vec, + pub requested_at: String, + pub runtime_id: String, + pub thread_id: String, +} + +impl RepoFrontierPlanningContextProjection { + pub(crate) fn from_request(request: &crate::RepoFrontierPlanningRequest) -> Self { + Self { + schema_version: REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION.into(), + contract: REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT.into(), + request_id: request.request_id.clone(), + model_revision: request.model_revision, + model_hash: request.model_hash.clone(), + admission_receipt_id: request.admission_receipt_id.clone(), + frontier_item_id: request.frontier_item_id.clone(), + frontier_item_hash: request.frontier_item_hash.clone(), + selected_organ: request.selected_organ.clone(), + source_scope: request.source_scope.clone(), + requested_at: request.requested_at.clone(), + runtime_id: request.runtime_id.clone(), + thread_id: request.thread_id.clone(), + } + } +} + pub fn build_reorient_job_launch_request( input: EpiphanyReorientLaunchRequestInput<'_>, ) -> EpiphanyJobLaunchRequest { @@ -298,6 +343,7 @@ pub fn build_reorient_job_launch_request( max_runtime_seconds: input.max_runtime_seconds, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, } } diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 201b7ea9..204457eb 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -948,3 +948,20 @@ embedded receipt binds back to the artifact target and audit; forged cardinality: Epiphany inherits a CultMesh command id, so the coordinator must ensure one typed command id per crossing rather than letting multiple posts share one canonical `crossing_` receipt. + +## Repo frontier planning launch artery (2026-07-15) + +Self's planning request is now v1 and binds the exact current RepoModel, +admission receipt, actionable Imagination frontier, runtime identity, and +authoritative thread. Coordinator launch commit replays that entire chain from +one transaction snapshot, forbids caller-authored planning projection and +mixed Modeling/repair/planning authority, injects the typed Imagination context, +and atomically persists a request-keyed +`RepoFrontierPlanningLaunchBinding`. Its SHA-256 covers the exact launch bytes +stored for the worker. Hostile tests cover swapped model/admission/frontier/ +scope/organ/runtime/thread fields, wrong role/binding, prepopulated projection, +dual authority, single-use replay, and two concurrent launches; only one wins +and the loser leaves no job, worker launch, or opened event. Full core proof: +322 passed, 1 ignored. Next build the exclusive immutable Imagination result +echo carrying the typed candidate, then Mind-only Adopt/Refuse/Hold admission +and the narrow Adopt model transition that makes Hands routing true. diff --git a/notes/repo-frontier-planning-authority-map.md b/notes/repo-frontier-planning-authority-map.md index 0665a1a2..ce131b00 100644 --- a/notes/repo-frontier-planning-authority-map.md +++ b/notes/repo-frontier-planning-authority-map.md @@ -1,18 +1,24 @@ # Repo Frontier Planning Authority Map -## Current wound +## Current state -The planning family has typed documents but not a live organ chain. Self can -persist an exact `RepoFrontierPlanningRequest`. Nothing launches Imagination -from it. `RepoFrontierPlanCandidate` and `RepoFrontierPlanAdoption` were exposed -through public persistence functions, allowing callers to author documents in -Imagination and Mind namespaces without either organ participating. Adopt then -blocked duplicate planning requests but changed no model bytes and affected no -Hands route. The type names were substituting for cognition. +The planning family now has a real Self-to-Imagination launch artery. Self's +`RepoFrontierPlanningRequest` binds the uniquely admitted current model, exact +actionable frontier, runtime, and authoritative thread. Coordinator commit +replays those bytes, injects the exact typed Imagination projection, and +atomically persists a request-keyed launch binding whose hash covers the stored +worker launch document. Caller-authored projections, mixed authority carriers, +stale requests, causal substitutions, and duplicate/racing launches are +refused without loser artifacts. -The public candidate/adoption writers are removed. The existing internal -functions remain temporarily as test scaffolding while the real nerve replaces -them; they are not production authority. +The downstream nerve is still incomplete. Imagination has no exclusive +immutable result carrying the candidate; Mind has no correlated admission; the +old internal candidate/adoption fixture writers still exist; and Adopt still +changes no RepoModel bytes or Hands truth. + +The public candidate/adoption writers remain removed. The existing internal +functions are test scaffolding only and die when result correlation and Mind +admission replace them. ## Target authority diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 035b64a3c6c0c73a2d172f37b750575b71a87e64..49f31ea56c41bd56638ee2ed93e41ef5035ec2cd 100644 GIT binary patch delta 318 zcmXwzy-ve05XW)&$iRZs5ve+f2`N=W5Ci22Sb>Sn*q6rO*y-76=>(_}@B&e0b|jD( zcmq~gc@G`{N0i~GJDu+T@qFL^eC)sW*w(>WnvT+Jlpdacti+4tZHG-5ds;NnLldz; zH&+8|f%U+~dlSX&ZTm8fH{+$c{n};QH{U-eSXs!)8n2L`(%cwj#-Mp^L;;*U(j1JP z!G$u?!71t-2q!f<57RyH7N`!|5{JGZ