diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index 64388345..a75177b3 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -8,14 +8,10 @@ use crate::EpiphanyJobLaunchRequest; use crate::EpiphanyPressure; use crate::EpiphanyPressureLevel as CoreEpiphanyPressureLevel; use crate::EpiphanyReorientAction as CoreEpiphanyReorientAction; -use crate::EpiphanyReorientDecision as CoreEpiphanyReorientDecision; -use crate::EpiphanyReorientLaunchRequestInput; use crate::EpiphanyRoleResultRoleId; use crate::EpiphanyRoleWorkerLaunchDocument; use crate::EpiphanyWorkerLaunchDocument; -use crate::build_reorient_job_launch_request; use crate::default_launch_organ_contract; -use epiphany_state_model::EpiphanyInvestigationCheckpoint; use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind; use epiphany_state_model::EpiphanyThreadState; @@ -1238,49 +1234,6 @@ fn build_epiphany_role_launch_instruction(role_id: EpiphanyRoleResultRoleId) -> epiphany_worker_prompt(body) } -pub fn build_epiphany_reorient_launch_request( - thread_id: &str, - expected_revision: Option, - max_runtime_seconds: Option, - state: &EpiphanyThreadState, - checkpoint: &EpiphanyInvestigationCheckpoint, - decision: &CoreEpiphanyReorientDecision, -) -> EpiphanyJobLaunchRequest { - build_epiphany_reorient_launch_request_with_dynamic_context( - thread_id, - expected_revision, - max_runtime_seconds, - state, - checkpoint, - decision, - None, - ) -} - -pub fn build_epiphany_reorient_launch_request_with_dynamic_context( - thread_id: &str, - expected_revision: Option, - max_runtime_seconds: Option, - state: &EpiphanyThreadState, - checkpoint: &EpiphanyInvestigationCheckpoint, - decision: &CoreEpiphanyReorientDecision, - dynamic_prompt_context: Option, -) -> EpiphanyJobLaunchRequest { - let instruction = build_epiphany_reorient_launch_instruction(decision.action); - build_reorient_job_launch_request(EpiphanyReorientLaunchRequestInput { - thread_id, - expected_revision, - max_runtime_seconds, - binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID, - owner_role: EPIPHANY_REORIENT_OWNER_ROLE, - instruction, - state, - checkpoint, - decision, - dynamic_prompt_context, - }) -} - pub fn build_epiphany_reorient_launch_instruction(action: CoreEpiphanyReorientAction) -> String { let prompts = &epiphany_specialist_prompt_config().reorientation; let body = match action { diff --git a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs index f06fda6d..c9796cfc 100644 --- a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs +++ b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs @@ -1072,15 +1072,20 @@ fn run_coordinator(args: &Args) -> Result { steps.push(step); break; } - let can_accept = args.auto_review - && reorient_result_auto_acceptable(&result) - && revision.is_some(); + if result["status"].as_str() == Some("failed") { + let recorded = accept_reorient(&runtime_store)?; + push_event( + &mut step, + json!({"type": "reorientFailureRecorded", "recorded": status_cli::sanitize_for_operator(recorded)}), + ); + final_status = collect_coordinator_status(&runtime_store, &thread_id)?; + append_operator_step_jsonl(&steps_path, &step)?; + steps.push(step); + continue; + } + let can_accept = args.auto_review && reorient_result_auto_acceptable(&result); if can_accept { - let accepted = accept_reorient( - &runtime_store, - &thread_id, - revision.and_then(|value| u64::try_from(value).ok()), - )?; + let accepted = accept_reorient(&runtime_store)?; push_event( &mut step, json!({"type": "reorientAccept", "accepted": status_cli::sanitize_for_operator(accepted)}), @@ -1858,29 +1863,6 @@ fn launch_role( })) } -fn launch_job_value( - service: &epiphany_core::EpiphanyCoordinatorService, - thread_id: &str, - state: &epiphany_state_model::EpiphanyThreadState, - request: &epiphany_core::EpiphanyJobLaunchRequest, -) -> Result { - let launched = service.launch_job( - thread_id, - state, - request, - format!("epiphany-heartbeat-launch-{}", Uuid::new_v4()), - Uuid::new_v4().to_string(), - now(), - )?; - Ok(json!({ - "bindingId": launched.binding_id, - "launcherJobId": launched.launcher_job_id, - "backendJobId": launched.backend_job_id, - "revision": launched.epiphany_state.revision, - "state": launched.epiphany_state, - })) -} - fn accept_role( runtime_store: &Path, thread_id: &str, @@ -1919,52 +1901,30 @@ fn accept_role( })) } -fn accept_reorient( - runtime_store: &Path, - thread_id: &str, - expected_revision: Option, -) -> Result { - let service = epiphany_core::EpiphanyCoordinatorService::new(runtime_store); - let state = service - .state()? - .ok_or_else(|| anyhow!("cannot accept reorientation without native coordinator state"))?; - let snapshot = service.reorient_result(epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID)?; - if let Some(result_id) = snapshot - .finding - .as_ref() - .and_then(|finding| finding.runtime_result_id.as_deref()) - && let Some(existing) = state.acceptance_receipts.iter().find(|receipt| { - receipt.result_id == result_id - && receipt.binding_id == epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID - && receipt.surface == "reorientAccept" - && receipt.status == "accepted" - }) - { - return Ok(json!({ - "revision": state.revision, - "state": state, - "acceptedReceiptId": existing.id, - "changed": false, - })); +fn accept_reorient(runtime_store: &Path) -> Result { + let work = epiphany_core::project_current_work(runtime_store)? + .reorientation + .ok_or_else(|| anyhow!("Mind has no reviewable reorientation work"))?; + if work.action != epiphany_core::EpiphanyAgentPassContinuationAction::Review { + return Err(anyhow!("reorientation work is not reviewable")); } - let accepted = service.accept_reorient( - thread_id, - &state, - epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID, - expected_revision, - None, - now(), - &Uuid::new_v4().to_string(), - true, - true, - )?; + let job_id = work + .job_id + .ok_or_else(|| anyhow!("reorientation review lost its runtime job"))?; + let snapshot = epiphany_core::runtime_job_snapshot(runtime_store, &job_id)? + .ok_or_else(|| anyhow!("reorientation review lost its runtime job"))?; + let accepted = match snapshot.job.status { + epiphany_core::EpiphanyRuntimeJobStatus::Completed => { + epiphany_core::accept_reorientation_result(runtime_store, &job_id, &now())? + } + epiphany_core::EpiphanyRuntimeJobStatus::Failed => { + epiphany_core::record_reorientation_pass_failure(runtime_store, &job_id)? + } + _ => return Err(anyhow!("reorientation work is not terminally reviewable")), + }; Ok(json!({ - "revision": accepted.state.revision, - "state": accepted.state, - "acceptedReceiptId": accepted.update.accepted_receipt_id, - "acceptedObservationId": accepted.update.accepted_observation_id, - "acceptedEvidenceId": accepted.update.accepted_evidence_id, - "finding": accepted.finding, + "mindCommitReceipt": accepted, + "changed": true, })) } @@ -2151,64 +2111,18 @@ fn default_binding_id_for_role(role_id: &str) -> String { fn launch_reorient( runtime_store: &Path, - local_verse_store: &Path, - thread_id: &str, - expected_revision: Option, - max_runtime_seconds: u64, + _local_verse_store: &Path, + _thread_id: &str, + _expected_revision: Option, + _max_runtime_seconds: u64, ) -> Result { - let service = epiphany_core::EpiphanyCoordinatorService::new(runtime_store); - let state = service - .state()? - .ok_or_else(|| anyhow!("cannot launch reorientation without native coordinator state"))?; - let checkpoint = match state.investigation_checkpoint.clone() { - Some(checkpoint) => checkpoint, - None => { - let projection = epiphany_core::runtime_modeling_semantic_projection_input( - runtime_store, - ) - .context("cannot launch reorientation without a valid admitted RepoModel checkpoint")?; - epiphany_state_model::reorient_checkpoint_from_admitted_repo_model( - projection.snapshot(), - &projection.obligation().obligation_id, - ) - } - }; - let status = collect_coordinator_status(runtime_store, thread_id)?; - let decision: epiphany_core::EpiphanyReorientDecision = - serde_json::from_value(status["reorient"]["decision"].clone()) - .context("native reorientation status did not contain a typed decision")?; - let context = epiphany_core::render_launch_dynamic_prompt_context( - runtime_store, - local_verse_store, - &state, - epiphany_core::reorient_launch_context_focus(&state, &decision.next_action), - ) - .map_err(anyhow::Error::msg)?; - let expected_revision = expected_revision.and_then(|value| u64::try_from(value).ok()); - let request = epiphany_core::build_epiphany_reorient_launch_request_with_dynamic_context( - thread_id, - expected_revision, - Some(max_runtime_seconds), - &state, - &checkpoint, - &decision, - Some(context), - ); - let launched = service.launch_job( - thread_id, - &state, - &request, - format!("epiphany-heartbeat-launch-{}", Uuid::new_v4()), - Uuid::new_v4().to_string(), - now(), - )?; + let requested_at = now(); + let request = epiphany_core::request_current_reorientation(runtime_store, &requested_at)?; + let job_id = epiphany_core::launch_current_reorientation_work(runtime_store, &requested_at)?; Ok(json!({ - "bindingId": launched.binding_id, - "launcherJobId": launched.launcher_job_id, - "backendJobId": launched.backend_job_id, - "revision": launched.epiphany_state.revision, - "state": launched.epiphany_state, - "decision": decision, + "bindingId": epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + "backendJobId": job_id, + "request": request, })) } @@ -2344,8 +2258,35 @@ fn read_role_result(runtime_store: &Path, thread_id: &str, role_id: &str) -> Res Ok(collect_coordinator_status(runtime_store, thread_id)?["roleResults"][role_id].clone()) } -fn read_reorient_result(runtime_store: &Path, thread_id: &str) -> Result { - Ok(collect_coordinator_status(runtime_store, thread_id)?["reorientResult"].clone()) +fn read_reorient_result(runtime_store: &Path, _thread_id: &str) -> Result { + let Some(work) = epiphany_core::project_current_work(runtime_store)?.reorientation else { + return Ok(json!({ + "source": "native", + "status": "backendMissing", + "bindingId": epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + "note": "No keyed reorientation request is active." + })); + }; + let Some(job_id) = work.job_id else { + return Ok(json!({ + "source": "native", + "status": "pending", + "bindingId": epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + "note": "The keyed reorientation request is ready to launch." + })); + }; + let snapshot = epiphany_core::read_runtime_reorient_result(Some(runtime_store), &job_id); + let mut value = json!({ + "source": "native", + "status": snapshot.status, + "bindingId": epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + "runtimeJobId": job_id, + "note": snapshot.note, + }); + if let Some(finding) = snapshot.finding { + value["finding"] = json!(finding); + } + Ok(value) } fn maybe_apply_role_self_patch(accepted: &Value, agent_memory_dir: &Path) -> Result> { diff --git a/epiphany-core/src/bin/epiphany-mvp-status.rs b/epiphany-core/src/bin/epiphany-mvp-status.rs index a6410fdb..29e74773 100644 --- a/epiphany-core/src/bin/epiphany-mvp-status.rs +++ b/epiphany-core/src/bin/epiphany-mvp-status.rs @@ -4,20 +4,14 @@ use anyhow::anyhow; use epiphany_core::EpiphanyCoordinatorRoleResultStatus; use epiphany_core::EpiphanyCoordinatorStatus; use epiphany_core::EpiphanyCoordinatorStatusInput; -use epiphany_core::EpiphanyCrrcInput; -use epiphany_core::EpiphanyCrrcReorientAction; +use epiphany_core::EpiphanyCrrcAction; +use epiphany_core::EpiphanyCrrcRecommendation; use epiphany_core::EpiphanyCrrcResultStatus; +use epiphany_core::EpiphanyCrrcSceneAction; use epiphany_core::EpiphanyCrrcStateStatus; -use epiphany_core::EpiphanyFreshnessInput; -use epiphany_core::EpiphanyGraphFreshnessStatus; -use epiphany_core::EpiphanyInvalidationStatus; use epiphany_core::EpiphanyJobStatus; use epiphany_core::EpiphanyJobsInput; use epiphany_core::EpiphanyReorientAction; -use epiphany_core::EpiphanyReorientFreshnessStatus; -use epiphany_core::EpiphanyReorientInput; -use epiphany_core::EpiphanyReorientPressureLevel; -use epiphany_core::EpiphanyRetrievalFreshnessStatus; use epiphany_core::EpiphanyRoleBoardCheckpointSummary; use epiphany_core::EpiphanyRoleBoardInput; use epiphany_core::EpiphanyRoleBoardJob; @@ -29,20 +23,16 @@ use epiphany_core::EpiphanyRuntimeJobSnapshot; use epiphany_core::EpiphanyRuntimeJobStatus; use epiphany_core::EpiphanySceneInput; use epiphany_core::EpiphanyTokenUsageSnapshot; -use epiphany_core::derive_freshness; use epiphany_core::derive_jobs; use epiphany_core::derive_planning_view; use epiphany_core::derive_pressure_view; use epiphany_core::derive_role_board; use epiphany_core::derive_scene; use epiphany_core::read_accepted_coordinator_state; -use epiphany_core::recommend_crrc_action; -use epiphany_core::recommend_reorientation; use epiphany_core::runtime_job_snapshot; use epiphany_self_policy::crrc_scene_action_to_coordinator_scene_action; use epiphany_self_policy::derive_coordinator_finding_signals; use epiphany_self_policy::derive_coordinator_status; -use epiphany_self_policy::reorient_finding_already_accepted; use epiphany_state_model::EpiphanyThreadState; use serde_json::Value; use serde_json::json; @@ -279,7 +269,7 @@ pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result< } fn run_native_status(args: &Args, include_auxiliary_status: bool) -> Result { - let cwd = absolute_path(&args.cwd)?; + let _cwd = absolute_path(&args.cwd)?; let store_path = absolute_path(&args.store)?; let runtime_store_path = store_path.clone(); let state = if store_path.exists() { @@ -303,53 +293,33 @@ fn run_native_status(args: &Args, include_auxiliary_status: bool) -> Result); - let freshness = derive_freshness(EpiphanyFreshnessInput { - state: state_ref, - retrieval_override: None, - watcher: None, - }); - let reorient_pressure_level = match pressure.level { - epiphany_core::EpiphanyPressureLevel::Unknown => EpiphanyReorientPressureLevel::Unknown, - epiphany_core::EpiphanyPressureLevel::Low => EpiphanyReorientPressureLevel::Low, - epiphany_core::EpiphanyPressureLevel::Elevated => EpiphanyReorientPressureLevel::Medium, - epiphany_core::EpiphanyPressureLevel::High => EpiphanyReorientPressureLevel::High, - epiphany_core::EpiphanyPressureLevel::Critical => EpiphanyReorientPressureLevel::Critical, - }; - let reorient_checkpoint = match state_ref - .and_then(|state| state.investigation_checkpoint.clone()) - { - Some(checkpoint) => Some(checkpoint), - None => match epiphany_core::runtime_current_repo_model(&runtime_store_path)? { - None => None, - Some(_) => { - let projection = - epiphany_core::runtime_modeling_semantic_projection_input(&runtime_store_path) - .context("failed to validate current RepoModel continuity projection")?; - Some( - epiphany_state_model::reorient_checkpoint_from_admitted_repo_model( - projection.snapshot(), - &projection.obligation().obligation_id, - ), - ) + let mind = epiphany_core::assemble_mind_view(&runtime_store_path)?; + let durable_checkpoint_present = mind.investigation_checkpoint.is_some(); + let latest_reorientation_decision = mind.reorientation_decisions.last(); + let reorientation_work = current_work.reorientation.as_ref(); + let reorient_action = latest_reorientation_decision + .map(|decision| { + if decision.mode == "regather" { + EpiphanyReorientAction::Regather + } else { + EpiphanyReorientAction::Resume } + }) + .unwrap_or(EpiphanyReorientAction::Resume); + let reorient_next_action = latest_reorientation_decision + .map(|decision| decision.next_safe_move.clone()) + .unwrap_or_else(|| "No unresolved keyed reorientation obligation.".into()); + let reorient_state_status = "ready"; + let reorient_decision = json!({ + "action": match reorient_action { + EpiphanyReorientAction::Resume => "resume", + EpiphanyReorientAction::Regather => "regather", }, - }; - let durable_checkpoint_present = reorient_checkpoint.is_some(); - let (reorient_state_status, reorient_decision) = - recommend_reorientation(EpiphanyReorientInput { - checkpoint: reorient_checkpoint.as_ref(), - state_present: state_ref.is_some(), - pressure_level: reorient_pressure_level, - retrieval_status: reorient_retrieval_status(freshness.retrieval.status), - retrieval_dirty_paths: freshness.retrieval.dirty_paths.clone(), - graph_status: reorient_graph_status(freshness.graph.status), - graph_dirty_paths: freshness.graph.dirty_paths.clone(), - watcher_status: reorient_watcher_status(freshness.watcher.status), - watcher_changed_paths: freshness.watcher.changed_paths.clone(), - watcher_graph_node_ids: freshness.watcher.graph_node_ids.clone(), - active_frontier_node_ids: freshness.watcher.active_frontier_node_ids.clone(), - watched_root: freshness.watcher.watched_root.clone().or(Some(cwd.clone())), - }); + "nextAction": reorient_next_action, + "requestId": reorientation_work.map(|work| work.request.request_id.as_str()), + "basisDigest": reorientation_work.map(|work| work.request.projection.projection_digest.as_str()), + "authority": "keyedMindCurrentWork", + }); let planning = derive_planning_view(state_ref); let mut jobs = derive_jobs(EpiphanyJobsInput { state: state_ref, @@ -361,35 +331,23 @@ fn run_native_status(args: &Args, include_auxiliary_status: bool) -> Result EpiphanyCrrcReorientAction::Resume, - EpiphanyReorientAction::Regather => EpiphanyCrrcReorientAction::Regather, - }; - let result_status = - native_reorient_result_status(state_ref, &runtime_store_path, REORIENT_BINDING_ID); - let reorient_finding = - native_reorient_finding(state_ref, &runtime_store_path, REORIENT_BINDING_ID); - let reorient_finding_accepted = state_ref.is_some_and(|state| { - reorient_finding - .as_ref() - .is_some_and(|finding| reorient_finding_already_accepted(state, finding)) - }); + let result_status = keyed_reorientation_result_status( + &runtime_store_path, + reorientation_work, + latest_reorientation_decision.is_some(), + )?; + let reorient_finding_accepted = latest_reorientation_decision.is_some(); let modeling_result_status = native_role_result_status(state_ref, &runtime_store_path, MODELING_BINDING_ID); let research_result_status = native_role_result_status(state_ref, &runtime_store_path, RESEARCH_BINDING_ID); let verification_result_status = native_role_result_status(state_ref, &runtime_store_path, VERIFICATION_BINDING_ID); - let recommendation = recommend_crrc_action(EpiphanyCrrcInput { - loaded, - state_status, - should_prepare_compaction: pressure.should_prepare_compaction, - reorient_action, - result_status, - checkpoint_present: durable_checkpoint_present, - finding_present: reorient_finding.is_some(), - finding_accepted: reorient_finding_accepted, - }); + let recommendation = keyed_reorientation_recommendation( + reorientation_work, + latest_reorientation_decision, + pressure.should_prepare_compaction, + ); let role_jobs = jobs .iter() .map(|job| EpiphanyRoleBoardJob { @@ -416,14 +374,14 @@ fn run_native_status(args: &Args, include_auxiliary_status: bool) -> Result Result Result Result Result, - runtime_store: &Path, - binding_id: &str, -) -> Value { - let Some(job_id) = latest_runtime_job_id_for_binding(state, binding_id) else { - return json!({ - "source": "native", - "status": "backendMissing", - "bindingId": binding_id, - "note": "No runtime-spine job is linked to this native reorientation binding.", - }); - }; - let snapshot = epiphany_core::read_runtime_reorient_result(Some(runtime_store), job_id); - let mut result = json!({ - "source": "native", - "status": snapshot.status, - "bindingId": binding_id, - "runtimeJobId": job_id, - "note": snapshot.note, - }); - if let Some(finding) = snapshot.finding { - result["finding"] = json!(finding); - } - result -} - fn native_role_result_status( state: Option<&EpiphanyThreadState>, runtime_store: &Path, @@ -959,51 +884,6 @@ fn native_role_finding( epiphany_core::read_runtime_role_result(Some(runtime_store), job_id, role_id).finding } -fn native_reorient_finding( - state: Option<&EpiphanyThreadState>, - runtime_store: &Path, - binding_id: &str, -) -> Option { - let job_id = latest_runtime_job_id_for_binding(state, binding_id)?; - epiphany_core::read_runtime_reorient_result(Some(runtime_store), job_id).finding -} - -fn native_reorient_result_status( - state: Option<&EpiphanyThreadState>, - runtime_store: &Path, - binding_id: &str, -) -> EpiphanyCrrcResultStatus { - let Some(job_id) = latest_runtime_job_id_for_binding(state, binding_id) else { - return EpiphanyCrrcResultStatus::BackendMissing; - }; - match runtime_job_snapshot(runtime_store, job_id) { - Ok(Some(snapshot)) => map_runtime_reorient_result_status(&snapshot), - Ok(None) => EpiphanyCrrcResultStatus::BackendMissing, - Err(_) => EpiphanyCrrcResultStatus::BackendUnavailable, - } -} - -fn crrc_regather_current_after_relinquishment( - state: Option<&EpiphanyThreadState>, - runtime_store: &Path, - relinquishment: Option<&epiphany_core::RepoFrontierRelinquishmentReceipt>, -) -> Result { - let Some(relinquishment) = relinquishment else { - return Ok(true); - }; - let Some(job_id) = latest_runtime_job_id_for_binding(state, REORIENT_BINDING_ID) else { - return Ok(false); - }; - let Some(snapshot) = runtime_job_snapshot(runtime_store, job_id)? else { - return Ok(false); - }; - let reorient_updated_at = chrono::DateTime::parse_from_rfc3339(&snapshot.job.updated_at) - .context("reorientation job updated_at is not RFC3339")?; - let relinquished_at = chrono::DateTime::parse_from_rfc3339(&relinquishment.relinquished_at) - .context("frontier relinquishment timestamp is not RFC3339")?; - Ok(reorient_updated_at > relinquished_at) -} - fn reconcile_native_runtime_jobs( jobs: &mut [epiphany_core::EpiphanyJobView], state: Option<&EpiphanyThreadState>, @@ -1037,33 +917,6 @@ fn latest_runtime_job_id_for_binding<'a>( .map(|link| link.runtime_job_id.as_str()) } -fn reorient_retrieval_status( - status: EpiphanyRetrievalFreshnessStatus, -) -> EpiphanyReorientFreshnessStatus { - match status { - EpiphanyRetrievalFreshnessStatus::Missing - | EpiphanyRetrievalFreshnessStatus::Unavailable => EpiphanyReorientFreshnessStatus::Unknown, - EpiphanyRetrievalFreshnessStatus::Ready => EpiphanyReorientFreshnessStatus::Clean, - EpiphanyRetrievalFreshnessStatus::Stale => EpiphanyReorientFreshnessStatus::Stale, - EpiphanyRetrievalFreshnessStatus::Indexing => EpiphanyReorientFreshnessStatus::Dirty, - } -} - -fn reorient_graph_status(status: EpiphanyGraphFreshnessStatus) -> EpiphanyReorientFreshnessStatus { - match status { - EpiphanyGraphFreshnessStatus::Missing => EpiphanyReorientFreshnessStatus::Unknown, - EpiphanyGraphFreshnessStatus::Stale => EpiphanyReorientFreshnessStatus::Stale, - } -} - -fn reorient_watcher_status(status: EpiphanyInvalidationStatus) -> EpiphanyReorientFreshnessStatus { - match status { - EpiphanyInvalidationStatus::Unavailable => EpiphanyReorientFreshnessStatus::Unknown, - EpiphanyInvalidationStatus::Clean => EpiphanyReorientFreshnessStatus::Clean, - EpiphanyInvalidationStatus::Changed => EpiphanyReorientFreshnessStatus::Changed, - } -} - fn map_runtime_role_result_status( snapshot: &EpiphanyRuntimeJobSnapshot, ) -> EpiphanyCoordinatorRoleResultStatus { @@ -1104,6 +957,103 @@ fn map_runtime_reorient_result_status( } } +fn keyed_reorientation_result_status( + runtime_store: &Path, + work: Option<&epiphany_core::EpiphanyReorientationWorkProjection>, + accepted_decision_present: bool, +) -> Result { + let Some(work) = work else { + return Ok(if accepted_decision_present { + EpiphanyCrrcResultStatus::Completed + } else { + EpiphanyCrrcResultStatus::MissingBinding + }); + }; + let Some(job_id) = work.job_id.as_deref() else { + return Ok(EpiphanyCrrcResultStatus::MissingBinding); + }; + let snapshot = runtime_job_snapshot(runtime_store, job_id)? + .ok_or_else(|| anyhow!("keyed reorientation work lost its runtime job"))?; + Ok(map_runtime_reorient_result_status(&snapshot)) +} + +fn keyed_reorientation_recommendation( + work: Option<&epiphany_core::EpiphanyReorientationWorkProjection>, + accepted: Option<&epiphany_core::EpiphanyMindReorientationDecisionDocument>, + should_prepare_compaction: bool, +) -> EpiphanyCrrcRecommendation { + let build = |action, scene, reason: &str| EpiphanyCrrcRecommendation { + action, + recommended_scene_action: scene, + reason: reason.into(), + }; + if let Some(work) = work { + return match work.action { + epiphany_core::EpiphanyAgentPassContinuationAction::Launch => build( + EpiphanyCrrcAction::LaunchReorientWorker, + Some(EpiphanyCrrcSceneAction::ReorientLaunch), + "A keyed Mind continuity request has no live attempt; launch its exact sealed projection.", + ), + epiphany_core::EpiphanyAgentPassContinuationAction::Wait => build( + EpiphanyCrrcAction::WaitForReorientWorker, + Some(EpiphanyCrrcSceneAction::ReorientResult), + "The exact keyed reorientation attempt is still live.", + ), + epiphany_core::EpiphanyAgentPassContinuationAction::Review => build( + EpiphanyCrrcAction::ReviewReorientResult, + Some(EpiphanyCrrcSceneAction::ReorientResult), + "The exact keyed reorientation pass has a terminal decision or model-backed failure to review.", + ), + }; + } + if should_prepare_compaction { + return build( + EpiphanyCrrcAction::LaunchReorientWorker, + Some(EpiphanyCrrcSceneAction::ReorientLaunch), + "Context pressure requires a new keyed continuity request over the current Mind projection.", + ); + } + if accepted.is_some_and(|decision| decision.mode == "regather") { + return build( + EpiphanyCrrcAction::RegatherManually, + Some(EpiphanyCrrcSceneAction::Reorient), + "The accepted keyed continuity decision requires explicit regather work.", + ); + } + build( + EpiphanyCrrcAction::Continue, + Some(EpiphanyCrrcSceneAction::Reorient), + "No unresolved keyed reorientation obligation exists.", + ) +} + +fn keyed_reorientation_result( + runtime_store: &Path, + work: Option<&epiphany_core::EpiphanyReorientationWorkProjection>, + accepted: Option<&epiphany_core::EpiphanyMindReorientationDecisionDocument>, +) -> Result { + let Some(work) = work else { + return Ok(accepted + .map(|decision| json!({"status": "accepted", "decision": decision})) + .unwrap_or(Value::Null)); + }; + let Some(job_id) = work.job_id.as_deref() else { + return Ok(json!({ + "status": "unlaunched", + "requestId": work.request.request_id, + })); + }; + let snapshot = runtime_job_snapshot(runtime_store, job_id)? + .ok_or_else(|| anyhow!("keyed reorientation work lost its runtime job"))?; + let result = epiphany_core::runtime_reorient_worker_result(runtime_store, job_id)?; + Ok(json!({ + "status": format!("{:?}", snapshot.job.status).to_ascii_lowercase(), + "requestId": work.request.request_id, + "jobId": job_id, + "result": result, + })) +} + fn map_runtime_job_status(status: EpiphanyRuntimeJobStatus) -> EpiphanyJobStatus { match status { EpiphanyRuntimeJobStatus::Queued => EpiphanyJobStatus::Pending, diff --git a/epiphany-core/src/coordinator_acceptance.rs b/epiphany-core/src/coordinator_acceptance.rs index c66604e5..fd9521f3 100644 --- a/epiphany-core/src/coordinator_acceptance.rs +++ b/epiphany-core/src/coordinator_acceptance.rs @@ -1,7 +1,6 @@ use crate::coordinator_results::latest_runtime_link; use crate::coordinator_state::changed_fields; use crate::*; -use anyhow::Context; use epiphany_state_model::EpiphanyThreadState; use std::path::Path; @@ -37,16 +36,6 @@ pub struct EpiphanyRoleAcceptanceUpdate { pub mind_review: MindGatewayReview, } -#[derive(Debug, Clone)] -pub struct EpiphanyReorientAcceptanceUpdate { - pub state_update: EpiphanyStateUpdate, - pub changed_fields: Vec, - pub accepted_receipt_id: String, - pub accepted_observation_id: String, - pub accepted_evidence_id: String, - pub mind_review: MindGatewayReview, -} - #[derive(Debug, Clone)] pub struct EpiphanyNativeRoleAcceptance { pub state: EpiphanyThreadState, @@ -54,118 +43,6 @@ pub struct EpiphanyNativeRoleAcceptance { pub update: EpiphanyRoleAcceptanceUpdate, } -#[derive(Debug, Clone)] -pub struct EpiphanyNativeReorientAcceptance { - pub state: EpiphanyThreadState, - pub finding: EpiphanyReorientFindingInterpretation, - pub update: EpiphanyReorientAcceptanceUpdate, -} - -pub fn accept_coordinator_reorient_finding( - store: &Path, - thread_id: &str, - state: &EpiphanyThreadState, - binding_id: &str, - expected_revision: Option, - reference_turn_id: Option, - accepted_at: String, - nonce: &str, - update_scratch: bool, - update_investigation_checkpoint: bool, -) -> anyhow::Result { - let investigation_checkpoint = if update_investigation_checkpoint { - match state.investigation_checkpoint.clone() { - Some(checkpoint) => Some(checkpoint), - None => { - let model = crate::assemble_repo_model_view(store).context( - "cannot accept reorientation without a current keyed RepoModel checkpoint", - )?; - Some( - epiphany_state_model::reorient_checkpoint_from_admitted_repo_model( - &model.memory_context_projection(), - &model.projection_digest, - ), - ) - } - } - } else { - None - }; - let snapshot = read_reorient_result_snapshot(Some(state), Some(store), binding_id); - if snapshot.status != EpiphanyCrrcResultStatus::Completed { - return Err(anyhow::anyhow!( - "reorientation finding is not completed: {}", - snapshot.note - )); - } - let finding = snapshot - .finding - .ok_or_else(|| anyhow::anyhow!("completed reorientation result has no typed finding"))?; - let update = build_native_reorient_acceptance_update( - expected_revision, - binding_id, - &finding, - format!("ev-reorient-{nonce}"), - format!("obs-reorient-{nonce}"), - accepted_at.clone(), - update_scratch, - update_investigation_checkpoint, - investigation_checkpoint, - ) - .map_err(anyhow::Error::msg)?; - let contract = acceptance_launch_contract_for_binding(store, state, binding_id, "reorient") - .map_err(|error| anyhow::anyhow!(error.to_string()))?; - enforce_acceptance_receipt_proofs( - &contract, - &reorient_acceptance_claimed_effects(), - &[ - MIND_GATEWAY_REVIEW_TYPE.to_string(), - CONTINUITY_RECOVERY_RECEIPT_TYPE.to_string(), - ], - &[ - MIND_GATEWAY_REVIEW_TYPE.to_string(), - CONTINUITY_RECOVERY_RECEIPT_TYPE.to_string(), - ], - ) - .map_err(|error| anyhow::anyhow!(error.to_string()))?; - let recovery = continuity_recovery_receipt_from_reorient_finding( - format!("continuity-recovery-{}", update.accepted_receipt_id), - binding_id.to_string(), - &finding, - accepted_at.clone(), - ); - let next_state = apply_coordinator_state_update_to_state( - state, - update.state_update.clone(), - reference_turn_id, - )?; - let commit = mind_state_commit_receipt( - format!("mind-commit-{}", update.accepted_receipt_id), - &update.mind_review, - next_state.revision, - update - .changed_fields - .iter() - .map(|field| format!("{field:?}")) - .collect(), - accepted_at, - ); - commit_state_with_mind_witness( - store, - thread_id, - state, - &next_state, - &update.mind_review, - &commit, - &[EpiphanyAcceptancePrerequisite::Continuity(recovery)], - )?; - Ok(EpiphanyNativeReorientAcceptance { - state: next_state, - finding, - update, - }) -} - pub fn accept_coordinator_role_finding( store: &Path, thread_id: &str, @@ -332,48 +209,6 @@ pub fn completed_role_finding( }) } -pub fn completed_reorient_finding( - runtime_store_path: Option<&Path>, - state: &EpiphanyThreadState, - binding_id: &str, -) -> Result { - let Some(link) = latest_runtime_link(state, binding_id) else { - return if state - .job_bindings - .iter() - .any(|binding| binding.id == binding_id) - { - Err(EpiphanyCoordinatorAdmissionError::InvalidRequest( - "reorientation findings without runtime-spine results are unsupported; accept only typed runtime-spine results".to_string(), - )) - } else { - Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!( - "epiphany reorientation binding {binding_id:?} was not found" - ))) - }; - }; - let snapshot = read_runtime_reorient_result(runtime_store_path, &link.runtime_job_id); - if snapshot.status != EpiphanyCrrcResultStatus::Completed { - return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!( - "cannot accept reorientation result while worker status is {:?}", - snapshot.status - ))); - } - let store = runtime_store_path.ok_or_else(|| { - EpiphanyCoordinatorAdmissionError::InvalidRequest( - "cannot accept completed reorientation worker without a loaded runtime-spine store" - .to_string(), - ) - })?; - load_launch_organ_contract(store, &link.runtime_job_id, "reorient")?; - snapshot.finding.ok_or_else(|| { - EpiphanyCoordinatorAdmissionError::InvalidRequest( - "cannot accept completed reorientation worker because no typed runtime-spine result was recorded" - .to_string(), - ) - }) -} - pub fn load_launch_organ_contract( runtime_store_path: &Path, job_id: &str, @@ -495,13 +330,6 @@ pub fn role_acceptance_enforceable_receipts(role_id: EpiphanyRoleResultRoleId) - receipts } -pub fn reorient_acceptance_claimed_effects() -> Vec { - vec![ - EpiphanyReceiptEffectKind::StateAdmission, - EpiphanyReceiptEffectKind::ContinuityRecovery, - ] -} - pub fn coordinator_acceptance_cache(store_path: &Path) -> anyhow::Result { let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; @@ -649,76 +477,6 @@ pub fn build_native_role_acceptance_update( }) } -pub fn build_native_reorient_acceptance_update( - expected_revision: Option, - binding_id: &str, - finding: &EpiphanyReorientFindingInterpretation, - accepted_evidence_id: String, - accepted_observation_id: String, - accepted_at: String, - update_scratch: bool, - update_investigation_checkpoint: bool, - checkpoint: Option, -) -> Result { - let mind_finding = EpiphanyReorientAcceptanceFinding { - mode: finding.mode.clone(), - summary: finding.summary.clone(), - next_safe_move: finding.next_safe_move.clone(), - checkpoint_still_valid: finding.checkpoint_still_valid, - files_inspected: finding.files_inspected.clone(), - runtime_result_id: finding.runtime_result_id.clone(), - runtime_job_id: finding.runtime_job_id.clone(), - }; - let mind_review = mind_review_reorient_acceptance( - binding_id, - &mind_finding, - update_scratch, - update_investigation_checkpoint, - ); - mind_review_allows_state(&mind_review)?; - let bundle = build_reorient_acceptance_bundle( - binding_id, - mind_finding, - accepted_evidence_id, - accepted_observation_id, - accepted_at, - update_scratch, - update_investigation_checkpoint - .then_some(checkpoint) - .flatten(), - )?; - let accepted_receipt_id = bundle.accepted_receipt_id.clone(); - let accepted_observation_id = bundle.accepted_observation_id.clone(); - let accepted_evidence_id = bundle.accepted_evidence_id.clone(); - let mut changed_fields = vec![ - EpiphanyStateUpdatedField::AcceptanceReceipts, - EpiphanyStateUpdatedField::Observations, - EpiphanyStateUpdatedField::Evidence, - ]; - if bundle.scratch.is_some() { - changed_fields.push(EpiphanyStateUpdatedField::Scratch); - } - if bundle.investigation_checkpoint.is_some() { - changed_fields.push(EpiphanyStateUpdatedField::InvestigationCheckpoint); - } - Ok(EpiphanyReorientAcceptanceUpdate { - state_update: EpiphanyStateUpdate { - expected_revision, - scratch: bundle.scratch, - investigation_checkpoint: bundle.investigation_checkpoint, - acceptance_receipts: vec![bundle.receipt], - observations: vec![bundle.observation], - evidence: vec![bundle.evidence], - ..Default::default() - }, - changed_fields, - accepted_receipt_id, - accepted_observation_id, - accepted_evidence_id, - mind_review, - }) -} - fn state_update_from_patch( expected_revision: Option, patch: EpiphanyRoleStatePatchDocument, diff --git a/epiphany-core/src/coordinator_service.rs b/epiphany-core/src/coordinator_service.rs index 0cdb6131..808f652d 100644 --- a/epiphany-core/src/coordinator_service.rs +++ b/epiphany-core/src/coordinator_service.rs @@ -5,11 +5,9 @@ use crate::EpiphanyJobInterruptRequest; use crate::EpiphanyJobInterruptResult; use crate::EpiphanyJobLaunchRequest; use crate::EpiphanyJobLaunchResult; -use crate::EpiphanyNativeReorientAcceptance; use crate::EpiphanyNativeRoleAcceptance; use crate::EpiphanyRoleResultRoleId; use crate::EpiphanyStateUpdate; -use crate::accept_coordinator_reorient_finding; use crate::accept_coordinator_role_finding; use crate::apply_coordinator_state_update; use crate::apply_coordinator_state_update_from_state; @@ -162,32 +160,6 @@ impl EpiphanyCoordinatorService { ) } - pub fn accept_reorient( - &self, - thread_id: &str, - state: &EpiphanyThreadState, - binding_id: &str, - expected_revision: Option, - reference_turn_id: Option, - accepted_at: String, - nonce: &str, - update_scratch: bool, - update_investigation_checkpoint: bool, - ) -> Result { - accept_coordinator_reorient_finding( - &self.store, - thread_id, - state, - binding_id, - expected_revision, - reference_turn_id, - accepted_at, - nonce, - update_scratch, - update_investigation_checkpoint, - ) - } - pub fn interrupt_job( &self, thread_id: &str, diff --git a/epiphany-core/src/current_work.rs b/epiphany-core/src/current_work.rs index ed506b55..76dbc7ab 100644 --- a/epiphany-core/src/current_work.rs +++ b/epiphany-core/src/current_work.rs @@ -25,6 +25,7 @@ pub struct EpiphanyCurrentWorkProjection { pub proposal_modeling: Option, pub frontier_verdict_modeling: Option, pub verification: Option, + pub reorientation: Option, pub hands_frontier_ready: bool, } @@ -339,6 +340,7 @@ pub fn project_current_work(store_path: impl AsRef) -> Result, diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 2c9105ad..2a76d596 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -41,6 +41,7 @@ mod prompt_context; mod proposal; mod public_source_identity; mod reasoning_context; +mod reorientation_work; mod repo_model_documents; mod repo_model_gateway; mod repository_body_observer; @@ -106,8 +107,6 @@ pub use agent_launch::build_epiphany_admitted_model_direction_consideration_laun pub use agent_launch::build_epiphany_imagination_consideration_launch_request; pub use agent_launch::build_epiphany_job_launch_request; pub use agent_launch::build_epiphany_reorient_launch_instruction; -pub use agent_launch::build_epiphany_reorient_launch_request; -pub use agent_launch::build_epiphany_reorient_launch_request_with_dynamic_context; pub use agent_launch::build_epiphany_role_launch_request; pub use agent_launch::build_epiphany_role_launch_request_with_dynamic_context; pub use agent_launch::epiphany_admitted_model_direction_consideration_output_schema; @@ -203,22 +202,16 @@ pub use causal_work_identity::*; pub use continuity_gateway::*; pub use coordinator_acceptance::EpiphanyAcceptancePrerequisite; pub use coordinator_acceptance::EpiphanyCoordinatorAdmissionError; -pub use coordinator_acceptance::EpiphanyNativeReorientAcceptance; pub use coordinator_acceptance::EpiphanyNativeRoleAcceptance; -pub use coordinator_acceptance::EpiphanyReorientAcceptanceUpdate; pub use coordinator_acceptance::EpiphanyRoleAcceptanceUpdate; -pub use coordinator_acceptance::accept_coordinator_reorient_finding; pub use coordinator_acceptance::accept_coordinator_role_finding; pub use coordinator_acceptance::acceptance_launch_contract_for_binding; -pub use coordinator_acceptance::build_native_reorient_acceptance_update; pub use coordinator_acceptance::build_native_role_acceptance_update; -pub use coordinator_acceptance::completed_reorient_finding; pub use coordinator_acceptance::completed_role_finding; pub use coordinator_acceptance::coordinator_acceptance_cache; pub use coordinator_acceptance::enforce_acceptance_receipt_proofs; pub use coordinator_acceptance::load_launch_organ_contract; pub use coordinator_acceptance::read_accepted_coordinator_state; -pub use coordinator_acceptance::reorient_acceptance_claimed_effects; pub use coordinator_acceptance::role_acceptance_claimed_effects; pub use coordinator_acceptance::role_acceptance_enforceable_receipts; pub use coordinator_acceptance::state_update_from_role_patch; @@ -974,6 +967,7 @@ pub use proposal::EpiphanyMapProposalInput; pub use proposal::propose_map_update; pub use public_source_identity::ImmutableGithubSource; pub use reasoning_context::*; +pub use reorientation_work::*; pub use repo_model_documents::*; pub use repo_model_gateway::*; pub use repository_body_observer::*; @@ -1267,15 +1261,7 @@ pub use surfaces::EpiphanyPressureStatus; pub use surfaces::EpiphanyReorientAcceptanceBundle; pub use surfaces::EpiphanyReorientAcceptanceFinding; pub use surfaces::EpiphanyReorientAction; -pub use surfaces::EpiphanyReorientCheckpointStatus; -pub use surfaces::EpiphanyReorientDecision; pub use surfaces::EpiphanyReorientFindingInterpretation; -pub use surfaces::EpiphanyReorientFreshnessStatus; -pub use surfaces::EpiphanyReorientInput; -pub use surfaces::EpiphanyReorientLaunchRequestInput; -pub use surfaces::EpiphanyReorientPressureLevel; -pub use surfaces::EpiphanyReorientReason; -pub use surfaces::EpiphanyReorientStateStatus; pub use surfaces::EpiphanyReorientWorkerLaunchDocument; pub use surfaces::EpiphanyRetrievalFreshness; pub use surfaces::EpiphanyRetrievalFreshnessStatus; @@ -1327,7 +1313,6 @@ pub use surfaces::RepoFrontierProposalModelingContextProjection; pub use surfaces::RepoFrontierResearchContextProjection; pub use surfaces::RepoFrontierVerificationContextProjection; pub use surfaces::build_reorient_acceptance_bundle; -pub use surfaces::build_reorient_job_launch_request; pub use surfaces::build_role_acceptance_bundle; pub use surfaces::default_epiphany_view_lenses; pub use surfaces::derive_context; @@ -1347,7 +1332,6 @@ pub use surfaces::interpret_runtime_reorient_worker_result; pub use surfaces::interpret_runtime_role_worker_result; pub use surfaces::modeling_role_state_patch_policy_errors; pub use surfaces::recommend_crrc_action; -pub use surfaces::recommend_reorientation; pub use surfaces::render_role_board_note; pub use surfaces::reorientation_role_status; pub use surfaces::research_role_state_patch_policy_errors; diff --git a/epiphany-core/src/mind_documents.rs b/epiphany-core/src/mind_documents.rs index dbd06ed0..a55697a4 100644 --- a/epiphany-core/src/mind_documents.rs +++ b/epiphany-core/src/mind_documents.rs @@ -263,6 +263,8 @@ pub struct EpiphanyMindView { pub observations: Vec, pub evidence: Vec, pub verification_audits: Vec, + pub reorientation_decisions: Vec, + pub reorientation_failures: Vec, pub investigation_checkpoint: Option, pub mode: Option, pub planning: EpiphanyPlanningState, @@ -280,6 +282,8 @@ pub(crate) fn register_mind_document_types(cache: &mut CultCache) -> 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::()?; cache.register_entry_type::()?; @@ -398,6 +402,28 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu rmp_serde::from_slice(&envelope.payload)?; value.validate()?; value.audit_id + } else if envelope.r#type == crate::EpiphanyMindReorientationDecisionDocument::TYPE { + let value: crate::EpiphanyMindReorientationDecisionDocument = + rmp_serde::from_slice(&envelope.payload)?; + if value.schema_version != crate::MIND_REORIENTATION_DECISION_SCHEMA_VERSION + || value.decision_id.trim().is_empty() + || value.request_id.trim().is_empty() + || value.decision_context_id.trim().is_empty() + { + return Err(anyhow!("Mind reorientation decision is invalid")); + } + value.decision_id + } else if envelope.r#type == crate::EpiphanyMindReorientationPassFailureDocument::TYPE { + let value: crate::EpiphanyMindReorientationPassFailureDocument = + rmp_serde::from_slice(&envelope.payload)?; + if value.schema_version != crate::MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION + || value.failure_id.trim().is_empty() + || value.request_id.trim().is_empty() + || value.decision_context_id.trim().is_empty() + { + return Err(anyhow!("Mind reorientation failure is invalid")); + } + value.failure_id } else if envelope.r#type == EpiphanyMindInvestigationCheckpointDocument::TYPE { rmp_serde::from_slice::(&envelope.payload)? .value @@ -462,6 +488,10 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result(&cache, |value| value.value)?; let mut evidence = values::(&cache, |value| value.value)?; let mut verification_audits = cache.get_all::()?; + let mut reorientation_decisions = + cache.get_all::()?; + let mut reorientation_failures = + cache.get_all::()?; let checkpoints = values::(&cache, |value| value.value)?; let mut captures = @@ -503,6 +533,8 @@ pub fn assemble_mind_view(store_path: impl AsRef) -> Result) -> Result, pub evidence: Vec, pub verification_audits: Vec, + pub reorientation_decisions: Vec, + pub reorientation_failures: Vec, pub investigation_checkpoint: Option, pub mode: Option, pub planning: epiphany_state_model::EpiphanyPlanningState, @@ -130,6 +132,8 @@ impl From for EpiphanyMindPromptProjection { observations: value.observations, evidence: value.evidence, verification_audits: value.verification_audits, + reorientation_decisions: value.reorientation_decisions, + reorientation_failures: value.reorientation_failures, investigation_checkpoint: value.investigation_checkpoint, mode: value.mode, planning: value.planning, @@ -145,6 +149,12 @@ pub struct EpiphanyRoleReasoningProjection { pub mind: EpiphanyMindPromptProjection, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EpiphanyReorientationReasoningProjection { + pub authority: crate::EpiphanyReorientWorkerLaunchDocument, + pub request: crate::EpiphanyReorientationRequest, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct EpiphanyMindDocumentVersion { pub store_id: String, @@ -194,7 +204,7 @@ impl EpiphanyMindDocumentVersion { #[serde(tag = "kind", rename_all = "snake_case")] pub enum EpiphanyReasoningProjection { RolePass(EpiphanyRoleReasoningProjection), - ReorientLaunch(crate::EpiphanyReorientWorkerLaunchDocument), + ReorientLaunch(EpiphanyReorientationReasoningProjection), PersonaProjector(PersonaProjectorInput), PersonaTurn(PersonaTurnInput), PersonaInterpreter(PersonaInterpreterInput), @@ -462,7 +472,7 @@ pub fn worker_reasoning_basis( ) -> Result { let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; - let launch_envelope = cache + cache .get_envelope::(&launch.job_id)? .ok_or_else(|| anyhow!("worker reasoning basis lost its launch envelope"))?; let launch_document = launch.launch_document()?; @@ -478,13 +488,21 @@ pub fn worker_reasoning_basis( }), ) } - EpiphanyWorkerLaunchDocument::Reorient(document) => ( - vec![EpiphanyMindDocumentVersion::from_envelope( - "epiphany-mind", - &launch_envelope, - )?], - EpiphanyReasoningProjection::ReorientLaunch(document), - ), + EpiphanyWorkerLaunchDocument::Reorient(document) => { + let request = cache + .get::(&document.request_id)? + .ok_or_else(|| anyhow!("reorientation reasoning basis lost its request"))?; + crate::reorientation_work::validate_reorientation_request_current(&cache, &request)?; + ( + request.source_documents.clone(), + EpiphanyReasoningProjection::ReorientLaunch( + EpiphanyReorientationReasoningProjection { + authority: document, + request, + }, + ), + ) + } }; EpiphanyReasoningBasis::new( &launch.job_id, @@ -545,29 +563,54 @@ pub fn put_reasoning_basis( basis.validate()?; let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; - if let EpiphanyReasoningProjection::RolePass(projection) = basis.projection()? { - let launch = cache - .get::(&basis.pass_id)? - .ok_or_else(|| anyhow!("role reasoning basis lost its worker launch"))?; - let EpiphanyWorkerLaunchDocument::Role(document) = launch.launch_document()? else { - return Err(anyhow!("role reasoning basis cites a non-role launch")); - }; - let current_mind = crate::assemble_mind_view(store_path)?; - let modeling_body_mismatch = projection - .authority - .role_id - .eq_ignore_ascii_case("modeling") - && projection.authority.repository_body_observation_basis - != current_mind.repository_body_observation; - if projection.authority != document.into() - || projection.mind != current_mind.clone().into() - || basis.source_documents != current_mind.source_documents - || modeling_body_mismatch - { - return Err(anyhow!( - "role reasoning projection diverges from its exact launch or keyed Mind sources" - )); + match basis.projection()? { + EpiphanyReasoningProjection::RolePass(projection) => { + let launch = cache + .get::(&basis.pass_id)? + .ok_or_else(|| anyhow!("role reasoning basis lost its worker launch"))?; + let EpiphanyWorkerLaunchDocument::Role(document) = launch.launch_document()? else { + return Err(anyhow!("role reasoning basis cites a non-role launch")); + }; + let current_mind = crate::assemble_mind_view(store_path)?; + let modeling_body_mismatch = projection + .authority + .role_id + .eq_ignore_ascii_case("modeling") + && projection.authority.repository_body_observation_basis + != current_mind.repository_body_observation; + if projection.authority != document.into() + || projection.mind != current_mind.clone().into() + || basis.source_documents != current_mind.source_documents + || modeling_body_mismatch + { + return Err(anyhow!( + "role reasoning projection diverges from its exact launch or keyed Mind sources" + )); + } } + EpiphanyReasoningProjection::ReorientLaunch(projection) => { + let launch = cache + .get::(&basis.pass_id)? + .ok_or_else(|| anyhow!("reorientation reasoning basis lost its worker launch"))?; + let EpiphanyWorkerLaunchDocument::Reorient(document) = launch.launch_document()? else { + return Err(anyhow!("reorientation basis cites a non-reorient launch")); + }; + if projection.authority != document + || projection.request.request_id != document.request_id + || projection.request.source_documents != basis.source_documents + { + return Err(anyhow!( + "reorientation reasoning projection diverges from its request" + )); + } + crate::reorientation_work::validate_reorientation_request_current( + &cache, + &projection.request, + )?; + } + EpiphanyReasoningProjection::PersonaProjector(_) + | EpiphanyReasoningProjection::PersonaTurn(_) + | EpiphanyReasoningProjection::PersonaInterpreter(_) => {} } for source in &basis.source_documents { if source.store_id != "epiphany-mind" { @@ -1334,6 +1377,8 @@ mod tests { observations: Vec::new(), evidence: Vec::new(), verification_audits: Vec::new(), + reorientation_decisions: Vec::new(), + reorientation_failures: Vec::new(), investigation_checkpoint: None, mode: None, planning: Default::default(), diff --git a/epiphany-core/src/reorientation_work.rs b/epiphany-core/src/reorientation_work.rs new file mode 100644 index 00000000..b89e1689 --- /dev/null +++ b/epiphany-core/src/reorientation_work.rs @@ -0,0 +1,1189 @@ +use anyhow::{Result, anyhow}; +use cultcache_rs::{CultCache, DatabaseEntry}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::path::Path; + +use crate::{ + EpiphanyAgentPassContinuationAction, EpiphanyMindDocumentVersion, EpiphanyRuntimeJobStatus, +}; + +pub const REORIENTATION_REQUEST_SCHEMA_VERSION: &str = "epiphany.self.reorientation_request.v1"; +pub const REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION: &str = + "epiphany.runtime.reorientation_launch.v1"; +pub const MIND_REORIENTATION_DECISION_SCHEMA_VERSION: &str = + "epiphany.mind.reorientation_decision.v1"; +pub const MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION: &str = + "epiphany.mind.reorientation_pass_failure.v1"; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EpiphanyReorientationStateProjection { + pub runtime_id: String, + pub objective: Option, + pub active_subgoal_id: Option, + pub investigation_checkpoint: Option, + pub mode: Option, + pub repository_body_observation: Option, + pub frontier: Vec, + pub projection_digest: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.self.reorientation_request.v1", + schema = "EpiphanyReorientationRequest" +)] +pub struct EpiphanyReorientationRequest { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub request_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub projection: EpiphanyReorientationStateProjection, + #[cultcache(key = 4)] + pub source_documents: Vec, + #[cultcache(key = 5)] + pub requested_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.runtime.reorientation_launch.v1", + schema = "EpiphanyReorientationLaunchBinding" +)] +pub struct EpiphanyReorientationLaunchBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_id: String, + #[cultcache(key = 2)] + pub request_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub attempt_ordinal: u64, + #[cultcache(key = 5)] + pub launched_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.reorientation_decision.v1", + schema = "EpiphanyMindReorientationDecisionDocument" +)] +pub struct EpiphanyMindReorientationDecisionDocument { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub decision_id: String, + #[cultcache(key = 2)] + pub request_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub result_id: String, + #[cultcache(key = 5)] + pub decision_context_id: String, + #[cultcache(key = 6)] + pub mode: String, + #[cultcache(key = 7)] + pub checkpoint_still_valid: Option, + #[cultcache(key = 8)] + pub summary: String, + #[cultcache(key = 9)] + pub next_safe_move: String, + #[cultcache(key = 10)] + pub decided_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.reorientation_pass_failure.v1", + schema = "EpiphanyMindReorientationPassFailureDocument" +)] +pub struct EpiphanyMindReorientationPassFailureDocument { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub failure_id: String, + #[cultcache(key = 2)] + pub request_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub runtime_result_id: String, + #[cultcache(key = 5)] + pub decision_context_id: String, + #[cultcache(key = 6)] + pub verdict: String, + #[cultcache(key = 7)] + pub summary: String, + #[cultcache(key = 8)] + pub failed_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EpiphanyReorientationWorkProjection { + pub request: EpiphanyReorientationRequest, + pub action: EpiphanyAgentPassContinuationAction, + pub job_id: Option, +} + +pub fn request_current_reorientation( + store_path: impl AsRef, + requested_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(requested_at) + .map_err(|_| anyhow!("reorientation request time is invalid"))?; + let store_path = store_path.as_ref(); + let mind = crate::assemble_mind_view(store_path)?; + let mut source_documents = mind + .source_documents + .iter() + .filter(|source| { + matches!( + source.document_type.as_str(), + crate::EpiphanyMindIdentity::TYPE + | crate::EpiphanyMindObjectiveDocument::TYPE + | crate::EpiphanyMindFocusDocument::TYPE + | crate::EpiphanyMindModeDocument::TYPE + | crate::EpiphanyMindInvestigationCheckpointDocument::TYPE + | crate::EpiphanyMindRepositoryBodyObservationDocument::TYPE + | crate::EpiphanyMindReorientationDecisionDocument::TYPE + | crate::EpiphanyRepoModelIdentityDocument::TYPE + | crate::EpiphanyRepoModelFrontierDocument::TYPE + ) + }) + .cloned() + .collect::>(); + source_documents.sort_by(|left, right| { + (&left.document_type, &left.document_key).cmp(&(&right.document_type, &right.document_key)) + }); + let projection_digest = crate::epiphany_mind_projection_digest(&source_documents)?; + let projection = EpiphanyReorientationStateProjection { + runtime_id: mind.runtime_id.clone(), + objective: mind.objective, + active_subgoal_id: mind.active_subgoal_id, + investigation_checkpoint: mind.investigation_checkpoint, + mode: mind.mode, + repository_body_observation: mind.repository_body_observation, + frontier: mind + .repo_model + .map(|model| model.frontier) + .unwrap_or_default(), + projection_digest: projection_digest.clone(), + }; + let request_id = format!( + "reorientation-request-{:x}", + Sha256::digest(rmp_serde::to_vec_named(&( + REORIENTATION_REQUEST_SCHEMA_VERSION, + mind.runtime_id.as_str(), + projection_digest.as_str(), + ))?) + ); + let request = EpiphanyReorientationRequest { + schema_version: REORIENTATION_REQUEST_SCHEMA_VERSION.into(), + request_id: request_id.clone(), + runtime_id: mind.runtime_id, + projection, + source_documents, + requested_at: requested_at.into(), + }; + validate_reorientation_request_intrinsic(&request)?; + let mut cache = crate::runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + if let Some(existing) = cache.get::(&request_id)? { + validate_reorientation_request_current(&cache, &existing)?; + return Ok(existing); + } + let expected = exact_source_envelopes(&cache, &request.source_documents)?; + let mut replacements = expected.clone(); + let write = cache.prepare_entry(&request_id, &request)?.0; + replacements.push(write); + if !crate::runtime_store_backend::runtime_spine_backing_store(store_path)? + .compare_and_swap_batch(&expected, replacements)? + { + return request_current_reorientation(store_path, requested_at); + } + Ok(request) +} + +pub(crate) fn current_reorientation_work( + cache: &CultCache, +) -> Result> { + let mut requests = cache.get_all::()?; + requests.sort_by(|left, right| left.request_id.cmp(&right.request_id)); + let decisions = cache.get_all::()?; + let failures = cache.get_all::()?; + for request in requests.into_iter().rev() { + if validate_reorientation_request_current(cache, &request).is_err() + || decisions + .iter() + .any(|decision| decision.request_id == request.request_id) + { + continue; + } + let mut bindings = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.request_id == request.request_id) + .collect::>(); + bindings.sort_by_key(|binding| binding.attempt_ordinal); + let Some(binding) = bindings.last() else { + return Ok(Some(EpiphanyReorientationWorkProjection { + request, + action: EpiphanyAgentPassContinuationAction::Launch, + job_id: None, + })); + }; + let job = cache + .get::(&binding.job_id)? + .ok_or_else(|| anyhow!("reorientation launch lost its runtime job"))?; + let result = cache.get::(&binding.job_id)?; + let action = match job.status { + EpiphanyRuntimeJobStatus::Completed if result.is_some() => { + EpiphanyAgentPassContinuationAction::Review + } + EpiphanyRuntimeJobStatus::Failed => { + let terminal = exact_runtime_job_result(cache, &binding.job_id)?; + match terminal.decision_context_id.as_deref() { + Some(context_id) + if failures.iter().any(|failure| { + failure.request_id == request.request_id + && failure.job_id == binding.job_id + && failure.runtime_result_id == terminal.result_id + && failure.decision_context_id == context_id + }) => + { + EpiphanyAgentPassContinuationAction::Launch + } + Some(_) => EpiphanyAgentPassContinuationAction::Review, + None => EpiphanyAgentPassContinuationAction::Launch, + } + } + EpiphanyRuntimeJobStatus::Cancelled => EpiphanyAgentPassContinuationAction::Launch, + _ => EpiphanyAgentPassContinuationAction::Wait, + }; + return Ok(Some(EpiphanyReorientationWorkProjection { + request, + action, + job_id: Some(binding.job_id.clone()), + })); + } + Ok(None) +} + +pub fn record_reorientation_pass_failure( + store_path: impl AsRef, + job_id: &str, +) -> Result { + let store_path = store_path.as_ref(); + let mut cache = crate::runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let binding = cache + .get_all::()? + .into_iter() + .find(|binding| binding.job_id == job_id) + .ok_or_else(|| anyhow!("reorientation failure has no exact launch binding"))?; + let request = cache + .get::(&binding.request_id)? + .ok_or_else(|| anyhow!("reorientation failure lost its request"))?; + validate_reorientation_request_intrinsic(&request)?; + let job = cache + .get::(job_id)? + .ok_or_else(|| anyhow!("reorientation failure lost its runtime job"))?; + if job.status != EpiphanyRuntimeJobStatus::Failed { + return Err(anyhow!("reorientation pass is not a failed model pass")); + } + let terminal = exact_runtime_job_result(&cache, job_id)?; + let decision_context_id = terminal + .decision_context_id + .as_deref() + .ok_or_else(|| anyhow!("model-backed reorientation failure has no decision context"))?; + let context = cache + .get::(decision_context_id)? + .ok_or_else(|| anyhow!("reorientation failure lost its decision context"))?; + let basis = cache + .get::(&context.basis_id)? + .ok_or_else(|| anyhow!("reorientation failure lost its reasoning basis"))?; + context.validate(&basis)?; + match basis.projection()? { + crate::EpiphanyReasoningProjection::ReorientLaunch(projection) + if projection.request == request => {} + _ => return Err(anyhow!("reorientation failure crossed its sealed request")), + } + let failure = EpiphanyMindReorientationPassFailureDocument { + schema_version: MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION.into(), + failure_id: format!("reorientation-pass-failure-{}", terminal.result_id), + request_id: request.request_id.clone(), + job_id: job_id.into(), + runtime_result_id: terminal.result_id.clone(), + decision_context_id: decision_context_id.into(), + verdict: terminal.verdict.clone(), + summary: terminal.summary.clone(), + failed_at: terminal.completed_at.clone(), + }; + if let Some(existing) = + cache.get::(&failure.failure_id)? + { + if existing != failure { + return Err(anyhow!("reorientation failure replay was substituted")); + } + return mind_receipt_for_failure_context(&cache, decision_context_id); + } + let snapshot = cache.snapshot_envelopes(); + let mut strong_reads = Vec::new(); + for (document_type, document_key) in [ + ( + EpiphanyReorientationRequest::TYPE, + request.request_id.as_str(), + ), + ( + EpiphanyReorientationLaunchBinding::TYPE, + binding.binding_id.as_str(), + ), + (crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id), + (crate::EpiphanyRuntimeJob::TYPE, job_id), + ( + crate::EpiphanyRuntimeJobResult::TYPE, + terminal.result_id.as_str(), + ), + (crate::EpiphanyDecisionContext::TYPE, decision_context_id), + ] { + strong_reads.push( + snapshot + .iter() + .find(|envelope| envelope.r#type == document_type && envelope.key == document_key) + .cloned() + .ok_or_else(|| anyhow!("reorientation failure lost a strong source"))?, + ); + } + let write = + crate::mind_documents::prepare_mind_document(&cache, &failure.failure_id, &failure)?; + match crate::reasoning_context::commit_mind_mutation_with_derived_companions( + store_path, + decision_context_id, + "Continuity.reorientation_failure", + strong_reads, + vec![write], + Vec::new(), + &terminal.completed_at, + )? { + crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt), + crate::EpiphanyMindCommitOutcome::Conflict { + document_identities, + } => Err(anyhow!( + "reorientation failure record lost exact runtime authority: {document_identities:?}" + )), + } +} + +pub fn launch_current_reorientation_work( + store_path: impl AsRef, + created_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(created_at) + .map_err(|_| anyhow!("reorientation launch time is invalid"))?; + let store_path = store_path.as_ref(); + let mut cache = crate::runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let work = current_reorientation_work(&cache)? + .ok_or_else(|| anyhow!("Mind has no unresolved reorientation request"))?; + if work.action != EpiphanyAgentPassContinuationAction::Launch { + return Err(anyhow!("reorientation request is not launchable")); + } + validate_reorientation_request_current(&cache, &work.request)?; + let attempt_ordinal = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.request_id == work.request.request_id) + .count() as u64; + let job_id = format!( + "reorientation-{}-attempt-{attempt_ordinal}", + work.request.request_id + ); + let launch_document = crate::EpiphanyWorkerLaunchDocument::Reorient( + crate::EpiphanyReorientWorkerLaunchDocument { + schema_version: "epiphany.reorientation_launch_projection.v1".into(), + request_id: work.request.request_id.clone(), + creation_thread_id: job_id.clone(), + }, + ); + let output_contract_id = launch_document.output_contract_id().to_string(); + let prepared = crate::prepare_runtime_spine_heartbeat_job( + &cache, + crate::RuntimeSpineHeartbeatJobOptions { + runtime_id: work.request.runtime_id.clone(), + display_name: "Epiphany Local".into(), + session_id: crate::EPIPHANY_RUNTIME_ROOT_SESSION_ID.into(), + objective: "Reorient from one exact keyed Mind projection".into(), + coordinator_note: "Reorientation current-work owner opened this pass.".into(), + job_id: job_id.clone(), + role: crate::EPIPHANY_REORIENT_OWNER_ROLE.into(), + binding_id: crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(), + authority_scope: "epiphany.reorient.decision".into(), + instruction: "Decide resume or regather from only the sealed typed continuity projection. Return the reorientation result contract; do not invent live state.".into(), + launch_document, + output_contract_id: output_contract_id.clone(), + organ_launch_contract: crate::default_launch_organ_contract( + "epiphany.reorient.decision", + "reorient", + &output_contract_id, + ), + proposal_modeling_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_verification_request_id: None, + created_at: created_at.into(), + }, + )?; + let binding = EpiphanyReorientationLaunchBinding { + schema_version: REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_id: format!("reorientation-launch-{job_id}"), + request_id: work.request.request_id.clone(), + job_id: job_id.clone(), + attempt_ordinal, + launched_at: created_at.into(), + }; + let expected = exact_source_envelopes(&cache, &work.request.source_documents)? + .into_iter() + .chain( + cache + .get_envelope::(&work.request.request_id)? + .into_iter(), + ) + .collect(); + crate::current_work::commit_current_work_launch( + store_path, + &cache, + expected, + prepared.envelopes, + vec![cache.prepare_entry(&binding.binding_id, &binding)?.0], + "reorientation", + )?; + Ok(job_id) +} + +pub fn accept_reorientation_result( + store_path: impl AsRef, + job_id: &str, + accepted_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(accepted_at) + .map_err(|_| anyhow!("reorientation acceptance time is invalid"))?; + let store_path = store_path.as_ref(); + let mut cache = crate::runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let binding = cache + .get_all::()? + .into_iter() + .find(|binding| binding.job_id == job_id) + .ok_or_else(|| anyhow!("reorientation result has no exact launch binding"))?; + let request = cache + .get::(&binding.request_id)? + .ok_or_else(|| anyhow!("reorientation result lost its request"))?; + validate_reorientation_request_intrinsic(&request)?; + let job = cache + .get::(job_id)? + .ok_or_else(|| anyhow!("reorientation result lost its runtime job"))?; + if job.status != EpiphanyRuntimeJobStatus::Completed { + return Err(anyhow!("reorientation runtime job is not completed")); + } + let result = cache + .get::(job_id)? + .ok_or_else(|| anyhow!("reorientation pass has no typed result"))?; + if result.item_error.is_some() + || !matches!( + result.mode.trim().to_ascii_lowercase().as_str(), + "resume" | "regather" + ) + || result.summary.trim().is_empty() + || result.next_safe_move.trim().is_empty() + { + return Err(anyhow!( + "reorientation result is not an admissible decision" + )); + } + let context = cache + .get::(&result.decision_context_id)? + .ok_or_else(|| anyhow!("reorientation result lost its decision context"))?; + let basis = cache + .get::(&context.basis_id)? + .ok_or_else(|| anyhow!("reorientation result lost its reasoning basis"))?; + match basis.projection()? { + crate::EpiphanyReasoningProjection::ReorientLaunch(projection) + if projection.request == request => {} + _ => return Err(anyhow!("reorientation result crossed its sealed request")), + } + if let Some(existing) = cache + .get_all::()? + .into_iter() + .find(|decision| decision.request_id == request.request_id) + { + if existing.job_id != job_id + || existing.result_id != result.result_id + || existing.decision_context_id != result.decision_context_id + { + return Err(anyhow!("reorientation decision replay was substituted")); + } + return mind_receipt_for_context(&cache, &result.decision_context_id); + } + let decision = EpiphanyMindReorientationDecisionDocument { + schema_version: MIND_REORIENTATION_DECISION_SCHEMA_VERSION.into(), + decision_id: format!("reorientation-decision-{}", request.request_id), + request_id: request.request_id.clone(), + job_id: job_id.into(), + result_id: result.result_id.clone(), + decision_context_id: result.decision_context_id.clone(), + mode: result.mode.trim().to_ascii_lowercase(), + checkpoint_still_valid: result.checkpoint_still_valid, + summary: result.summary.clone(), + next_safe_move: result.next_safe_move.clone(), + decided_at: accepted_at.into(), + }; + let finding = crate::interpret_runtime_reorient_worker_result(&result); + let recovery = crate::continuity_recovery_receipt_from_reorient_finding( + format!("continuity-recovery-{}", request.request_id), + crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(), + &finding, + accepted_at.into(), + ); + let mut strong_reads = exact_source_envelopes(&cache, &request.source_documents)?; + for (document_type, document_key) in [ + ( + EpiphanyReorientationRequest::TYPE, + request.request_id.as_str(), + ), + ( + EpiphanyReorientationLaunchBinding::TYPE, + binding.binding_id.as_str(), + ), + (crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id), + (crate::EpiphanyRuntimeJob::TYPE, job_id), + (crate::EpiphanyRuntimeReorientWorkerResult::TYPE, job_id), + ( + crate::EpiphanyDecisionContext::TYPE, + result.decision_context_id.as_str(), + ), + ] { + strong_reads.push( + cache + .snapshot_envelopes() + .into_iter() + .find(|envelope| envelope.r#type == document_type && envelope.key == document_key) + .ok_or_else(|| anyhow!("reorientation admission lost a strong source"))?, + ); + } + let write = + crate::mind_documents::prepare_mind_document(&cache, &decision.decision_id, &decision)?; + let companion = cache.prepare_entry(&recovery.receipt_id, &recovery)?.0; + match crate::reasoning_context::commit_mind_mutation_with_derived_companions( + store_path, + &result.decision_context_id, + "Continuity.reorientation", + strong_reads, + vec![write], + vec![companion], + accepted_at, + )? { + crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt), + crate::EpiphanyMindCommitOutcome::Conflict { + document_identities, + } => Err(anyhow!( + "reorientation output cannot be rebased onto changed continuity authority: {document_identities:?}" + )), + } +} + +pub(crate) fn validate_reorientation_request_intrinsic( + request: &EpiphanyReorientationRequest, +) -> Result<()> { + if request.schema_version != REORIENTATION_REQUEST_SCHEMA_VERSION + || request.request_id.trim().is_empty() + || request.runtime_id.trim().is_empty() + || request.projection.runtime_id != request.runtime_id + || request.projection.projection_digest + != crate::epiphany_mind_projection_digest(&request.source_documents)? + || request.source_documents.is_empty() + || chrono::DateTime::parse_from_rfc3339(&request.requested_at).is_err() + { + return Err(anyhow!("reorientation request is invalid")); + } + let expected_id = format!( + "reorientation-request-{:x}", + Sha256::digest(rmp_serde::to_vec_named(&( + REORIENTATION_REQUEST_SCHEMA_VERSION, + request.runtime_id.as_str(), + request.projection.projection_digest.as_str(), + ))?) + ); + if request.request_id != expected_id { + return Err(anyhow!("reorientation request identity is not canonical")); + } + if projection_from_sources(&request.source_documents)? != request.projection { + return Err(anyhow!( + "reorientation projection diverges from its exact Mind documents" + )); + } + Ok(()) +} + +pub(crate) fn validate_reorientation_request_current( + cache: &CultCache, + request: &EpiphanyReorientationRequest, +) -> Result<()> { + validate_reorientation_request_intrinsic(request)?; + exact_source_envelopes(cache, &request.source_documents)?; + Ok(()) +} + +fn exact_source_envelopes( + cache: &CultCache, + sources: &[EpiphanyMindDocumentVersion], +) -> Result> { + let snapshot = cache.snapshot_envelopes(); + sources + .iter() + .map(|source| { + let envelope = snapshot + .iter() + .find(|envelope| { + envelope.r#type == source.document_type && envelope.key == source.document_key + }) + .cloned() + .ok_or_else(|| anyhow!("reorientation source document is absent"))?; + if EpiphanyMindDocumentVersion::from_envelope("epiphany-mind", &envelope)? != *source { + return Err(anyhow!("reorientation source document changed")); + } + Ok(envelope) + }) + .collect() +} + +fn projection_from_sources( + sources: &[EpiphanyMindDocumentVersion], +) -> Result { + let mut runtime_id = None; + let mut objective = None; + let mut focus = None; + let mut checkpoints = Vec::new(); + let mut mode = None; + let mut bodies = Vec::new(); + let mut frontier = Vec::new(); + for source in sources { + match source.document_type.as_str() { + crate::EpiphanyMindIdentity::TYPE => { + runtime_id = Some( + rmp_serde::from_slice::(&source.payload_msgpack)? + .runtime_id, + ); + } + crate::EpiphanyMindObjectiveDocument::TYPE => { + objective = Some( + rmp_serde::from_slice::( + &source.payload_msgpack, + )? + .objective, + ); + } + crate::EpiphanyMindFocusDocument::TYPE => { + focus = Some(rmp_serde::from_slice::( + &source.payload_msgpack, + )?); + } + crate::EpiphanyMindInvestigationCheckpointDocument::TYPE => checkpoints.push( + rmp_serde::from_slice::( + &source.payload_msgpack, + )? + .value, + ), + crate::EpiphanyMindModeDocument::TYPE => { + mode = Some( + rmp_serde::from_slice::( + &source.payload_msgpack, + )? + .mode, + ); + } + crate::EpiphanyMindRepositoryBodyObservationDocument::TYPE => bodies.push( + rmp_serde::from_slice::( + &source.payload_msgpack, + )? + .basis, + ), + crate::EpiphanyRepoModelFrontierDocument::TYPE => frontier.push( + rmp_serde::from_slice::( + &source.payload_msgpack, + )? + .value()?, + ), + crate::EpiphanyRepoModelIdentityDocument::TYPE + | crate::EpiphanyMindReorientationDecisionDocument::TYPE => {} + other => { + return Err(anyhow!( + "reorientation request contains non-projection source {other:?}" + )); + } + } + } + let runtime_id = + runtime_id.ok_or_else(|| anyhow!("reorientation projection has no Mind identity"))?; + let active_subgoal_id = focus + .as_ref() + .and_then(|value| value.active_subgoal_id.clone()); + let checkpoint_id = focus + .as_ref() + .and_then(|value| value.investigation_checkpoint_id.as_deref()); + let investigation_checkpoint = match checkpoint_id { + Some(id) => Some( + checkpoints + .into_iter() + .find(|checkpoint| checkpoint.checkpoint_id == id) + .ok_or_else(|| anyhow!("reorientation focus cites a missing checkpoint"))?, + ), + None => None, + }; + bodies.sort_by_key(|body| body.generation); + frontier.sort_by(|left, right| left.id.cmp(&right.id)); + Ok(EpiphanyReorientationStateProjection { + runtime_id, + objective, + active_subgoal_id, + investigation_checkpoint, + mode, + repository_body_observation: bodies.pop(), + frontier, + projection_digest: crate::epiphany_mind_projection_digest(sources)?, + }) +} + +fn mind_receipt_for_context( + cache: &CultCache, + decision_context_id: &str, +) -> Result { + let mut receipts = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.invariant_owner == "Continuity.reorientation" + && matches!( + &receipt.authority, + crate::EpiphanyMindCommitAuthority::ModelDecisionContext { + decision_context_id: id + } if id == decision_context_id + ) + }) + .collect::>(); + match receipts.len() { + 1 => Ok(receipts.remove(0)), + 0 => Err(anyhow!( + "reorientation decision lost its Mind commit receipt" + )), + _ => Err(anyhow!( + "reorientation decision has multiple Mind commit receipts" + )), + } +} + +fn mind_receipt_for_failure_context( + cache: &CultCache, + decision_context_id: &str, +) -> Result { + let mut receipts = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.invariant_owner == "Continuity.reorientation_failure" + && matches!( + &receipt.authority, + crate::EpiphanyMindCommitAuthority::ModelDecisionContext { + decision_context_id: id + } if id == decision_context_id + ) + }) + .collect::>(); + match receipts.len() { + 1 => Ok(receipts.remove(0)), + 0 => Err(anyhow!( + "reorientation failure lost its Mind commit receipt" + )), + _ => Err(anyhow!( + "reorientation failure has multiple Mind commit receipts" + )), + } +} + +fn exact_runtime_job_result( + cache: &CultCache, + job_id: &str, +) -> Result { + let mut results = cache + .get_all::()? + .into_iter() + .filter(|result| result.job_id == job_id) + .collect::>(); + match results.len() { + 1 => Ok(results.remove(0)), + 0 => Err(anyhow!( + "reorientation attempt lost its terminal runtime result" + )), + _ => Err(anyhow!( + "reorientation attempt has multiple terminal runtime results" + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use epiphany_model_adapter::EpiphanyModelRequest; + + #[test] + fn reorientation_has_no_aggregate_or_freshness_authority_path() { + let coordinator = include_str!("bin/epiphany-mvp-coordinator.rs"); + let status = include_str!("bin/epiphany-mvp-status.rs"); + let acceptance = include_str!("coordinator_acceptance.rs"); + let agent_launch = include_str!("agent_launch.rs"); + let worker_launch = include_str!("surfaces/worker_launch.rs"); + for (source, forbidden) in [ + (coordinator, "build_epiphany_reorient_launch_request"), + (status, "recommend_reorientation"), + (status, "crrc_regather_current_after_relinquishment"), + (acceptance, "accept_coordinator_reorient_finding"), + (acceptance, "build_native_reorient_acceptance_update"), + (agent_launch, "build_epiphany_reorient_launch_request"), + (worker_launch, "EpiphanyReorientLaunchRequestInput"), + ] { + assert!(!source.contains(forbidden), "obsolete reorientation authority {forbidden:?} survived"); + } + } + + #[test] + fn reorientation_is_decision_auditable_thread_free_and_keyed() -> Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("reorientation.cc"); + crate::initialize_runtime_spine( + &store, + crate::RuntimeSpineInitOptions { + runtime_id: "runtime".into(), + display_name: "Reorientation test".into(), + created_at: "2026-08-18T10:00:00Z".into(), + }, + )?; + crate::runtime_spine::tests::bind_test_runtime_swarm(&store, "reorientation-swarm")?; + crate::runtime_spine::tests::bind_test_repository_body(&store, "reorientation-workspace")?; + let body = crate::observe_runtime_repository_body_basis(&store)?; + crate::initialize_keyed_repo_model( + &store, + &crate::EpiphanyRepoModelSeed::new( + "reorientation-seed", + "reorientation-graph", + "reorientation-swarm", + "reorientation-workspace", + body.body_binding_sha256, + crate::EpiphanyRepoModelSeedDocuments { + domains: Vec::new(), + nodes: Vec::new(), + edges: Vec::new(), + summaries: Vec::new(), + frontier: Vec::new(), + lifecycle_receipts: Vec::new(), + }, + )?, + "2026-08-18T10:00:00Z", + )?; + let mut cache = crate::runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let objective = crate::EpiphanyMindObjectiveDocument { + objective: "Continue the keyed Mind migration".into(), + }; + cache.put(crate::MIND_OBJECTIVE_KEY, &objective)?; + + let request = request_current_reorientation(&store, "2026-08-18T10:00:01Z")?; + assert!( + request + .source_documents + .iter() + .all(|source| { source.document_type != crate::EpiphanyThreadStateEntry::TYPE }) + ); + assert_eq!( + crate::project_current_work(&store)? + .reorientation + .as_ref() + .map(|work| work.action), + Some(EpiphanyAgentPassContinuationAction::Launch) + ); + let job_id = launch_current_reorientation_work(&store, "2026-08-18T10:00:02Z")?; + cache.pull_all_backing_stores()?; + let launch = cache + .get::(&job_id)? + .expect("reorientation launch"); + let crate::EpiphanyWorkerLaunchDocument::Reorient(authority) = launch.launch_document()? + else { + panic!("expected reorientation launch"); + }; + assert_eq!(authority.request_id, request.request_id); + assert_eq!(authority.creation_thread_id, job_id); + + let basis = crate::worker_reasoning_basis(&store, &launch)?; + crate::put_reasoning_basis(&store, &basis)?; + assert_eq!(basis.source_documents, request.source_documents); + let mut native = EpiphanyModelRequest::new( + "reorientation-model-request", + "reorientation-conversation", + "openai-codex", + "gpt-test", + "reorient", + ); + native.reasoning_basis_id = Some(basis.basis_id.clone()); + native.source_worker_job_id = Some(job_id.clone()); + native.output_contract_id = Some(launch.output_contract_id.clone()); + native.output_schema_json = Some(serde_json::to_string( + &crate::epiphany_reorient_launch_output_schema(), + )?); + let provider = epiphany_openai_adapter::request_from_native(&native); + crate::open_runtime_model_execution( + &store, + crate::RuntimeSpineSessionOptions { + session_id: "reorientation-model-session".into(), + objective: "Reorient from exact Mind state".into(), + created_at: "2026-08-18T10:00:03Z".into(), + coordinator_note: "test".into(), + }, + crate::RuntimeSpineJobOptions { + job_id: "reorientation-model-job".into(), + session_id: "reorientation-model-session".into(), + role: "openai-model".into(), + created_at: "2026-08-18T10:00:03Z".into(), + summary: "Bound reorientation inference".into(), + artifact_refs: Vec::new(), + }, + &native, + &provider, + "2026-08-18T10:00:03Z", + )?; + let context = crate::EpiphanyDecisionContext::new(&basis, native, provider, Vec::new())?; + crate::put_decision_context(&store, &context)?; + let process = crate::ProcessInstanceIdentity { + process_id: 91, + creation_token: 17, + created_at_rfc3339: Some("2026-08-18T10:00:04Z".into()), + executable_path: "reorientation-worker".into(), + }; + let activation = "reorientation-activation"; + crate::claim_runtime_worker_process( + &store, + &job_id, + &process, + &format!("{:x}", Sha256::digest(activation.as_bytes())), + "2026-08-18T10:00:04Z", + )?; + crate::activate_runtime_worker_process( + &store, + &job_id, + &process, + activation, + "2026-08-18T10:00:05Z", + )?; + let result = crate::EpiphanyRuntimeReorientWorkerResult { + schema_version: crate::RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION.into(), + result_id: "reorientation-result".into(), + job_id: job_id.clone(), + mode: "resume".into(), + summary: "The sealed checkpoint remains coherent.".into(), + next_safe_move: "Continue the keyed migration.".into(), + checkpoint_still_valid: Some(true), + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: Vec::new(), + artifact_refs: Vec::new(), + open_questions: Vec::new(), + continuity_risks: Vec::new(), + item_error: None, + metadata: Default::default(), + decision_context_id: context.context_id.clone(), + }; + crate::put_runtime_reorient_worker_result(&store, &result)?; + crate::complete_runtime_job( + &store, + crate::RuntimeSpineJobResultOptions { + result_id: format!("runtime-result-{job_id}"), + job_id: job_id.clone(), + completed_at: "2026-08-18T10:00:06Z".into(), + verdict: "resume".into(), + summary: result.summary.clone(), + next_safe_move: result.next_safe_move.clone(), + evidence_refs: Vec::new(), + artifact_refs: Vec::new(), + decision_context_id: Some(context.context_id.clone()), + }, + )?; + + cache.pull_all_backing_stores()?; + cache.put( + "unrelated-observation", + &crate::EpiphanyMindObservationDocument { + value: epiphany_state_model::EpiphanyObservation { + id: "unrelated-observation".into(), + summary: "Persona observed an unrelated social fact.".into(), + ..Default::default() + }, + }, + )?; + let stale_store = temp.path().join("reorientation-stale.cc"); + std::fs::copy(&store, &stale_store)?; + + let receipt = accept_reorientation_result(&store, &job_id, "2026-08-18T10:00:07Z")?; + assert_eq!(receipt.invariant_owner, "Continuity.reorientation"); + assert_eq!( + accept_reorientation_result(&store, &job_id, "2026-08-18T10:00:07Z")?, + receipt + ); + assert!(crate::project_current_work(&store)?.reorientation.is_none()); + let mut accepted = crate::runtime_spine_cache(&store)?; + accepted.pull_all_backing_stores()?; + assert!( + accepted + .get::(&format!( + "reorientation-decision-{}", + request.request_id + ))? + .is_some() + ); + let next_request = request_current_reorientation(&store, "2026-08-18T10:00:08Z")?; + assert_ne!(next_request.request_id, request.request_id); + assert!(next_request.source_documents.iter().any(|source| { + source.document_type == EpiphanyMindReorientationDecisionDocument::TYPE + && source.document_key == format!("reorientation-decision-{}", request.request_id) + })); + let failed_job = launch_current_reorientation_work(&store, "2026-08-18T10:00:09Z")?; + let mut failed_cache = crate::runtime_spine_cache(&store)?; + failed_cache.pull_all_backing_stores()?; + let failed_launch = failed_cache + .get::(&failed_job)? + .expect("failed reorientation launch"); + let failed_basis = crate::worker_reasoning_basis(&store, &failed_launch)?; + crate::put_reasoning_basis(&store, &failed_basis)?; + let mut failed_native = EpiphanyModelRequest::new( + "failed-reorientation-model-request", + "failed-reorientation-conversation", + "openai-codex", + "gpt-test", + "reorient", + ); + failed_native.reasoning_basis_id = Some(failed_basis.basis_id.clone()); + failed_native.source_worker_job_id = Some(failed_job.clone()); + failed_native.output_contract_id = Some(failed_launch.output_contract_id.clone()); + failed_native.output_schema_json = Some(serde_json::to_string( + &crate::epiphany_reorient_launch_output_schema(), + )?); + let failed_provider = epiphany_openai_adapter::request_from_native(&failed_native); + crate::open_runtime_model_execution( + &store, + crate::RuntimeSpineSessionOptions { + session_id: "failed-reorientation-model-session".into(), + objective: "Fail from exact Mind state".into(), + created_at: "2026-08-18T10:00:10Z".into(), + coordinator_note: "test".into(), + }, + crate::RuntimeSpineJobOptions { + job_id: "failed-reorientation-model-job".into(), + session_id: "failed-reorientation-model-session".into(), + role: "openai-model".into(), + created_at: "2026-08-18T10:00:10Z".into(), + summary: "Bound failing inference".into(), + artifact_refs: Vec::new(), + }, + &failed_native, + &failed_provider, + "2026-08-18T10:00:10Z", + )?; + let failed_context = crate::EpiphanyDecisionContext::new( + &failed_basis, + failed_native, + failed_provider, + Vec::new(), + )?; + crate::put_decision_context(&store, &failed_context)?; + let failed_process = crate::ProcessInstanceIdentity { + process_id: 92, + creation_token: 18, + created_at_rfc3339: Some("2026-08-18T10:00:11Z".into()), + executable_path: "failed-reorientation-worker".into(), + }; + let failed_activation = "failed-reorientation-activation"; + crate::claim_runtime_worker_process( + &store, + &failed_job, + &failed_process, + &format!("{:x}", Sha256::digest(failed_activation.as_bytes())), + "2026-08-18T10:00:11Z", + )?; + crate::activate_runtime_worker_process( + &store, + &failed_job, + &failed_process, + failed_activation, + "2026-08-18T10:00:12Z", + )?; + crate::complete_runtime_job( + &store, + crate::RuntimeSpineJobResultOptions { + result_id: format!("runtime-result-{failed_job}"), + job_id: failed_job.clone(), + completed_at: "2026-08-18T10:00:13Z".into(), + verdict: "failed".into(), + summary: "Provider failed after the terminal request was sealed.".into(), + next_safe_move: "Review the typed failure, then retry.".into(), + evidence_refs: Vec::new(), + artifact_refs: Vec::new(), + decision_context_id: Some(failed_context.context_id.clone()), + }, + )?; + assert_eq!( + crate::project_current_work(&store)? + .reorientation + .as_ref() + .map(|work| work.action), + Some(EpiphanyAgentPassContinuationAction::Review) + ); + let failure_receipt = record_reorientation_pass_failure(&store, &failed_job)?; + assert_eq!( + record_reorientation_pass_failure(&store, &failed_job)?, + failure_receipt + ); + assert_eq!( + crate::project_current_work(&store)? + .reorientation + .as_ref() + .map(|work| work.action), + Some(EpiphanyAgentPassContinuationAction::Launch) + ); + assert!( + accepted + .get::(crate::THREAD_STATE_KEY)? + .is_none() + ); + + let mut stale = crate::runtime_spine_cache(&stale_store)?; + stale.pull_all_backing_stores()?; + stale.put( + crate::MIND_OBJECTIVE_KEY, + &crate::EpiphanyMindObjectiveDocument { + objective: "Changed strong objective".into(), + }, + )?; + let before = std::fs::read(&stale_store)?; + assert!( + accept_reorientation_result(&stale_store, &job_id, "2026-08-18T10:00:07Z").is_err() + ); + assert_eq!(std::fs::read(&stale_store)?, before); + Ok(()) + } +} diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 4d0e6931..81b7bbcb 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -1161,6 +1161,10 @@ 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::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; diff --git a/epiphany-core/src/surfaces/coordinator.rs b/epiphany-core/src/surfaces/coordinator.rs index 31838cd2..3a1e6b90 100644 --- a/epiphany-core/src/surfaces/coordinator.rs +++ b/epiphany-core/src/surfaces/coordinator.rs @@ -57,8 +57,7 @@ pub fn derive_coordinator_status( .iter() .map(coordinator_role_lane_from_role_board) .collect(); - let routing_recommendation = - routing_crrc_recommendation(&input.recommendation, input.crrc_regather_current); + let routing_recommendation = routing_crrc_recommendation(&input.recommendation); let decision = recommend_coordinator_action(EpiphanyCoordinatorInput { state_status: input.state_status, checkpoint_present: input.checkpoint_present, @@ -110,14 +109,7 @@ pub fn derive_coordinator_status( fn routing_crrc_recommendation( recommendation: &epiphany_core::EpiphanyCrrcRecommendation, - regather_current: bool, ) -> EpiphanyCoordinatorCrrcRecommendation { - if recommendation.action == EpiphanyCrrcAction::RegatherManually && !regather_current { - return EpiphanyCoordinatorCrrcRecommendation { - action: EpiphanyCrrcAction::Continue, - recommended_scene_action: None, - }; - } EpiphanyCoordinatorCrrcRecommendation { action: recommendation.action, recommended_scene_action: recommendation @@ -1057,26 +1049,6 @@ mod tests { use epiphany_state_model::EpiphanyAcceptanceReceipt; use epiphany_state_model::EpiphanyEvidenceRecord; - #[test] - fn relinquishment_demotes_older_manual_regather_to_observation_only() { - let recommendation = epiphany_core::EpiphanyCrrcRecommendation { - action: EpiphanyCrrcAction::RegatherManually, - recommended_scene_action: Some(EpiphanyCrrcSceneAction::ReorientResult), - reason: "historical failed reorientation".to_string(), - }; - - let stale = routing_crrc_recommendation(&recommendation, false); - assert_eq!(stale.action, EpiphanyCrrcAction::Continue); - assert_eq!(stale.recommended_scene_action, None); - - let current = routing_crrc_recommendation(&recommendation, true); - assert_eq!(current.action, EpiphanyCrrcAction::RegatherManually); - assert_eq!( - current.recommended_scene_action, - Some(EpiphanyCoordinatorSceneAction::ReorientResult) - ); - } - fn base_roles() -> Vec { vec![ role( diff --git a/epiphany-core/src/surfaces/coordinator_contract.rs b/epiphany-core/src/surfaces/coordinator_contract.rs index bd5374b8..ff1df20d 100644 --- a/epiphany-core/src/surfaces/coordinator_contract.rs +++ b/epiphany-core/src/surfaces/coordinator_contract.rs @@ -194,10 +194,6 @@ pub struct EpiphanyCoordinatorStatusInput { pub checkpoint_present: bool, pub pressure: EpiphanyPressure, pub recommendation: EpiphanyCrrcRecommendation, - /// True when CRRC's manual-regather pressure is causally newer than the - /// latest typed frontier relinquishment. Raw CRRC remains observable when - /// false, but it cannot route Self from stale continuity history. - pub crrc_regather_current: bool, pub roles: Vec, pub reorient_action: EpiphanyReorientAction, pub research_result_status: EpiphanyCoordinatorRoleResultStatus, diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index b9622fd8..1741dc28 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -61,14 +61,6 @@ pub use pressure::EpiphanyPressureStatus; pub use pressure::EpiphanyTokenUsageSnapshot; pub use pressure::derive_pressure_view; pub use reorient::EpiphanyReorientAction; -pub use reorient::EpiphanyReorientCheckpointStatus; -pub use reorient::EpiphanyReorientDecision; -pub use reorient::EpiphanyReorientFreshnessStatus; -pub use reorient::EpiphanyReorientInput; -pub use reorient::EpiphanyReorientPressureLevel; -pub use reorient::EpiphanyReorientReason; -pub use reorient::EpiphanyReorientStateStatus; -pub use reorient::recommend_reorientation; pub use role_board::EpiphanyRoleBoardCheckpointSummary; pub use role_board::EpiphanyRoleBoardInput; pub use role_board::EpiphanyRoleBoardJob; @@ -119,7 +111,6 @@ pub use view::epiphany_view_needs_pressure; pub use view::epiphany_view_needs_reorientation_inputs; pub use view::epiphany_view_needs_runtime_store; pub use worker_launch::AdmittedModelDirectionConsiderationContextProjection; -pub use worker_launch::EpiphanyReorientLaunchRequestInput; pub use worker_launch::EpiphanyReorientWorkerLaunchDocument; pub use worker_launch::EpiphanyRoleWorkerLaunchDocument; pub use worker_launch::EpiphanyWorkerLaunchDocument; @@ -141,4 +132,3 @@ pub use worker_launch::RepoFrontierPlanningContextProjection; pub use worker_launch::RepoFrontierProposalModelingContextProjection; pub use worker_launch::RepoFrontierResearchContextProjection; pub use worker_launch::RepoFrontierVerificationContextProjection; -pub use worker_launch::build_reorient_job_launch_request; diff --git a/epiphany-core/src/surfaces/reorient.rs b/epiphany-core/src/surfaces/reorient.rs index 1c5b83c4..adfc9033 100644 --- a/epiphany-core/src/surfaces/reorient.rs +++ b/epiphany-core/src/surfaces/reorient.rs @@ -1,566 +1,13 @@ -use epiphany_state_model::EpiphanyInvestigationCheckpoint; -use epiphany_state_model::EpiphanyInvestigationDisposition; -use serde::Deserialize; -use serde::Serialize; -use std::collections::HashSet; -use std::path::Path; -use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +/// Display projection of an accepted keyed continuity decision. +/// +/// This enum does not decide whether Reorientation should run. The keyed +/// current-work owner in `reorientation_work` owns launch, wait, review, and +/// admission behavior. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum EpiphanyReorientAction { Resume, Regather, } - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum EpiphanyReorientStateStatus { - Missing, - Ready, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum EpiphanyReorientCheckpointStatus { - Missing, - ResumeReady, - RegatherRequired, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum EpiphanyReorientReason { - MissingState, - MissingCheckpoint, - CheckpointReady, - CheckpointRequestedRegather, - CheckpointPathsDirty, - CheckpointPathsChanged, - FrontierChanged, - UnanchoredCheckpointWhileStateStale, - RetrievalFreshnessBlocked, - GraphFreshnessBlocked, - WatcherFreshnessBlocked, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum EpiphanyReorientPressureLevel { - Unknown, - Low, - Medium, - High, - Critical, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum EpiphanyReorientFreshnessStatus { - Unknown, - Clean, - Dirty, - Stale, - Changed, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EpiphanyReorientInput<'a> { - pub checkpoint: Option<&'a EpiphanyInvestigationCheckpoint>, - pub state_present: bool, - pub pressure_level: EpiphanyReorientPressureLevel, - pub retrieval_status: EpiphanyReorientFreshnessStatus, - pub retrieval_dirty_paths: Vec, - pub graph_status: EpiphanyReorientFreshnessStatus, - pub graph_dirty_paths: Vec, - pub watcher_status: EpiphanyReorientFreshnessStatus, - pub watcher_changed_paths: Vec, - pub watcher_graph_node_ids: Vec, - pub active_frontier_node_ids: Vec, - pub watched_root: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EpiphanyReorientDecision { - pub action: EpiphanyReorientAction, - pub checkpoint_status: EpiphanyReorientCheckpointStatus, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub checkpoint_id: Option, - pub pressure_level: EpiphanyReorientPressureLevel, - pub retrieval_status: EpiphanyReorientFreshnessStatus, - pub graph_status: EpiphanyReorientFreshnessStatus, - pub watcher_status: EpiphanyReorientFreshnessStatus, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub reasons: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub checkpoint_dirty_paths: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub checkpoint_changed_paths: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub active_frontier_node_ids: Vec, - pub next_action: String, - pub note: String, -} - -pub fn recommend_reorientation( - input: EpiphanyReorientInput<'_>, -) -> (EpiphanyReorientStateStatus, EpiphanyReorientDecision) { - let build_decision = |action: EpiphanyReorientAction, - checkpoint_status: EpiphanyReorientCheckpointStatus, - checkpoint_id: Option, - reasons: Vec, - checkpoint_dirty_paths: Vec, - checkpoint_changed_paths: Vec, - active_frontier_node_ids: Vec, - next_action: String, - note: String| EpiphanyReorientDecision { - action, - checkpoint_status, - checkpoint_id, - pressure_level: input.pressure_level, - retrieval_status: input.retrieval_status, - graph_status: input.graph_status, - watcher_status: input.watcher_status, - reasons, - checkpoint_dirty_paths, - checkpoint_changed_paths, - active_frontier_node_ids, - next_action, - note, - }; - - let high_pressure = matches!( - input.pressure_level, - EpiphanyReorientPressureLevel::High | EpiphanyReorientPressureLevel::Critical - ); - - if !input.state_present { - let note = if high_pressure { - "No Epiphany state survived, and the last recorded context pressure was high; re-gather before editing." - .to_string() - } else { - "No Epiphany state survived, so there is no authoritative checkpoint to resume." - .to_string() - }; - return ( - EpiphanyReorientStateStatus::Missing, - build_decision( - EpiphanyReorientAction::Regather, - EpiphanyReorientCheckpointStatus::Missing, - None, - vec![ - EpiphanyReorientReason::MissingState, - EpiphanyReorientReason::MissingCheckpoint, - ], - Vec::new(), - Vec::new(), - Vec::new(), - "Re-gather source context before editing.".to_string(), - note, - ), - ); - } - - let Some(checkpoint) = input.checkpoint else { - let note = if high_pressure { - "Epiphany state survived, but no durable investigation checkpoint was banked before high context pressure; re-gather before editing." - .to_string() - } else { - "Epiphany state survived, but there is no durable investigation checkpoint to resume from." - .to_string() - }; - return ( - EpiphanyReorientStateStatus::Ready, - build_decision( - EpiphanyReorientAction::Regather, - EpiphanyReorientCheckpointStatus::Missing, - None, - vec![EpiphanyReorientReason::MissingCheckpoint], - Vec::new(), - Vec::new(), - Vec::new(), - "Re-gather source context before editing.".to_string(), - note, - ), - ); - }; - - let checkpoint_status = match checkpoint.disposition { - EpiphanyInvestigationDisposition::ResumeReady => { - EpiphanyReorientCheckpointStatus::ResumeReady - } - EpiphanyInvestigationDisposition::RegatherRequired => { - EpiphanyReorientCheckpointStatus::RegatherRequired - } - }; - let workspace_root = input.watched_root.as_deref(); - let checkpoint_dirty_paths = overlapping_checkpoint_paths( - checkpoint, - input - .retrieval_dirty_paths - .iter() - .chain(input.graph_dirty_paths.iter()), - workspace_root, - ); - let checkpoint_changed_paths = overlapping_checkpoint_paths( - checkpoint, - input.watcher_changed_paths.iter(), - workspace_root, - ); - let frontier_changed = !input.active_frontier_node_ids.is_empty(); - let unanchored_checkpoint_while_state_stale = checkpoint.code_refs.is_empty() - && (!input.retrieval_dirty_paths.is_empty() - || !input.graph_dirty_paths.is_empty() - || !input.watcher_graph_node_ids.is_empty() - || frontier_changed); - - let mut reasons = Vec::new(); - if checkpoint.disposition == EpiphanyInvestigationDisposition::RegatherRequired { - reasons.push(EpiphanyReorientReason::CheckpointRequestedRegather); - } - if !checkpoint_dirty_paths.is_empty() { - reasons.push(EpiphanyReorientReason::CheckpointPathsDirty); - } - if !checkpoint_changed_paths.is_empty() { - reasons.push(EpiphanyReorientReason::CheckpointPathsChanged); - } - if frontier_changed { - reasons.push(EpiphanyReorientReason::FrontierChanged); - } - if unanchored_checkpoint_while_state_stale { - reasons.push(EpiphanyReorientReason::UnanchoredCheckpointWhileStateStale); - } - let retrieval_blocked = input.retrieval_status != EpiphanyReorientFreshnessStatus::Clean; - let graph_blocked = input.graph_status != EpiphanyReorientFreshnessStatus::Clean; - let watcher_blocked = matches!( - input.watcher_status, - EpiphanyReorientFreshnessStatus::Dirty - | EpiphanyReorientFreshnessStatus::Stale - | EpiphanyReorientFreshnessStatus::Changed - ); - if retrieval_blocked { - reasons.push(EpiphanyReorientReason::RetrievalFreshnessBlocked); - } - if graph_blocked { - reasons.push(EpiphanyReorientReason::GraphFreshnessBlocked); - } - if watcher_blocked { - reasons.push(EpiphanyReorientReason::WatcherFreshnessBlocked); - } - - let should_regather = checkpoint.disposition - == EpiphanyInvestigationDisposition::RegatherRequired - || !checkpoint_dirty_paths.is_empty() - || !checkpoint_changed_paths.is_empty() - || frontier_changed - || unanchored_checkpoint_while_state_stale - || retrieval_blocked - || graph_blocked - || watcher_blocked; - - if !should_regather { - reasons.push(EpiphanyReorientReason::CheckpointReady); - return ( - EpiphanyReorientStateStatus::Ready, - build_decision( - EpiphanyReorientAction::Resume, - checkpoint_status, - Some(checkpoint.checkpoint_id.clone()), - reasons, - Vec::new(), - Vec::new(), - Vec::new(), - checkpoint.next_action.clone().unwrap_or_else(|| { - "Resume from the durable checkpoint focus and verify the seam before broad edits." - .to_string() - }), - "Resume-ready checkpoint remains aligned with current freshness and watcher signals." - .to_string(), - ), - ); - } - - let mut note_fragments = Vec::new(); - if checkpoint.disposition == EpiphanyInvestigationDisposition::RegatherRequired { - note_fragments.push("the checkpoint explicitly requests re-gather".to_string()); - } - if !checkpoint_dirty_paths.is_empty() { - note_fragments.push(format!( - "{} checkpoint path(s) are already marked dirty", - checkpoint_dirty_paths.len() - )); - } - if !checkpoint_changed_paths.is_empty() { - note_fragments.push(format!( - "watcher observed {} changed checkpoint path(s)", - checkpoint_changed_paths.len() - )); - } - if frontier_changed { - note_fragments.push(format!( - "watcher hit {} active frontier node(s)", - input.active_frontier_node_ids.len() - )); - } - if unanchored_checkpoint_while_state_stale { - note_fragments.push( - "the checkpoint has no code refs while freshness signals already show drift" - .to_string(), - ); - } - if retrieval_blocked { - note_fragments.push(format!( - "retrieval freshness is {:?}, not clean", - input.retrieval_status - )); - } - if graph_blocked { - note_fragments.push(format!( - "graph freshness is {:?}, not clean", - input.graph_status - )); - } - if watcher_blocked { - note_fragments.push(format!( - "watcher freshness is {:?} and reports invalidation pressure", - input.watcher_status - )); - } - - ( - EpiphanyReorientStateStatus::Ready, - build_decision( - EpiphanyReorientAction::Regather, - checkpoint_status, - Some(checkpoint.checkpoint_id.clone()), - reasons, - checkpoint_dirty_paths, - checkpoint_changed_paths, - input.active_frontier_node_ids, - checkpoint - .next_action - .clone() - .unwrap_or_else(|| "Re-gather source context before editing.".to_string()), - format!("Re-gather before editing: {}.", note_fragments.join("; ")), - ), - ) -} - -fn overlapping_checkpoint_paths<'a>( - checkpoint: &EpiphanyInvestigationCheckpoint, - candidate_paths: impl IntoIterator, - workspace_root: Option<&Path>, -) -> Vec { - let checkpoint_path_keys: HashSet = checkpoint - .code_refs - .iter() - .map(|code_ref| code_ref_path_key(code_ref.path.as_path(), workspace_root)) - .collect(); - if checkpoint_path_keys.is_empty() { - return Vec::new(); - } - - let mut overlaps = Vec::new(); - let mut seen = HashSet::new(); - for path in candidate_paths { - let path_key = code_ref_path_key(path.as_path(), workspace_root); - if checkpoint_path_keys.contains(&path_key) && seen.insert(path_key) { - overlaps.push(path.clone()); - } - } - overlaps -} - -fn code_ref_path_key(path: &Path, workspace_root: Option<&Path>) -> String { - if let Some(workspace_root) = workspace_root - && let Ok(relative_path) = path.strip_prefix(workspace_root) - { - return epiphany_path_key(relative_path); - } - epiphany_path_key(path) -} - -fn epiphany_path_key(path: &Path) -> String { - path.to_string_lossy() - .replace('\\', "/") - .trim_start_matches("./") - .to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - use epiphany_state_model::EpiphanyCodeRef; - - fn checkpoint() -> EpiphanyInvestigationCheckpoint { - EpiphanyInvestigationCheckpoint { - checkpoint_id: "ix-1".to_string(), - kind: "implementation".to_string(), - disposition: EpiphanyInvestigationDisposition::ResumeReady, - focus: "runtime seam".to_string(), - summary: Some("resume here".to_string()), - next_action: Some("Continue from the seam.".to_string()), - captured_at_turn_id: Some("turn-1".to_string()), - open_questions: Vec::new(), - code_refs: vec![EpiphanyCodeRef { - path: PathBuf::from("src/lib.rs"), - start_line: None, - end_line: None, - symbol: None, - note: None, - }], - evidence_ids: vec!["ev-1".to_string()], - } - } - - fn input(checkpoint: Option<&EpiphanyInvestigationCheckpoint>) -> EpiphanyReorientInput<'_> { - EpiphanyReorientInput { - checkpoint, - state_present: true, - pressure_level: EpiphanyReorientPressureLevel::Low, - retrieval_status: EpiphanyReorientFreshnessStatus::Clean, - retrieval_dirty_paths: Vec::new(), - graph_status: EpiphanyReorientFreshnessStatus::Clean, - graph_dirty_paths: Vec::new(), - watcher_status: EpiphanyReorientFreshnessStatus::Clean, - watcher_changed_paths: Vec::new(), - watcher_graph_node_ids: Vec::new(), - active_frontier_node_ids: Vec::new(), - watched_root: None, - } - } - - #[test] - fn resumes_when_checkpoint_is_clean() { - let checkpoint = checkpoint(); - let (state_status, decision) = recommend_reorientation(input(Some(&checkpoint))); - - assert_eq!(state_status, EpiphanyReorientStateStatus::Ready); - assert_eq!(decision.action, EpiphanyReorientAction::Resume); - assert_eq!(decision.checkpoint_id.as_deref(), Some("ix-1")); - assert_eq!( - decision.reasons, - vec![EpiphanyReorientReason::CheckpointReady] - ); - } - - #[test] - fn regathers_when_checkpoint_path_changes() { - let checkpoint = checkpoint(); - let mut input = input(Some(&checkpoint)); - input.watcher_status = EpiphanyReorientFreshnessStatus::Changed; - input.watcher_changed_paths = vec![PathBuf::from("./src/lib.rs")]; - - let (_state_status, decision) = recommend_reorientation(input); - - assert_eq!(decision.action, EpiphanyReorientAction::Regather); - assert_eq!( - decision.reasons, - vec![ - EpiphanyReorientReason::CheckpointPathsChanged, - EpiphanyReorientReason::WatcherFreshnessBlocked, - ] - ); - assert_eq!( - decision.checkpoint_changed_paths, - vec![PathBuf::from("./src/lib.rs")] - ); - } - - #[test] - fn freshness_authorities_block_resume_but_unknown_watcher_does_not() { - let checkpoint = checkpoint(); - for (field, status, reason) in [ - ( - "retrieval", - EpiphanyReorientFreshnessStatus::Unknown, - EpiphanyReorientReason::RetrievalFreshnessBlocked, - ), - ( - "retrieval", - EpiphanyReorientFreshnessStatus::Dirty, - EpiphanyReorientReason::RetrievalFreshnessBlocked, - ), - ( - "retrieval", - EpiphanyReorientFreshnessStatus::Stale, - EpiphanyReorientReason::RetrievalFreshnessBlocked, - ), - ( - "retrieval", - EpiphanyReorientFreshnessStatus::Changed, - EpiphanyReorientReason::RetrievalFreshnessBlocked, - ), - ( - "graph", - EpiphanyReorientFreshnessStatus::Unknown, - EpiphanyReorientReason::GraphFreshnessBlocked, - ), - ( - "graph", - EpiphanyReorientFreshnessStatus::Dirty, - EpiphanyReorientReason::GraphFreshnessBlocked, - ), - ( - "graph", - EpiphanyReorientFreshnessStatus::Stale, - EpiphanyReorientReason::GraphFreshnessBlocked, - ), - ( - "graph", - EpiphanyReorientFreshnessStatus::Changed, - EpiphanyReorientReason::GraphFreshnessBlocked, - ), - ( - "watcher", - EpiphanyReorientFreshnessStatus::Dirty, - EpiphanyReorientReason::WatcherFreshnessBlocked, - ), - ( - "watcher", - EpiphanyReorientFreshnessStatus::Stale, - EpiphanyReorientReason::WatcherFreshnessBlocked, - ), - ( - "watcher", - EpiphanyReorientFreshnessStatus::Changed, - EpiphanyReorientReason::WatcherFreshnessBlocked, - ), - ] { - let mut candidate = input(Some(&checkpoint)); - match field { - "retrieval" => candidate.retrieval_status = status, - "graph" => candidate.graph_status = status, - "watcher" => candidate.watcher_status = status, - _ => unreachable!(), - } - let (_, decision) = recommend_reorientation(candidate); - assert_eq!(decision.action, EpiphanyReorientAction::Regather); - assert!(decision.reasons.contains(&reason)); - } - - let mut candidate = input(Some(&checkpoint)); - candidate.watcher_status = EpiphanyReorientFreshnessStatus::Unknown; - let (_, decision) = recommend_reorientation(candidate); - assert_eq!(decision.action, EpiphanyReorientAction::Resume); - } - - #[test] - fn missing_state_regathers() { - let mut input = input(None); - input.state_present = false; - input.pressure_level = EpiphanyReorientPressureLevel::High; - - let (state_status, decision) = recommend_reorientation(input); - - assert_eq!(state_status, EpiphanyReorientStateStatus::Missing); - assert_eq!(decision.action, EpiphanyReorientAction::Regather); - assert!( - decision - .reasons - .contains(&EpiphanyReorientReason::MissingState) - ); - assert!(decision.note.contains("high")); - } -} diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs index e7ebb394..3a033fbe 100644 --- a/epiphany-core/src/surfaces/worker_launch.rs +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -1,28 +1,16 @@ use epiphany_state_model::EpiphanyChurnState; -use epiphany_state_model::EpiphanyCodeRef; use epiphany_state_model::EpiphanyEvidenceRecord; use epiphany_state_model::EpiphanyGraphCheckpoint; use epiphany_state_model::EpiphanyGraphFrontier; use epiphany_state_model::EpiphanyGraphs; use epiphany_state_model::EpiphanyInvariant; use epiphany_state_model::EpiphanyInvestigationCheckpoint; -use epiphany_state_model::EpiphanyInvestigationDisposition; -use epiphany_state_model::EpiphanyJobKind; use epiphany_state_model::EpiphanyObservation; use epiphany_state_model::EpiphanyPlanningState; use epiphany_state_model::EpiphanyScratchPad; use epiphany_state_model::EpiphanySubgoal; -use epiphany_state_model::EpiphanyThreadState; use serde::Deserialize; use serde::Serialize; -use std::path::Path; - -use crate::EpiphanyJobLaunchRequest; -use crate::EpiphanyReorientAction; -use crate::EpiphanyReorientDecision; -use crate::EpiphanyReorientFreshnessStatus; -use crate::EpiphanyReorientPressureLevel; -use crate::EpiphanyReorientReason; pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v3"; pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0"; @@ -80,14 +68,14 @@ impl EpiphanyWorkerLaunchDocument { pub fn thread_id(&self) -> &str { match self { Self::Role(document) => &document.thread_id, - Self::Reorient(document) => &document.thread_id, + Self::Reorient(document) => &document.creation_thread_id, } } pub fn dynamic_prompt_context(&self) -> Option<&str> { match self { Self::Role(document) => document.dynamic_prompt_context.as_deref(), - Self::Reorient(document) => document.dynamic_prompt_context.as_deref(), + Self::Reorient(_) => None, } .map(str::trim) .filter(|context| !context.is_empty()) @@ -188,62 +176,9 @@ impl From<&crate::RepoFrontierResearchRequest> for RepoFrontierResearchContextPr #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EpiphanyReorientWorkerLaunchDocument { - pub thread_id: String, - pub mode: String, - pub checkpoint_id: String, - pub checkpoint_kind: String, - pub checkpoint_disposition: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dynamic_prompt_context: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub checkpoint_focus: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub checkpoint_summary: Option, - pub checkpoint_next_action: String, - #[serde(default)] - pub checkpoint_open_questions: Vec, - #[serde(default)] - pub checkpoint_evidence_ids: Vec, - #[serde(default)] - pub checkpoint_code_refs: Vec, - #[serde(default)] - pub decision_reasons: Vec, - pub decision_note: String, - pub pressure_level: String, - pub retrieval_status: String, - pub graph_status: String, - pub watcher_status: String, - #[serde(default)] - pub checkpoint_dirty_paths: Vec, - #[serde(default)] - pub checkpoint_changed_paths: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub scratch: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub graphs: Option, - #[serde(default)] - pub recent_evidence: Vec, - #[serde(default)] - pub recent_observations: Vec, - #[serde(default)] - pub active_frontier_node_ids: Vec, - #[serde(default)] - pub linked_subgoal_ids: Vec, - #[serde(default)] - pub linked_graph_node_ids: Vec, -} - -pub struct EpiphanyReorientLaunchRequestInput<'a> { - pub thread_id: &'a str, - pub expected_revision: Option, - pub max_runtime_seconds: Option, - pub binding_id: &'a str, - pub owner_role: &'a str, - pub instruction: String, - pub state: &'a EpiphanyThreadState, - pub checkpoint: &'a EpiphanyInvestigationCheckpoint, - pub decision: &'a EpiphanyReorientDecision, - pub dynamic_prompt_context: Option, + pub schema_version: String, + pub request_id: String, + pub creation_thread_id: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -402,301 +337,3 @@ impl RepoFrontierPlanMindContextProjection { } } } - -pub fn build_reorient_job_launch_request( - input: EpiphanyReorientLaunchRequestInput<'_>, -) -> EpiphanyJobLaunchRequest { - let linked_subgoal_ids = active_subgoal_ids(input.state); - let linked_graph_node_ids = unique_strings( - active_graph_node_ids(input.state) - .into_iter() - .chain(input.decision.active_frontier_node_ids.iter().cloned()), - ); - let launch_document = - EpiphanyWorkerLaunchDocument::Reorient(build_reorient_worker_launch_document( - input.thread_id, - input.state, - input.checkpoint, - input.decision, - linked_subgoal_ids.clone(), - linked_graph_node_ids.clone(), - input.dynamic_prompt_context, - )); - let output_contract_id = launch_document.output_contract_id().to_string(); - let organ_launch_contract = crate::default_launch_organ_contract( - reorient_launch_authority_scope(input.decision.action), - launch_document.document_kind(), - &output_contract_id, - ); - - EpiphanyJobLaunchRequest { - expected_revision: input.expected_revision, - binding_id: input.binding_id.to_string(), - kind: EpiphanyJobKind::Specialist, - scope: reorient_launch_scope(input.decision.action).to_string(), - owner_role: input.owner_role.to_string(), - authority_scope: reorient_launch_authority_scope(input.decision.action).to_string(), - linked_subgoal_ids, - linked_graph_node_ids, - instruction: input.instruction, - launch_document, - output_contract_id, - organ_launch_contract, - max_runtime_seconds: input.max_runtime_seconds, - proposal_modeling_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_verification_request_id: None, - } -} - -fn build_reorient_worker_launch_document( - thread_id: &str, - state: &EpiphanyThreadState, - checkpoint: &EpiphanyInvestigationCheckpoint, - decision: &EpiphanyReorientDecision, - linked_subgoal_ids: Vec, - linked_graph_node_ids: Vec, - dynamic_prompt_context: Option, -) -> EpiphanyReorientWorkerLaunchDocument { - EpiphanyReorientWorkerLaunchDocument { - thread_id: thread_id.to_string(), - mode: reorient_action_label(decision.action).to_string(), - checkpoint_id: checkpoint.checkpoint_id.clone(), - checkpoint_kind: checkpoint.kind.clone(), - checkpoint_disposition: investigation_disposition_label(checkpoint.disposition).to_string(), - dynamic_prompt_context, - checkpoint_focus: Some(checkpoint.focus.clone()), - checkpoint_summary: checkpoint.summary.clone(), - checkpoint_next_action: checkpoint - .next_action - .clone() - .unwrap_or_else(|| decision.next_action.clone()), - checkpoint_open_questions: checkpoint.open_questions.clone(), - checkpoint_evidence_ids: checkpoint.evidence_ids.clone(), - checkpoint_code_refs: checkpoint.code_refs.clone(), - decision_reasons: decision - .reasons - .iter() - .map(|reason| reorient_reason_label(*reason).to_string()) - .collect(), - decision_note: decision.note.clone(), - pressure_level: reorient_pressure_level_label(decision.pressure_level).to_string(), - retrieval_status: reorient_freshness_status_label(decision.retrieval_status).to_string(), - graph_status: reorient_freshness_status_label(decision.graph_status).to_string(), - watcher_status: reorient_freshness_status_label(decision.watcher_status).to_string(), - checkpoint_dirty_paths: decision - .checkpoint_dirty_paths - .iter() - .map(path_to_display_string) - .collect(), - checkpoint_changed_paths: decision - .checkpoint_changed_paths - .iter() - .map(path_to_display_string) - .collect(), - scratch: state.scratch.clone(), - graphs: Some(state.graphs.clone()), - recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(), - recent_observations: state.observations.iter().take(8).cloned().collect(), - active_frontier_node_ids: decision.active_frontier_node_ids.clone(), - linked_subgoal_ids, - linked_graph_node_ids, - } -} - -fn active_subgoal_ids(state: &EpiphanyThreadState) -> Vec { - state - .active_subgoal_id - .clone() - .map(|id| vec![id]) - .unwrap_or_default() -} - -fn active_graph_node_ids(state: &EpiphanyThreadState) -> Vec { - state - .graph_frontier - .as_ref() - .map(|frontier| frontier.active_node_ids.clone()) - .unwrap_or_default() -} - -fn unique_strings(values: impl IntoIterator) -> Vec { - let mut unique = Vec::new(); - for value in values { - if !unique.iter().any(|existing| existing == &value) { - unique.push(value); - } - } - unique -} - -fn path_to_display_string(path: impl AsRef) -> String { - path.as_ref().to_string_lossy().to_string() -} - -fn reorient_launch_authority_scope(action: EpiphanyReorientAction) -> &'static str { - match action { - EpiphanyReorientAction::Resume => "epiphany.reorient.resume", - EpiphanyReorientAction::Regather => "epiphany.reorient.regather", - } -} - -fn reorient_launch_scope(action: EpiphanyReorientAction) -> &'static str { - match action { - EpiphanyReorientAction::Resume => "reorient-guided checkpoint resume", - EpiphanyReorientAction::Regather => "reorient-guided checkpoint regather", - } -} - -fn reorient_action_label(action: EpiphanyReorientAction) -> &'static str { - match action { - EpiphanyReorientAction::Resume => "resume", - EpiphanyReorientAction::Regather => "regather", - } -} - -fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str { - match disposition { - EpiphanyInvestigationDisposition::ResumeReady => "resume_ready", - EpiphanyInvestigationDisposition::RegatherRequired => "regather_required", - } -} - -fn reorient_reason_label(reason: EpiphanyReorientReason) -> &'static str { - match reason { - EpiphanyReorientReason::MissingState => "missingState", - EpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint", - EpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather", - EpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty", - EpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged", - EpiphanyReorientReason::FrontierChanged => "frontierChanged", - EpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => { - "unanchoredCheckpointWhileStateStale" - } - EpiphanyReorientReason::RetrievalFreshnessBlocked => "retrievalFreshnessBlocked", - EpiphanyReorientReason::GraphFreshnessBlocked => "graphFreshnessBlocked", - EpiphanyReorientReason::WatcherFreshnessBlocked => "watcherFreshnessBlocked", - EpiphanyReorientReason::CheckpointReady => "checkpointReady", - } -} - -fn reorient_pressure_level_label(level: EpiphanyReorientPressureLevel) -> &'static str { - match level { - EpiphanyReorientPressureLevel::Unknown => "unknown", - EpiphanyReorientPressureLevel::Low => "low", - EpiphanyReorientPressureLevel::Medium => "medium", - EpiphanyReorientPressureLevel::High => "high", - EpiphanyReorientPressureLevel::Critical => "critical", - } -} - -fn reorient_freshness_status_label(status: EpiphanyReorientFreshnessStatus) -> &'static str { - match status { - EpiphanyReorientFreshnessStatus::Unknown => "unknown", - EpiphanyReorientFreshnessStatus::Clean => "clean", - EpiphanyReorientFreshnessStatus::Dirty => "dirty", - EpiphanyReorientFreshnessStatus::Stale => "stale", - EpiphanyReorientFreshnessStatus::Changed => "changed", - } -} - -#[cfg(test)] -mod tests { - use super::*; - use epiphany_state_model::EpiphanyCodeRef; - use epiphany_state_model::EpiphanyGraphFrontier; - use std::path::PathBuf; - - #[test] - fn reorient_launch_request_is_composed_from_typed_state() { - let state = EpiphanyThreadState { - revision: 7, - active_subgoal_id: Some("sg-1".to_string()), - graph_frontier: Some(EpiphanyGraphFrontier { - active_node_ids: vec!["node-a".to_string()], - active_edge_ids: Vec::new(), - open_question_ids: Vec::new(), - open_gap_ids: Vec::new(), - dirty_paths: Vec::new(), - }), - ..Default::default() - }; - let checkpoint = EpiphanyInvestigationCheckpoint { - checkpoint_id: "ix-1".to_string(), - kind: "implementation".to_string(), - disposition: EpiphanyInvestigationDisposition::RegatherRequired, - focus: "runtime seam".to_string(), - summary: Some("resume here".to_string()), - next_action: None, - captured_at_turn_id: Some("turn-1".to_string()), - open_questions: Vec::new(), - code_refs: vec![EpiphanyCodeRef { - path: PathBuf::from("src/lib.rs"), - start_line: None, - end_line: None, - symbol: None, - note: None, - }], - evidence_ids: vec!["ev-1".to_string()], - }; - let decision = EpiphanyReorientDecision { - action: EpiphanyReorientAction::Regather, - checkpoint_status: crate::EpiphanyReorientCheckpointStatus::RegatherRequired, - checkpoint_id: Some("ix-1".to_string()), - pressure_level: EpiphanyReorientPressureLevel::High, - retrieval_status: EpiphanyReorientFreshnessStatus::Stale, - graph_status: EpiphanyReorientFreshnessStatus::Changed, - watcher_status: EpiphanyReorientFreshnessStatus::Dirty, - reasons: vec![EpiphanyReorientReason::FrontierChanged], - checkpoint_dirty_paths: vec![PathBuf::from("src/lib.rs")], - checkpoint_changed_paths: vec![PathBuf::from("src/main.rs")], - active_frontier_node_ids: vec!["node-b".to_string()], - next_action: "Regather the seam.".to_string(), - note: "continuity moved".to_string(), - }; - - let request = build_reorient_job_launch_request(EpiphanyReorientLaunchRequestInput { - thread_id: "thread-1", - expected_revision: Some(state.revision), - max_runtime_seconds: Some(60), - binding_id: "reorient-worker", - owner_role: "epiphany-reorient", - instruction: "do the reorientation".to_string(), - state: &state, - checkpoint: &checkpoint, - decision: &decision, - dynamic_prompt_context: Some( - "\ncheckpoint weather\n" - .to_string(), - ), - }); - - assert_eq!(request.binding_id, "reorient-worker"); - assert_eq!(request.scope, "reorient-guided checkpoint regather"); - assert_eq!(request.authority_scope, "epiphany.reorient.regather"); - assert_eq!(request.linked_subgoal_ids, vec!["sg-1"]); - assert_eq!( - request.linked_graph_node_ids, - vec!["node-a".to_string(), "node-b".to_string()] - ); - let EpiphanyWorkerLaunchDocument::Reorient(document) = request.launch_document else { - panic!("expected reorient launch document"); - }; - assert_eq!(document.mode, "regather"); - assert_eq!(document.checkpoint_next_action, "Regather the seam."); - assert_eq!(document.decision_reasons, vec!["frontierChanged"]); - assert_eq!(document.pressure_level, "high"); - assert_eq!(document.retrieval_status, "stale"); - assert_eq!(document.graph_status, "changed"); - assert_eq!(document.watcher_status, "dirty"); - assert_eq!( - document.dynamic_prompt_context.as_deref(), - Some("\ncheckpoint weather\n") - ); - } -} diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index 2d2999cd..14929e36 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -630,7 +630,7 @@ pub fn build_worker_model_request( ( EpiphanyWorkerLaunchDocument::Reorient(document), epiphany_core::EpiphanyReasoningProjection::ReorientLaunch(projection), - ) if projection == document => {} + ) if projection.authority == *document => {} _ => { return Err(anyhow!( "worker model request reasoning projection does not match its launch authority" @@ -2571,6 +2571,8 @@ mod tests { observations: Vec::new(), evidence: Vec::new(), verification_audits: Vec::new(), + reorientation_decisions: Vec::new(), + reorientation_failures: Vec::new(), investigation_checkpoint: None, mode: None, planning: Default::default(),