Split native coordinator acceptance organ
This commit is contained in:
parent
61187f7a22
commit
588725f5e1
6 changed files with 388 additions and 395 deletions
367
epiphany-core/src/coordinator_acceptance.rs
Normal file
367
epiphany-core/src/coordinator_acceptance.rs
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
use crate::coordinator_results::latest_runtime_link;
|
||||
use crate::coordinator_service::changed_fields;
|
||||
use crate::*;
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EpiphanyCoordinatorAdmissionError {
|
||||
InvalidRequest(String),
|
||||
Store(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EpiphanyCoordinatorAdmissionError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidRequest(message) | Self::Store(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EpiphanyCoordinatorAdmissionError {}
|
||||
|
||||
/// The Epiphany-native owner of coordinator state.
|
||||
///
|
||||
/// Codex compatibility surfaces may translate requests into calls on this
|
||||
/// service, but they do not receive a persistence hook or retain an independent
|
||||
/// state opinion.
|
||||
pub struct EpiphanyRoleAcceptanceUpdate {
|
||||
pub state_update: EpiphanyStateUpdate,
|
||||
pub applied_patch: EpiphanyRoleStatePatchDocument,
|
||||
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
|
||||
pub accepted_receipt_id: String,
|
||||
pub accepted_observation_id: String,
|
||||
pub accepted_evidence_id: String,
|
||||
pub mind_review: MindGatewayReview,
|
||||
}
|
||||
|
||||
pub struct EpiphanyReorientAcceptanceUpdate {
|
||||
pub state_update: EpiphanyStateUpdate,
|
||||
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
|
||||
pub accepted_receipt_id: String,
|
||||
pub accepted_observation_id: String,
|
||||
pub accepted_evidence_id: String,
|
||||
pub mind_review: MindGatewayReview,
|
||||
}
|
||||
|
||||
pub fn completed_role_finding(
|
||||
runtime_store_path: Option<&Path>,
|
||||
state: &EpiphanyThreadState,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
binding_id: &str,
|
||||
) -> Result<EpiphanyRoleFindingInterpretation, EpiphanyCoordinatorAdmissionError> {
|
||||
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(
|
||||
"role findings without runtime-spine results are unsupported; accept only typed runtime-spine results".to_string(),
|
||||
))
|
||||
} else {
|
||||
Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"epiphany role binding {binding_id:?} was not found"
|
||||
)))
|
||||
};
|
||||
};
|
||||
let snapshot = read_runtime_role_result(runtime_store_path, &link.runtime_job_id, role_id);
|
||||
if snapshot.status != EpiphanyCoordinatorRoleResultStatus::Completed {
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept role result while worker status is {:?}",
|
||||
snapshot.status
|
||||
)));
|
||||
}
|
||||
let store = runtime_store_path.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(
|
||||
"cannot accept completed role worker without a loaded runtime-spine store".to_string(),
|
||||
)
|
||||
})?;
|
||||
load_launch_organ_contract(store, &link.runtime_job_id, "role")?;
|
||||
snapshot.finding.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(
|
||||
"cannot accept completed role worker because no typed runtime-spine result was recorded"
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn completed_reorient_finding(
|
||||
runtime_store_path: Option<&Path>,
|
||||
state: &EpiphanyThreadState,
|
||||
binding_id: &str,
|
||||
) -> Result<EpiphanyReorientFindingInterpretation, EpiphanyCoordinatorAdmissionError> {
|
||||
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,
|
||||
expected_document_kind: &str,
|
||||
) -> Result<EpiphanyLaunchOrganContract, EpiphanyCoordinatorAdmissionError> {
|
||||
let request = runtime_worker_launch_request(runtime_store_path, job_id).map_err(|error| {
|
||||
EpiphanyCoordinatorAdmissionError::Store(format!(
|
||||
"failed to read worker launch request for runtime job {job_id:?}: {error}"
|
||||
))
|
||||
})?;
|
||||
let request = request.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?} without its typed worker launch request"
|
||||
))
|
||||
})?;
|
||||
if request.document_kind != expected_document_kind {
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: launch document kind {:?} does not match expected {expected_document_kind:?}",
|
||||
request.document_kind
|
||||
)));
|
||||
}
|
||||
if request.organ_launch_contract.dependencies.is_empty()
|
||||
|| request
|
||||
.organ_launch_contract
|
||||
.receipt_proof_profiles
|
||||
.is_empty()
|
||||
{
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: worker launch request has no organ dependency/proof-profile contract"
|
||||
)));
|
||||
}
|
||||
if !request
|
||||
.organ_launch_contract
|
||||
.receipt_proof_profiles
|
||||
.iter()
|
||||
.any(|profile| {
|
||||
profile.effect_kind == EpiphanyReceiptEffectKind::StateAdmission
|
||||
&& profile
|
||||
.required_before_promotion_document_types
|
||||
.iter()
|
||||
.any(|document_type| document_type == MIND_GATEWAY_REVIEW_TYPE)
|
||||
})
|
||||
{
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: worker launch contract has no state-admission proof profile requiring Mind review"
|
||||
)));
|
||||
}
|
||||
Ok(request.organ_launch_contract)
|
||||
}
|
||||
|
||||
pub fn build_native_role_acceptance_update(
|
||||
expected_revision: Option<u64>,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
binding_id: &str,
|
||||
finding: &EpiphanyRoleFindingInterpretation,
|
||||
accepted_evidence_id: String,
|
||||
accepted_observation_id: String,
|
||||
accepted_at: String,
|
||||
) -> Result<EpiphanyRoleAcceptanceUpdate, String> {
|
||||
let mut patch = finding.state_patch.clone().unwrap_or_default();
|
||||
let errors = match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => imagination_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Modeling => modeling_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Research => research_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Verification => {
|
||||
patch = EpiphanyRoleStatePatchDocument::default();
|
||||
Vec::new()
|
||||
}
|
||||
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
|
||||
return Err(format!(
|
||||
"role {role_id:?} cannot be accepted through roleAccept"
|
||||
));
|
||||
}
|
||||
};
|
||||
if role_id != EpiphanyRoleResultRoleId::Verification && finding.state_patch.is_none() {
|
||||
return Err("completed role finding did not include a reviewable statePatch".to_string());
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(format!(
|
||||
"{} role state patch is not acceptable: {}",
|
||||
role_label_lower(role_id),
|
||||
errors.join("; ")
|
||||
));
|
||||
}
|
||||
|
||||
let projected_fields = changed_fields_from_patch(&patch)
|
||||
.iter()
|
||||
.map(|field| format!("{field:?}"))
|
||||
.collect();
|
||||
let mind_review = mind_review_role_acceptance(binding_id, role_id, finding, &patch);
|
||||
mind_review_allows_state(&mind_review)?;
|
||||
let bundle = build_role_acceptance_bundle(
|
||||
binding_id,
|
||||
EpiphanyRoleAcceptanceFinding {
|
||||
role_id,
|
||||
verdict: finding.verdict.clone(),
|
||||
summary: finding.summary.clone(),
|
||||
next_safe_move: finding.next_safe_move.clone(),
|
||||
files_inspected: finding.files_inspected.clone(),
|
||||
runtime_result_id: finding.runtime_result_id.clone(),
|
||||
runtime_job_id: finding.runtime_job_id.clone(),
|
||||
projected_fields,
|
||||
},
|
||||
accepted_evidence_id,
|
||||
accepted_observation_id,
|
||||
accepted_at,
|
||||
)?;
|
||||
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();
|
||||
patch.evidence.push(bundle.evidence);
|
||||
patch.observations.push(bundle.observation);
|
||||
patch.acceptance_receipts.push(bundle.receipt);
|
||||
let changed_fields = changed_fields_from_patch(&patch);
|
||||
Ok(EpiphanyRoleAcceptanceUpdate {
|
||||
state_update: state_update_from_patch(expected_revision, patch.clone()),
|
||||
applied_patch: patch,
|
||||
changed_fields,
|
||||
accepted_receipt_id,
|
||||
accepted_observation_id,
|
||||
accepted_evidence_id,
|
||||
mind_review,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_native_reorient_acceptance_update(
|
||||
expected_revision: Option<u64>,
|
||||
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<epiphany_state_model::EpiphanyInvestigationCheckpoint>,
|
||||
) -> Result<EpiphanyReorientAcceptanceUpdate, String> {
|
||||
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<u64>,
|
||||
patch: EpiphanyRoleStatePatchDocument,
|
||||
) -> EpiphanyStateUpdate {
|
||||
EpiphanyStateUpdate {
|
||||
expected_revision,
|
||||
objective: patch.objective,
|
||||
active_subgoal_id: patch.active_subgoal_id,
|
||||
subgoals: patch.subgoals,
|
||||
invariants: patch.invariants,
|
||||
graphs: patch.graphs,
|
||||
graph_frontier: patch.graph_frontier,
|
||||
graph_checkpoint: patch.graph_checkpoint,
|
||||
scratch: patch.scratch,
|
||||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence: patch.evidence,
|
||||
churn: patch.churn,
|
||||
mode: patch.mode,
|
||||
planning: patch.planning,
|
||||
}
|
||||
}
|
||||
|
||||
fn changed_fields_from_patch(
|
||||
patch: &EpiphanyRoleStatePatchDocument,
|
||||
) -> Vec<EpiphanyStateUpdatedField> {
|
||||
changed_fields(&state_update_from_patch(None, patch.clone()))
|
||||
}
|
||||
|
||||
fn role_label_lower(role_id: EpiphanyRoleResultRoleId) -> &'static str {
|
||||
match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => "imagination",
|
||||
EpiphanyRoleResultRoleId::Modeling => "modeling",
|
||||
EpiphanyRoleResultRoleId::Research => "research",
|
||||
EpiphanyRoleResultRoleId::Verification => "verification",
|
||||
EpiphanyRoleResultRoleId::Implementation => "implementation",
|
||||
EpiphanyRoleResultRoleId::Reorientation => "reorientation",
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,13 @@
|
|||
use crate::EpiphanyCoordinatorReorientResultSnapshot;
|
||||
use crate::EpiphanyCoordinatorRoleResultSnapshot;
|
||||
use crate::EpiphanyCoordinatorRoleResultStatus;
|
||||
use crate::EpiphanyCrrcResultStatus;
|
||||
use crate::EpiphanyLaunchOrganContract;
|
||||
use crate::EpiphanyReceiptEffectKind;
|
||||
use crate::EpiphanyReorientAcceptanceFinding;
|
||||
use crate::EpiphanyReorientFindingInterpretation;
|
||||
use crate::EpiphanyRoleAcceptanceFinding;
|
||||
use crate::EpiphanyRoleFindingInterpretation;
|
||||
use crate::EpiphanyRoleResultRoleId;
|
||||
use crate::EpiphanyRoleStatePatchDocument;
|
||||
use crate::EpiphanyStateUpdate;
|
||||
use crate::EpiphanyStateUpdatedField;
|
||||
use crate::MIND_GATEWAY_REVIEW_TYPE;
|
||||
use crate::MindGatewayReview;
|
||||
use crate::apply_epiphany_state_update;
|
||||
use crate::build_reorient_acceptance_bundle;
|
||||
use crate::build_role_acceptance_bundle;
|
||||
use crate::coordinator_results::latest_runtime_link;
|
||||
use crate::epiphany_state_update_validation_errors;
|
||||
use crate::imagination_role_state_patch_policy_errors;
|
||||
use crate::load_thread_state;
|
||||
use crate::mind_review_allows_state;
|
||||
use crate::mind_review_reorient_acceptance;
|
||||
use crate::mind_review_role_acceptance;
|
||||
use crate::modeling_role_state_patch_policy_errors;
|
||||
use crate::read_reorient_result_snapshot;
|
||||
use crate::read_role_result_snapshot;
|
||||
use crate::read_runtime_reorient_result;
|
||||
use crate::read_runtime_role_result;
|
||||
use crate::research_role_state_patch_policy_errors;
|
||||
use crate::runtime_worker_launch_request;
|
||||
use crate::write_thread_state;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
|
|
@ -38,27 +15,6 @@ use epiphany_state_model::EpiphanyThreadState;
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EpiphanyCoordinatorAdmissionError {
|
||||
InvalidRequest(String),
|
||||
Store(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EpiphanyCoordinatorAdmissionError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidRequest(message) | Self::Store(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EpiphanyCoordinatorAdmissionError {}
|
||||
|
||||
/// The Epiphany-native owner of coordinator state.
|
||||
///
|
||||
/// Codex compatibility surfaces may translate requests into calls on this
|
||||
/// service, but they do not receive a persistence hook or retain an independent
|
||||
/// state opinion.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EpiphanyCoordinatorService {
|
||||
thread_state_store: PathBuf,
|
||||
|
|
@ -72,25 +28,6 @@ pub struct EpiphanyCoordinatorStateApplied {
|
|||
pub state: EpiphanyThreadState,
|
||||
}
|
||||
|
||||
pub struct EpiphanyRoleAcceptanceUpdate {
|
||||
pub state_update: EpiphanyStateUpdate,
|
||||
pub applied_patch: EpiphanyRoleStatePatchDocument,
|
||||
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
|
||||
pub accepted_receipt_id: String,
|
||||
pub accepted_observation_id: String,
|
||||
pub accepted_evidence_id: String,
|
||||
pub mind_review: MindGatewayReview,
|
||||
}
|
||||
|
||||
pub struct EpiphanyReorientAcceptanceUpdate {
|
||||
pub state_update: EpiphanyStateUpdate,
|
||||
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
|
||||
pub accepted_receipt_id: String,
|
||||
pub accepted_observation_id: String,
|
||||
pub accepted_evidence_id: String,
|
||||
pub mind_review: MindGatewayReview,
|
||||
}
|
||||
|
||||
impl EpiphanyCoordinatorService {
|
||||
pub fn new(
|
||||
thread_state_store: impl Into<PathBuf>,
|
||||
|
|
@ -161,328 +98,6 @@ impl EpiphanyCoordinatorService {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn completed_role_finding(
|
||||
runtime_store_path: Option<&Path>,
|
||||
state: &EpiphanyThreadState,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
binding_id: &str,
|
||||
) -> Result<EpiphanyRoleFindingInterpretation, EpiphanyCoordinatorAdmissionError> {
|
||||
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(
|
||||
"role findings without runtime-spine results are unsupported; accept only typed runtime-spine results".to_string(),
|
||||
))
|
||||
} else {
|
||||
Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"epiphany role binding {binding_id:?} was not found"
|
||||
)))
|
||||
};
|
||||
};
|
||||
let snapshot = read_runtime_role_result(runtime_store_path, &link.runtime_job_id, role_id);
|
||||
if snapshot.status != EpiphanyCoordinatorRoleResultStatus::Completed {
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept role result while worker status is {:?}",
|
||||
snapshot.status
|
||||
)));
|
||||
}
|
||||
let store = runtime_store_path.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(
|
||||
"cannot accept completed role worker without a loaded runtime-spine store".to_string(),
|
||||
)
|
||||
})?;
|
||||
load_launch_organ_contract(store, &link.runtime_job_id, "role")?;
|
||||
snapshot.finding.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(
|
||||
"cannot accept completed role worker because no typed runtime-spine result was recorded"
|
||||
.to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn completed_reorient_finding(
|
||||
runtime_store_path: Option<&Path>,
|
||||
state: &EpiphanyThreadState,
|
||||
binding_id: &str,
|
||||
) -> Result<EpiphanyReorientFindingInterpretation, EpiphanyCoordinatorAdmissionError> {
|
||||
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,
|
||||
expected_document_kind: &str,
|
||||
) -> Result<EpiphanyLaunchOrganContract, EpiphanyCoordinatorAdmissionError> {
|
||||
let request = runtime_worker_launch_request(runtime_store_path, job_id).map_err(|error| {
|
||||
EpiphanyCoordinatorAdmissionError::Store(format!(
|
||||
"failed to read worker launch request for runtime job {job_id:?}: {error}"
|
||||
))
|
||||
})?;
|
||||
let request = request.ok_or_else(|| {
|
||||
EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?} without its typed worker launch request"
|
||||
))
|
||||
})?;
|
||||
if request.document_kind != expected_document_kind {
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: launch document kind {:?} does not match expected {expected_document_kind:?}",
|
||||
request.document_kind
|
||||
)));
|
||||
}
|
||||
if request.organ_launch_contract.dependencies.is_empty()
|
||||
|| request
|
||||
.organ_launch_contract
|
||||
.receipt_proof_profiles
|
||||
.is_empty()
|
||||
{
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: worker launch request has no organ dependency/proof-profile contract"
|
||||
)));
|
||||
}
|
||||
if !request
|
||||
.organ_launch_contract
|
||||
.receipt_proof_profiles
|
||||
.iter()
|
||||
.any(|profile| {
|
||||
profile.effect_kind == EpiphanyReceiptEffectKind::StateAdmission
|
||||
&& profile
|
||||
.required_before_promotion_document_types
|
||||
.iter()
|
||||
.any(|document_type| document_type == MIND_GATEWAY_REVIEW_TYPE)
|
||||
})
|
||||
{
|
||||
return Err(EpiphanyCoordinatorAdmissionError::InvalidRequest(format!(
|
||||
"cannot accept runtime job {job_id:?}: worker launch contract has no state-admission proof profile requiring Mind review"
|
||||
)));
|
||||
}
|
||||
Ok(request.organ_launch_contract)
|
||||
}
|
||||
|
||||
pub fn build_native_role_acceptance_update(
|
||||
expected_revision: Option<u64>,
|
||||
role_id: EpiphanyRoleResultRoleId,
|
||||
binding_id: &str,
|
||||
finding: &EpiphanyRoleFindingInterpretation,
|
||||
accepted_evidence_id: String,
|
||||
accepted_observation_id: String,
|
||||
accepted_at: String,
|
||||
) -> Result<EpiphanyRoleAcceptanceUpdate, String> {
|
||||
let mut patch = finding.state_patch.clone().unwrap_or_default();
|
||||
let errors = match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => imagination_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Modeling => modeling_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Research => research_role_state_patch_policy_errors(&patch),
|
||||
EpiphanyRoleResultRoleId::Verification => {
|
||||
patch = EpiphanyRoleStatePatchDocument::default();
|
||||
Vec::new()
|
||||
}
|
||||
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
|
||||
return Err(format!(
|
||||
"role {role_id:?} cannot be accepted through roleAccept"
|
||||
));
|
||||
}
|
||||
};
|
||||
if role_id != EpiphanyRoleResultRoleId::Verification && finding.state_patch.is_none() {
|
||||
return Err("completed role finding did not include a reviewable statePatch".to_string());
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(format!(
|
||||
"{} role state patch is not acceptable: {}",
|
||||
role_label_lower(role_id),
|
||||
errors.join("; ")
|
||||
));
|
||||
}
|
||||
|
||||
let projected_fields = changed_fields_from_patch(&patch)
|
||||
.iter()
|
||||
.map(|field| format!("{field:?}"))
|
||||
.collect();
|
||||
let mind_review = mind_review_role_acceptance(binding_id, role_id, finding, &patch);
|
||||
mind_review_allows_state(&mind_review)?;
|
||||
let bundle = build_role_acceptance_bundle(
|
||||
binding_id,
|
||||
EpiphanyRoleAcceptanceFinding {
|
||||
role_id,
|
||||
verdict: finding.verdict.clone(),
|
||||
summary: finding.summary.clone(),
|
||||
next_safe_move: finding.next_safe_move.clone(),
|
||||
files_inspected: finding.files_inspected.clone(),
|
||||
runtime_result_id: finding.runtime_result_id.clone(),
|
||||
runtime_job_id: finding.runtime_job_id.clone(),
|
||||
projected_fields,
|
||||
},
|
||||
accepted_evidence_id,
|
||||
accepted_observation_id,
|
||||
accepted_at,
|
||||
)?;
|
||||
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();
|
||||
patch.evidence.push(bundle.evidence);
|
||||
patch.observations.push(bundle.observation);
|
||||
patch.acceptance_receipts.push(bundle.receipt);
|
||||
let changed_fields = changed_fields_from_patch(&patch);
|
||||
Ok(EpiphanyRoleAcceptanceUpdate {
|
||||
state_update: state_update_from_patch(expected_revision, patch.clone()),
|
||||
applied_patch: patch,
|
||||
changed_fields,
|
||||
accepted_receipt_id,
|
||||
accepted_observation_id,
|
||||
accepted_evidence_id,
|
||||
mind_review,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_native_reorient_acceptance_update(
|
||||
expected_revision: Option<u64>,
|
||||
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<epiphany_state_model::EpiphanyInvestigationCheckpoint>,
|
||||
) -> Result<EpiphanyReorientAcceptanceUpdate, String> {
|
||||
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<u64>,
|
||||
patch: EpiphanyRoleStatePatchDocument,
|
||||
) -> EpiphanyStateUpdate {
|
||||
EpiphanyStateUpdate {
|
||||
expected_revision,
|
||||
objective: patch.objective,
|
||||
active_subgoal_id: patch.active_subgoal_id,
|
||||
subgoals: patch.subgoals,
|
||||
invariants: patch.invariants,
|
||||
graphs: patch.graphs,
|
||||
graph_frontier: patch.graph_frontier,
|
||||
graph_checkpoint: patch.graph_checkpoint,
|
||||
scratch: patch.scratch,
|
||||
investigation_checkpoint: patch.investigation_checkpoint,
|
||||
job_bindings: patch.job_bindings,
|
||||
acceptance_receipts: patch.acceptance_receipts,
|
||||
runtime_links: patch.runtime_links,
|
||||
observations: patch.observations,
|
||||
evidence: patch.evidence,
|
||||
churn: patch.churn,
|
||||
mode: patch.mode,
|
||||
planning: patch.planning,
|
||||
}
|
||||
}
|
||||
|
||||
fn changed_fields_from_patch(
|
||||
patch: &EpiphanyRoleStatePatchDocument,
|
||||
) -> Vec<EpiphanyStateUpdatedField> {
|
||||
changed_fields(&state_update_from_patch(None, patch.clone()))
|
||||
}
|
||||
|
||||
fn role_label_lower(role_id: EpiphanyRoleResultRoleId) -> &'static str {
|
||||
match role_id {
|
||||
EpiphanyRoleResultRoleId::Imagination => "imagination",
|
||||
EpiphanyRoleResultRoleId::Modeling => "modeling",
|
||||
EpiphanyRoleResultRoleId::Research => "research",
|
||||
EpiphanyRoleResultRoleId::Verification => "verification",
|
||||
EpiphanyRoleResultRoleId::Implementation => "implementation",
|
||||
EpiphanyRoleResultRoleId::Reorientation => "reorientation",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_coordinator_state_update_to_state(
|
||||
current_state: &EpiphanyThreadState,
|
||||
update: EpiphanyStateUpdate,
|
||||
|
|
@ -515,7 +130,7 @@ pub fn apply_coordinator_state_update_to_state(
|
|||
Ok(next_state)
|
||||
}
|
||||
|
||||
fn changed_fields(update: &EpiphanyStateUpdate) -> Vec<EpiphanyStateUpdatedField> {
|
||||
pub(crate) fn changed_fields(update: &EpiphanyStateUpdate) -> Vec<EpiphanyStateUpdatedField> {
|
||||
let mut fields = Vec::new();
|
||||
if update.objective.is_some() {
|
||||
fields.push(EpiphanyStateUpdatedField::Objective);
|
||||
|
|
@ -574,6 +189,8 @@ fn changed_fields(update: &EpiphanyStateUpdate) -> Vec<EpiphanyStateUpdatedField
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::EpiphanyCoordinatorAdmissionError;
|
||||
use crate::completed_role_finding;
|
||||
|
||||
#[test]
|
||||
fn native_service_owns_revision_gated_thread_state() -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ mod agent_launch;
|
|||
mod agent_memory;
|
||||
mod agent_utterance_state;
|
||||
mod continuity_gateway;
|
||||
mod coordinator_acceptance;
|
||||
mod coordinator_results;
|
||||
mod coordinator_service;
|
||||
mod cultmesh_integration;
|
||||
|
|
@ -120,23 +121,23 @@ pub use agent_utterance_state::AgentUtteranceValue;
|
|||
pub use agent_utterance_state::WEKSA_UTTERANCE_HANDOFF_SCHEMA_VERSION;
|
||||
pub use agent_utterance_state::derive_agent_utterance_state;
|
||||
pub use continuity_gateway::*;
|
||||
pub use coordinator_acceptance::EpiphanyCoordinatorAdmissionError;
|
||||
pub use coordinator_acceptance::EpiphanyReorientAcceptanceUpdate;
|
||||
pub use coordinator_acceptance::EpiphanyRoleAcceptanceUpdate;
|
||||
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::load_launch_organ_contract;
|
||||
pub use coordinator_results::EpiphanyCoordinatorReorientResultSnapshot;
|
||||
pub use coordinator_results::EpiphanyCoordinatorRoleResultSnapshot;
|
||||
pub use coordinator_results::read_reorient_result_snapshot;
|
||||
pub use coordinator_results::read_role_result_snapshot;
|
||||
pub use coordinator_results::read_runtime_reorient_result;
|
||||
pub use coordinator_results::read_runtime_role_result;
|
||||
pub use coordinator_service::EpiphanyCoordinatorAdmissionError;
|
||||
pub use coordinator_service::EpiphanyCoordinatorService;
|
||||
pub use coordinator_service::EpiphanyCoordinatorStateApplied;
|
||||
pub use coordinator_service::EpiphanyReorientAcceptanceUpdate;
|
||||
pub use coordinator_service::EpiphanyRoleAcceptanceUpdate;
|
||||
pub use coordinator_service::apply_coordinator_state_update_to_state;
|
||||
pub use coordinator_service::build_native_reorient_acceptance_update;
|
||||
pub use coordinator_service::build_native_role_acceptance_update;
|
||||
pub use coordinator_service::completed_reorient_finding;
|
||||
pub use coordinator_service::completed_role_finding;
|
||||
pub use coordinator_service::load_launch_organ_contract;
|
||||
pub use cultcache_rs::CacheBackingStore;
|
||||
pub use cultcache_rs::CultCache;
|
||||
pub use cultcache_rs::CultCacheEnvelope;
|
||||
|
|
|
|||
|
|
@ -692,6 +692,14 @@ service fell from 1,122 to 661 lines; the result organ is 461 lines and remains
|
|||
read-only. Sixteen coordinator-related core tests and all eight bridge tests
|
||||
pass. Split acceptance and state next before resuming authority extraction.
|
||||
|
||||
Acceptance is now split into
|
||||
`epiphany-core/src/coordinator_acceptance.rs`: admission errors,
|
||||
completed-finding checks, launch-contract proof validation, Mind review, and
|
||||
role/reorientation acceptance mutation construction. The remaining mixed
|
||||
service fell from 661 to 278 lines; acceptance is 367 lines and results remain
|
||||
461 lines. The same sixteen coordinator-related core tests and eight bridge
|
||||
tests pass. Extract state ownership next, leaving a policy-free facade.
|
||||
|
||||
Bridge identity correction from 2026-06-22: Epiphany owns swarm execution,
|
||||
repo Body work, branch-local lanes, memory, evidence, and Persona cognition.
|
||||
Bifrost owns GameCult identity, governed crossings into GitHub/Discord/Reddit
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -659,4 +659,4 @@ current_status:
|
|||
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
|
||||
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
|
||||
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. Gjallar readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
|
||||
next_action: Continue the native coordinator anatomy split defined in `notes/native-coordinator-liberation-map.md`. Result readback now lives in `coordinator_results.rs` and the mixed service fell from 1,122 to 661 lines. Extract acceptance and state into narrow organs, leave a policy-free facade, then resume receipt persistence/proof enforcement extraction and remove `thread/epiphany/*` JSON-RPC from `epiphany-mvp-coordinator`.
|
||||
next_action: Finish the native coordinator anatomy split defined in `notes/native-coordinator-liberation-map.md`. Results and acceptance now have separate organs; `coordinator_service.rs` fell from 1,122 to 278 lines. Extract state ownership and leave a policy-free facade, then resume receipt persistence/proof enforcement extraction and remove `thread/epiphany/*` JSON-RPC from `epiphany-mvp-coordinator`.
|
||||
|
|
|
|||
Reference in a new issue