Bind Modeling claim repair launches atomically
This commit is contained in:
parent
7f4e1dc410
commit
2195347c8c
16 changed files with 1063 additions and 41 deletions
|
|
@ -569,6 +569,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context(
|
|||
objective: state.objective.clone(),
|
||||
dynamic_prompt_context,
|
||||
proposal_modeling_context: None,
|
||||
claim_repair_context: None,
|
||||
active_subgoal_id: state.active_subgoal_id.clone(),
|
||||
active_subgoals: state
|
||||
.subgoals
|
||||
|
|
@ -610,6 +611,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context(
|
|||
organ_launch_contract,
|
||||
max_runtime_seconds,
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -713,6 +715,7 @@ pub fn build_epiphany_job_launch_request(
|
|||
organ_launch_contract,
|
||||
max_runtime_seconds,
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -637,7 +637,11 @@ fn commit_state_with_mind_witness(
|
|||
});
|
||||
}
|
||||
coordinator_state_transaction::commit_coordinator_state_transaction(
|
||||
&mut cache, thread_id, state, batch,
|
||||
&mut cache,
|
||||
thread_id,
|
||||
state,
|
||||
batch,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ pub fn commit_coordinator_job_launch(
|
|||
}
|
||||
|
||||
fn commit_coordinator_job_launch_in_cache(
|
||||
cache: &mut CultCache,
|
||||
cache: &mut coordinator_state_transaction::CoordinatorStateTransaction,
|
||||
thread_id: &str,
|
||||
current_state: &EpiphanyThreadState,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
|
|
@ -44,6 +44,11 @@ fn commit_coordinator_job_launch_in_cache(
|
|||
created_at: String,
|
||||
injected_envelopes: Vec<CultCacheEnvelope>,
|
||||
) -> Result<EpiphanyJobLaunchResult> {
|
||||
if thread_id != request.launch_document.thread_id() {
|
||||
return Err(anyhow!(
|
||||
"coordinator commit thread id must match typed launch document"
|
||||
));
|
||||
}
|
||||
let runtime_identity = cache
|
||||
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
|
||||
.ok_or_else(|| anyhow!("coordinator launch requires runtime identity"))?;
|
||||
|
|
@ -79,6 +84,33 @@ fn commit_coordinator_job_launch_in_cache(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let claim_repair_launch = if let Some(request_id) = request.claim_repair_request_id.as_deref() {
|
||||
let (repair, challenge, identity) =
|
||||
validate_claim_repair_launch(cache, current_state, request, request_id)?;
|
||||
let projection = build_claim_repair_context_projection(&repair);
|
||||
match &mut effective_launch_document {
|
||||
EpiphanyWorkerLaunchDocument::Role(document) => {
|
||||
if document.proposal_modeling_context.is_some() {
|
||||
return Err(anyhow!(
|
||||
"claim repair and proposal contexts are mutually exclusive"
|
||||
));
|
||||
}
|
||||
document.claim_repair_context = Some(projection);
|
||||
}
|
||||
EpiphanyWorkerLaunchDocument::Reorient(_) => {
|
||||
return Err(anyhow!("reorient launch cannot carry claim repair context"));
|
||||
}
|
||||
}
|
||||
let bytes = rmp_serde::to_vec_named(&effective_launch_document)?;
|
||||
Some((
|
||||
repair,
|
||||
challenge,
|
||||
identity,
|
||||
format!("{:x}", Sha256::digest(bytes)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prepared = prepare_runtime_spine_heartbeat_job(
|
||||
&cache,
|
||||
RuntimeSpineHeartbeatJobOptions {
|
||||
|
|
@ -102,10 +134,18 @@ fn commit_coordinator_job_launch_in_cache(
|
|||
output_contract_id: request.output_contract_id.clone(),
|
||||
organ_launch_contract: request.organ_launch_contract.clone(),
|
||||
proposal_modeling_request_id: request.proposal_modeling_request_id.clone(),
|
||||
claim_repair_request_id: request.claim_repair_request_id.clone(),
|
||||
created_at: created_at.clone(),
|
||||
},
|
||||
)?;
|
||||
let mut batch = prepared.envelopes;
|
||||
let runtime_identity_position = batch
|
||||
.iter()
|
||||
.position(|envelope| {
|
||||
envelope.r#type == EpiphanyRuntimeIdentity::TYPE && envelope.key == RUNTIME_IDENTITY_KEY
|
||||
})
|
||||
.ok_or_else(|| anyhow!("prepared launch omitted its runtime identity replacement"))?;
|
||||
let runtime_identity_replacement = batch.remove(runtime_identity_position);
|
||||
if let Some((selection, proposal, identity, worker_launch_document_sha256)) = proposal_launch {
|
||||
let launch_binding = RepoFrontierProposalModelingLaunchBinding {
|
||||
schema_version: REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION.into(),
|
||||
|
|
@ -138,6 +178,36 @@ fn commit_coordinator_job_launch_in_cache(
|
|||
.0,
|
||||
);
|
||||
}
|
||||
if let Some((repair, challenge, identity, worker_launch_document_sha256)) = claim_repair_launch
|
||||
{
|
||||
let launch_binding = RepoModelClaimRepairLaunchBinding {
|
||||
schema_version: REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_SCHEMA_VERSION.into(),
|
||||
binding_record_id: format!("repo-model-claim-repair-launch-{}", repair.request_id),
|
||||
repair_request_id: repair.request_id,
|
||||
challenge_id: challenge.challenge_id,
|
||||
challenge_sha256: repair.challenge_sha256,
|
||||
job_id: plan.backend_job_id.clone(),
|
||||
binding_id: request.binding_id.clone(),
|
||||
runtime_id: identity.runtime_id,
|
||||
thread_id: repair.thread_id,
|
||||
launched_at: created_at.clone(),
|
||||
worker_launch_document_sha256,
|
||||
contract: REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_CONTRACT.into(),
|
||||
};
|
||||
if cache
|
||||
.get::<RepoModelClaimRepairLaunchBinding>(&launch_binding.binding_record_id)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"claim repair launch binding already exists for backend job"
|
||||
));
|
||||
}
|
||||
batch.push(
|
||||
cache
|
||||
.prepare_entry(&launch_binding.binding_record_id, &launch_binding)?
|
||||
.0,
|
||||
);
|
||||
}
|
||||
if request.binding_id == EPIPHANY_RESEARCH_ROLE_BINDING_ID {
|
||||
let grant = substrate_gate_repo_access_grant_for_launch(
|
||||
format!("substrate-grant-{}", plan.backend_job_id),
|
||||
|
|
@ -153,6 +223,7 @@ fn commit_coordinator_job_launch_in_cache(
|
|||
thread_id,
|
||||
&next_state,
|
||||
batch,
|
||||
vec![runtime_identity_replacement],
|
||||
)?;
|
||||
Ok(EpiphanyJobLaunchResult {
|
||||
epiphany_state: next_state,
|
||||
|
|
@ -169,15 +240,28 @@ pub fn plan_coordinator_job_launch(
|
|||
launcher_job_id: String,
|
||||
backend_job_id: String,
|
||||
) -> Result<EpiphanyCoordinatorJobLaunchPlan> {
|
||||
let caller_projection = match &request.launch_document {
|
||||
EpiphanyWorkerLaunchDocument::Role(document) => document.proposal_modeling_context.as_ref(),
|
||||
EpiphanyWorkerLaunchDocument::Reorient(_) => None,
|
||||
let (caller_proposal_projection, caller_repair_projection) = match &request.launch_document {
|
||||
EpiphanyWorkerLaunchDocument::Role(document) => (
|
||||
document.proposal_modeling_context.as_ref(),
|
||||
document.claim_repair_context.as_ref(),
|
||||
),
|
||||
EpiphanyWorkerLaunchDocument::Reorient(_) => (None, None),
|
||||
};
|
||||
if caller_projection.is_some() {
|
||||
if caller_proposal_projection.is_some() {
|
||||
return Err(anyhow!(
|
||||
"caller-prepopulated proposal Modeling context is forbidden; coordinator commit owns projection"
|
||||
));
|
||||
}
|
||||
if caller_repair_projection.is_some() {
|
||||
return Err(anyhow!(
|
||||
"caller-prepopulated claim repair context is forbidden; coordinator commit owns projection"
|
||||
));
|
||||
}
|
||||
if request.proposal_modeling_request_id.is_some() && request.claim_repair_request_id.is_some() {
|
||||
return Err(anyhow!(
|
||||
"proposal Modeling and claim repair launches are mutually exclusive"
|
||||
));
|
||||
}
|
||||
if let Some(expected) = request.expected_revision
|
||||
&& expected != state.revision
|
||||
{
|
||||
|
|
@ -190,6 +274,10 @@ pub fn plan_coordinator_job_launch(
|
|||
let mut cache = runtime_spine_cache(runtime_store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
validate_proposal_modeling_launch(&cache, state, request, request_id)?;
|
||||
} else if let Some(request_id) = request.claim_repair_request_id.as_deref() {
|
||||
let mut cache = runtime_spine_cache(runtime_store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
validate_claim_repair_launch(&cache, state, request, request_id)?;
|
||||
} else if request.owner_role == EPIPHANY_MODELING_OWNER_ROLE {
|
||||
// Ordinary Modeling launches remain valid, but carry no proposal authority.
|
||||
}
|
||||
|
|
@ -247,6 +335,81 @@ pub fn plan_coordinator_job_launch(
|
|||
})
|
||||
}
|
||||
|
||||
fn build_claim_repair_context_projection(
|
||||
request: &RepoModelClaimRepairRequest,
|
||||
) -> RepoModelClaimRepairContextProjection {
|
||||
RepoModelClaimRepairContextProjection {
|
||||
schema_version: REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION.into(),
|
||||
contract: REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT.into(),
|
||||
request_id: request.request_id.clone(),
|
||||
challenge_id: request.challenge_id.clone(),
|
||||
challenge_sha256: request.challenge_sha256.clone(),
|
||||
eyes_evidence_packet_id: request.eyes_evidence_packet_id.clone(),
|
||||
eyes_evidence_packet_sha256: request.eyes_evidence_packet_sha256.clone(),
|
||||
source_result_id: request.source_result_id.clone(),
|
||||
source_job_id: request.source_job_id.clone(),
|
||||
original_admission_receipt_id: request.original_admission_receipt_id.clone(),
|
||||
current_admission_receipt_id: request.current_admission_receipt_id.clone(),
|
||||
model_revision: request.model_revision,
|
||||
model_hash: request.model_hash.clone(),
|
||||
target_claim_id: request.target_claim_id.clone(),
|
||||
target_claim_sha256: request.target_claim_sha256.clone(),
|
||||
runtime_id: request.runtime_id.clone(),
|
||||
thread_id: request.thread_id.clone(),
|
||||
affected_frontier: request.affected_frontier.clone(),
|
||||
requested_at: request.requested_at.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_claim_repair_launch(
|
||||
cache: &CultCache,
|
||||
state: &EpiphanyThreadState,
|
||||
launch: &EpiphanyJobLaunchRequest,
|
||||
request_id: &str,
|
||||
) -> Result<(
|
||||
RepoModelClaimRepairRequest,
|
||||
RepoModelClaimChallenge,
|
||||
EpiphanyRuntimeIdentity,
|
||||
)> {
|
||||
if launch.owner_role != EPIPHANY_MODELING_OWNER_ROLE
|
||||
|| launch.binding_id != EPIPHANY_MODELING_ROLE_BINDING_ID
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"claim repair may only be carried by the Modeling role launch"
|
||||
));
|
||||
}
|
||||
let repair = cache
|
||||
.get::<RepoModelClaimRepairRequest>(request_id)?
|
||||
.ok_or_else(|| anyhow!("claim repair request {request_id:?} does not exist"))?;
|
||||
let challenge = cache
|
||||
.get::<RepoModelClaimChallenge>(&repair.challenge_id)?
|
||||
.ok_or_else(|| anyhow!("claim repair launch references a missing challenge"))?;
|
||||
let identity = cache
|
||||
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
|
||||
.ok_or_else(|| anyhow!("claim repair launch requires runtime identity"))?;
|
||||
let persisted_state = cache
|
||||
.get::<crate::EpiphanyThreadStateEntry>(crate::THREAD_STATE_KEY)?
|
||||
.ok_or_else(|| anyhow!("claim repair launch requires authoritative thread state"))?;
|
||||
let persisted_state_value = persisted_state.state()?;
|
||||
crate::runtime_spine::validate_current_repo_model_claim_repair_request(cache, &repair)?;
|
||||
if repair.request_id != request_id
|
||||
|| repair.runtime_id != identity.runtime_id
|
||||
|| repair.thread_id != persisted_state.thread_id
|
||||
|| persisted_state_value != *state
|
||||
|| launch.launch_document.thread_id() != repair.thread_id
|
||||
{
|
||||
return Err(anyhow!("claim repair launch provenance binding mismatch"));
|
||||
}
|
||||
if cache
|
||||
.get_all::<RepoModelClaimRepairLaunchBinding>()?
|
||||
.iter()
|
||||
.any(|binding| binding.repair_request_id == request_id)
|
||||
{
|
||||
return Err(anyhow!("claim repair request is already bound to a launch"));
|
||||
}
|
||||
Ok((repair, challenge, identity))
|
||||
}
|
||||
|
||||
fn build_proposal_modeling_context_projection(
|
||||
request: &RepoFrontierProposalModelingRequest,
|
||||
proposal: &RepoFrontierWorkProposal,
|
||||
|
|
@ -484,6 +647,51 @@ mod tests {
|
|||
Ok((store, state, request, selection))
|
||||
}
|
||||
|
||||
fn claim_repair_launch_fixture(
|
||||
root: &Path,
|
||||
suffix: &str,
|
||||
) -> Result<(
|
||||
std::path::PathBuf,
|
||||
EpiphanyThreadState,
|
||||
EpiphanyJobLaunchRequest,
|
||||
RepoModelClaimRepairRequest,
|
||||
)> {
|
||||
let (store, challenge) =
|
||||
crate::runtime_spine::tests::claim_challenge_fixture(root, suffix, "Hands")?;
|
||||
crate::commit_repo_model_claim_challenge(&store, &challenge)?;
|
||||
let repair = crate::commit_repo_model_claim_repair_request(
|
||||
&store,
|
||||
&challenge.challenge_id,
|
||||
"2026-07-14T09:00:02Z",
|
||||
)?;
|
||||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let mut state = cache
|
||||
.get_required::<EpiphanyThreadStateEntry>(THREAD_STATE_KEY)?
|
||||
.state()?;
|
||||
for link in &mut state.runtime_links {
|
||||
if link.binding_id == EPIPHANY_MODELING_ROLE_BINDING_ID
|
||||
&& link.runtime_result_id.is_none()
|
||||
{
|
||||
link.runtime_result_id = Some(format!("prior-modeling-result-{suffix}"));
|
||||
}
|
||||
}
|
||||
cache.put(
|
||||
THREAD_STATE_KEY,
|
||||
&EpiphanyThreadStateEntry::from_state(&repair.thread_id, &state)?,
|
||||
)?;
|
||||
let mut launch = build_epiphany_role_launch_request(
|
||||
&repair.thread_id,
|
||||
EpiphanyRoleResultRoleId::Modeling,
|
||||
Some(state.revision),
|
||||
Some(60),
|
||||
&state,
|
||||
)
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
launch.claim_repair_request_id = Some(repair.request_id.clone());
|
||||
Ok((store, state, launch, repair))
|
||||
}
|
||||
|
||||
fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest {
|
||||
build_epiphany_role_launch_request(
|
||||
"thread-1",
|
||||
|
|
@ -594,6 +802,8 @@ mod tests {
|
|||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
let initial = EpiphanyThreadStateEntry::from_state("thread-1", &state)?;
|
||||
cache.put(THREAD_STATE_KEY, &initial)?;
|
||||
let mut transaction =
|
||||
coordinator_state_transaction::open_coordinator_state_transaction(&store, &state)?;
|
||||
let request = research_launch(&state);
|
||||
let plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
|
|
@ -611,7 +821,7 @@ mod tests {
|
|||
};
|
||||
assert!(
|
||||
commit_coordinator_job_launch_in_cache(
|
||||
&mut cache,
|
||||
&mut transaction,
|
||||
"thread-1",
|
||||
&state,
|
||||
&request,
|
||||
|
|
@ -736,4 +946,245 @@ mod tests {
|
|||
assert_eq!(std::fs::read(&store)?, before);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_repair_launch_is_coordinator_owned_bound_and_single_use() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let (store, state, launch, repair) = claim_repair_launch_fixture(root.path(), "exact")?;
|
||||
for mutation in 0..4 {
|
||||
let mut forged = launch.clone();
|
||||
match mutation {
|
||||
0 => forged.owner_role = "Eyes".into(),
|
||||
1 => forged.binding_id = "wrong-binding".into(),
|
||||
2 => {
|
||||
if let EpiphanyWorkerLaunchDocument::Role(document) =
|
||||
&mut forged.launch_document
|
||||
{
|
||||
document.claim_repair_context =
|
||||
Some(build_claim_repair_context_projection(&repair));
|
||||
}
|
||||
}
|
||||
_ => forged.proposal_modeling_request_id = Some("dual-proposal".into()),
|
||||
}
|
||||
let before = std::fs::read(&store)?;
|
||||
assert!(
|
||||
plan_coordinator_job_launch(
|
||||
&state,
|
||||
&forged,
|
||||
&store,
|
||||
format!("lf-{mutation}"),
|
||||
format!("bf-{mutation}")
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(std::fs::read(&store)?, before);
|
||||
}
|
||||
let plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
&launch,
|
||||
&store,
|
||||
"launcher-repair".into(),
|
||||
"backend-repair".into(),
|
||||
)?;
|
||||
let committed = commit_coordinator_job_launch(
|
||||
&store,
|
||||
&repair.thread_id,
|
||||
&state,
|
||||
&launch,
|
||||
&plan,
|
||||
"2026-07-14T09:00:03Z".into(),
|
||||
)?;
|
||||
let persisted =
|
||||
runtime_worker_launch_request(&store, "backend-repair")?.expect("repair launch");
|
||||
let projection = match persisted.launch_document()? {
|
||||
EpiphanyWorkerLaunchDocument::Role(document) => {
|
||||
document.claim_repair_context.expect("typed repair context")
|
||||
}
|
||||
EpiphanyWorkerLaunchDocument::Reorient(_) => panic!("repair cannot reorient"),
|
||||
};
|
||||
assert_eq!(projection.request_id, repair.request_id);
|
||||
assert!(projection.affected_frontier == repair.affected_frontier);
|
||||
let cache = coordinator_acceptance_cache(&store)?;
|
||||
let binding = cache
|
||||
.get::<RepoModelClaimRepairLaunchBinding>(&format!(
|
||||
"repo-model-claim-repair-launch-{}",
|
||||
repair.request_id
|
||||
))?
|
||||
.expect("repair binding");
|
||||
assert_eq!(binding.challenge_id, repair.challenge_id);
|
||||
assert_eq!(binding.runtime_id, repair.runtime_id);
|
||||
assert_eq!(binding.thread_id, repair.thread_id);
|
||||
assert_eq!(
|
||||
binding.worker_launch_document_sha256,
|
||||
format!("{:x}", Sha256::digest(&persisted.launch_document_msgpack))
|
||||
);
|
||||
let mut second = build_epiphany_role_launch_request(
|
||||
&repair.thread_id,
|
||||
EpiphanyRoleResultRoleId::Modeling,
|
||||
Some(committed.epiphany_state.revision),
|
||||
Some(60),
|
||||
&committed.epiphany_state,
|
||||
)
|
||||
.map_err(|error| anyhow!(error))?;
|
||||
second.claim_repair_request_id = Some(repair.request_id);
|
||||
let before = std::fs::read(&store)?;
|
||||
assert!(
|
||||
plan_coordinator_job_launch(
|
||||
&committed.epiphany_state,
|
||||
&second,
|
||||
&store,
|
||||
"launcher-second".into(),
|
||||
"backend-second".into()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(std::fs::read(&store)?, before);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_repair_launch_refuses_every_swapped_causal_field_without_new_writes() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
for mutation in 0..8 {
|
||||
let (store, state, launch, repair) =
|
||||
claim_repair_launch_fixture(root.path(), &format!("causal-{mutation}"))?;
|
||||
let mut corrupt = repair.clone();
|
||||
match mutation {
|
||||
0 => corrupt.model_hash = "swapped-model-hash".into(),
|
||||
1 => corrupt.eyes_evidence_packet_id = "swapped-packet".into(),
|
||||
2 => corrupt.eyes_evidence_packet_sha256 = "swapped-packet-hash".into(),
|
||||
3 => corrupt.current_admission_receipt_id = "swapped-admission".into(),
|
||||
4 => corrupt.target_claim_sha256 = "swapped-claim-hash".into(),
|
||||
5 => {
|
||||
corrupt.affected_frontier[0].frontier_item_sha256 =
|
||||
"swapped-frontier-hash".into()
|
||||
}
|
||||
6 => corrupt.runtime_id = "swapped-runtime".into(),
|
||||
_ => corrupt.thread_id = "swapped-thread".into(),
|
||||
}
|
||||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
cache.put(&corrupt.request_id, &corrupt)?;
|
||||
let before = std::fs::read(&store)?;
|
||||
assert!(
|
||||
plan_coordinator_job_launch(
|
||||
&state,
|
||||
&launch,
|
||||
&store,
|
||||
format!("launcher-causal-{mutation}"),
|
||||
format!("backend-causal-{mutation}"),
|
||||
)
|
||||
.is_err(),
|
||||
"causal mutation {mutation} must be refused"
|
||||
);
|
||||
assert_eq!(std::fs::read(&store)?, before);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_claim_repair_launches_contend_on_one_request_key() -> Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let (store, state, launch, repair) = claim_repair_launch_fixture(root.path(), "race")?;
|
||||
let left_plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
&launch,
|
||||
&store,
|
||||
"launcher-left".into(),
|
||||
"backend-left".into(),
|
||||
)?;
|
||||
let right_plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
&launch,
|
||||
&store,
|
||||
"launcher-right".into(),
|
||||
"backend-right".into(),
|
||||
)?;
|
||||
let left_store = store.clone();
|
||||
let right_store = store.clone();
|
||||
let left_state = state.clone();
|
||||
let right_state = state.clone();
|
||||
let left_launch = launch.clone();
|
||||
let right_launch = launch.clone();
|
||||
let left_thread = repair.thread_id.clone();
|
||||
let right_thread = repair.thread_id.clone();
|
||||
let left = std::thread::spawn(move || {
|
||||
commit_coordinator_job_launch(
|
||||
&left_store,
|
||||
&left_thread,
|
||||
&left_state,
|
||||
&left_launch,
|
||||
&left_plan,
|
||||
"2026-07-14T09:00:03Z".into(),
|
||||
)
|
||||
});
|
||||
let right = std::thread::spawn(move || {
|
||||
commit_coordinator_job_launch(
|
||||
&right_store,
|
||||
&right_thread,
|
||||
&right_state,
|
||||
&right_launch,
|
||||
&right_plan,
|
||||
"2026-07-14T09:00:04Z".into(),
|
||||
)
|
||||
});
|
||||
let outcomes = [left.join().unwrap(), right.join().unwrap()];
|
||||
assert_eq!(outcomes.iter().filter(|outcome| outcome.is_ok()).count(), 1);
|
||||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let bindings = cache
|
||||
.get_all::<RepoModelClaimRepairLaunchBinding>()?
|
||||
.into_iter()
|
||||
.filter(|binding| binding.repair_request_id == repair.request_id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(bindings.len(), 1);
|
||||
assert_eq!(
|
||||
bindings[0].binding_record_id,
|
||||
format!("repo-model-claim-repair-launch-{}", repair.request_id)
|
||||
);
|
||||
let winner = bindings[0].job_id.as_str();
|
||||
let loser = if winner == "backend-left" {
|
||||
"backend-right"
|
||||
} else {
|
||||
assert_eq!(winner, "backend-right");
|
||||
"backend-left"
|
||||
};
|
||||
assert!(cache.get::<EpiphanyRuntimeJob>(winner)?.is_some());
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeWorkerLaunchRequest>(winner)?
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeEvent>(&format!("event-job-opened-{winner}"))?
|
||||
.is_some()
|
||||
);
|
||||
assert!(cache.get::<EpiphanyRuntimeJob>(loser)?.is_none());
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeWorkerLaunchRequest>(loser)?
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeEvent>(&format!("event-job-opened-{loser}"))?
|
||||
.is_none()
|
||||
);
|
||||
let persisted_state = cache
|
||||
.get_required::<EpiphanyThreadStateEntry>(THREAD_STATE_KEY)?
|
||||
.state()?;
|
||||
assert!(
|
||||
persisted_state
|
||||
.runtime_links
|
||||
.iter()
|
||||
.any(|link| link.runtime_job_id == winner)
|
||||
);
|
||||
assert!(
|
||||
persisted_state
|
||||
.runtime_links
|
||||
.iter()
|
||||
.all(|link| link.runtime_job_id != loser)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,7 @@ mod tests {
|
|||
output_contract_id: launch_request.output_contract_id.clone(),
|
||||
organ_launch_contract: launch_request.organ_launch_contract.clone(),
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
created_at: "2026-06-02T00:00:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ pub fn apply_coordinator_state_update_from_state(
|
|||
let mut cache = open_coordinator_state_transaction(store, current_state)?;
|
||||
let changed_fields = changed_fields(&update);
|
||||
let state = apply_coordinator_state_update_to_state(current_state, update, reference_turn_id)?;
|
||||
commit_coordinator_state_transaction(&mut cache, thread_id, &state, Vec::new())?;
|
||||
commit_coordinator_state_transaction(&mut cache, thread_id, &state, Vec::new(), Vec::new())?;
|
||||
Ok(EpiphanyCoordinatorStateApplied {
|
||||
revision: state.revision,
|
||||
changed_fields,
|
||||
|
|
|
|||
|
|
@ -1,65 +1,368 @@
|
|||
use crate::EpiphanyThreadStateEntry;
|
||||
use crate::THREAD_STATE_KEY;
|
||||
use crate::coordinator_acceptance_cache;
|
||||
use crate::read_accepted_coordinator_state;
|
||||
use crate::{
|
||||
EpiphanyThreadStateEntry, THREAD_STATE_KEY, THREAD_STATE_TYPE, coordinator_acceptance_cache,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope};
|
||||
use cultcache_rs::{
|
||||
CacheBackingStore, CultCache, CultCacheEnvelope, SingleFileMessagePackBackingStore,
|
||||
};
|
||||
use epiphany_state_model::EpiphanyThreadState;
|
||||
use std::path::Path;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) struct CoordinatorStateTransaction {
|
||||
cache: CultCache,
|
||||
store: PathBuf,
|
||||
opening_snapshot: Vec<CultCacheEnvelope>,
|
||||
expected_state_envelope: Option<CultCacheEnvelope>,
|
||||
}
|
||||
|
||||
impl Deref for CoordinatorStateTransaction {
|
||||
type Target = CultCache;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cache
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for CoordinatorStateTransaction {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cache
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_coordinator_state_transaction(
|
||||
store: &Path,
|
||||
expected_state: &EpiphanyThreadState,
|
||||
) -> Result<CultCache> {
|
||||
if let Some(persisted) = read_accepted_coordinator_state(store)?
|
||||
&& persisted != *expected_state
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"authoritative coordinator state changed before transaction commit"
|
||||
));
|
||||
) -> Result<CoordinatorStateTransaction> {
|
||||
let cache = coordinator_acceptance_cache(store)?;
|
||||
let opening_snapshot = cache.snapshot_envelopes();
|
||||
let expected_state_envelope = opening_snapshot
|
||||
.iter()
|
||||
.find(|entry| entry.r#type == THREAD_STATE_TYPE && entry.key == THREAD_STATE_KEY)
|
||||
.cloned();
|
||||
if let Some(envelope) = &expected_state_envelope {
|
||||
let entry: EpiphanyThreadStateEntry = rmp_serde::from_slice(&envelope.payload)?;
|
||||
if entry.state()? != *expected_state {
|
||||
return Err(anyhow!(
|
||||
"authoritative coordinator state changed before transaction opened"
|
||||
));
|
||||
}
|
||||
}
|
||||
coordinator_acceptance_cache(store)
|
||||
Ok(CoordinatorStateTransaction {
|
||||
cache,
|
||||
store: store.to_path_buf(),
|
||||
opening_snapshot,
|
||||
expected_state_envelope,
|
||||
})
|
||||
}
|
||||
|
||||
fn same_immutable_content(left: &CultCacheEnvelope, right: &CultCacheEnvelope) -> bool {
|
||||
left.r#type == right.r#type
|
||||
&& left.key == right.key
|
||||
&& left.payload == right.payload
|
||||
&& left.schema_id == right.schema_id
|
||||
}
|
||||
|
||||
pub(crate) fn commit_coordinator_state_transaction(
|
||||
cache: &mut CultCache,
|
||||
transaction: &mut CoordinatorStateTransaction,
|
||||
thread_id: &str,
|
||||
next_state: &EpiphanyThreadState,
|
||||
mut companion_envelopes: Vec<CultCacheEnvelope>,
|
||||
companion_envelopes: Vec<CultCacheEnvelope>,
|
||||
captured_replacements: Vec<CultCacheEnvelope>,
|
||||
) -> Result<EpiphanyThreadState> {
|
||||
if companion_envelopes
|
||||
.iter()
|
||||
.any(|envelope| envelope.key == THREAD_STATE_KEY)
|
||||
.chain(&captured_replacements)
|
||||
.any(|envelope| envelope.r#type == THREAD_STATE_TYPE && envelope.key == THREAD_STATE_KEY)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"coordinator state transaction companions cannot write the canonical state key"
|
||||
"coordinator state transaction companions cannot write the canonical state identity"
|
||||
));
|
||||
}
|
||||
|
||||
let state_entry = EpiphanyThreadStateEntry::from_state(thread_id, next_state)?;
|
||||
let (state_envelope, _) = cache.prepare_entry(THREAD_STATE_KEY, &state_entry)?;
|
||||
companion_envelopes.insert(0, state_envelope);
|
||||
cache.put_prepared_batch(companion_envelopes)?;
|
||||
let (state_envelope, _) = transaction
|
||||
.cache
|
||||
.prepare_entry(THREAD_STATE_KEY, &state_entry)?;
|
||||
let mut expected = transaction
|
||||
.expected_state_envelope
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut replacements = vec![state_envelope];
|
||||
|
||||
for companion in companion_envelopes {
|
||||
let opening = transaction
|
||||
.opening_snapshot
|
||||
.iter()
|
||||
.find(|entry| entry.r#type == companion.r#type && entry.key == companion.key);
|
||||
if let Some(existing) = opening {
|
||||
if !same_immutable_content(existing, &companion) {
|
||||
return Err(anyhow!(
|
||||
"coordinator state transaction companion identity collision at ({:?}, {:?})",
|
||||
companion.r#type,
|
||||
companion.key
|
||||
));
|
||||
}
|
||||
expected.push(existing.clone());
|
||||
replacements.push(existing.clone());
|
||||
} else {
|
||||
replacements.push(companion);
|
||||
}
|
||||
}
|
||||
for replacement in captured_replacements {
|
||||
let existing = transaction
|
||||
.opening_snapshot
|
||||
.iter()
|
||||
.find(|entry| entry.r#type == replacement.r#type && entry.key == replacement.key)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"coordinator state transaction captured replacement is absent at ({:?}, {:?})",
|
||||
replacement.r#type,
|
||||
replacement.key
|
||||
)
|
||||
})?;
|
||||
expected.push(existing.clone());
|
||||
replacements.push(replacement);
|
||||
}
|
||||
|
||||
let backing = SingleFileMessagePackBackingStore::new(&transaction.store);
|
||||
if !backing.compare_and_swap_batch(&expected, replacements)? {
|
||||
return Err(anyhow!(
|
||||
"coordinator state transaction lost its exact atomic compare-and-swap"
|
||||
));
|
||||
}
|
||||
Ok(next_state.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::EpiphanyThreadStateEntry;
|
||||
|
||||
fn companion(
|
||||
transaction: &mut CoordinatorStateTransaction,
|
||||
key: &str,
|
||||
thread_id: &str,
|
||||
state: &EpiphanyThreadState,
|
||||
) -> Result<CultCacheEnvelope> {
|
||||
let entry = EpiphanyThreadStateEntry::from_state(thread_id, state)?;
|
||||
Ok(transaction.prepare_entry(key, &entry)?.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_cannot_impersonate_canonical_state() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
let mut transaction = open_coordinator_state_transaction(&store, &state)?;
|
||||
let counterfeit = EpiphanyThreadStateEntry::from_state("counterfeit", &state)?;
|
||||
let envelope = cache.prepare_entry(THREAD_STATE_KEY, &counterfeit)?.0;
|
||||
let error =
|
||||
commit_coordinator_state_transaction(&mut cache, "thread-1", &state, vec![envelope])
|
||||
.expect_err("companion state writer must be refused");
|
||||
assert!(error.to_string().contains("canonical state key"));
|
||||
assert!(read_accepted_coordinator_state(&store)?.is_none());
|
||||
let envelope = transaction.prepare_entry(THREAD_STATE_KEY, &counterfeit)?.0;
|
||||
let error = commit_coordinator_state_transaction(
|
||||
&mut transaction,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![envelope],
|
||||
Vec::new(),
|
||||
)
|
||||
.expect_err("companion state writer must be refused");
|
||||
assert!(error.to_string().contains("canonical state identity"));
|
||||
assert!(
|
||||
SingleFileMessagePackBackingStore::new(&store)
|
||||
.pull_all()?
|
||||
.is_empty()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transactions_opened_on_the_same_absent_state_have_one_winner() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut first = open_coordinator_state_transaction(&store, &state)?;
|
||||
let mut second = open_coordinator_state_transaction(&store, &state)?;
|
||||
|
||||
commit_coordinator_state_transaction(
|
||||
&mut first,
|
||||
"thread-1",
|
||||
&state,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)?;
|
||||
let error = commit_coordinator_state_transaction(
|
||||
&mut second,
|
||||
"thread-1",
|
||||
&state,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)
|
||||
.expect_err("only one transaction may seed an absent canonical identity");
|
||||
assert!(error.to_string().contains("compare-and-swap"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_store_accepts_an_imported_nondefault_seed_once() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let mut imported = EpiphanyThreadState::default();
|
||||
imported.revision = 9;
|
||||
let mut transaction = open_coordinator_state_transaction(&store, &imported)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut transaction,
|
||||
"thread-import",
|
||||
&imported,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_companion_retry_preserves_the_persisted_envelope() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut first = open_coordinator_state_transaction(&store, &state)?;
|
||||
let first_companion = companion(&mut first, "immutable-companion", "thread-1", &state)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut first,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![first_companion],
|
||||
Vec::new(),
|
||||
)?;
|
||||
let backing = SingleFileMessagePackBackingStore::new(&store);
|
||||
let persisted_before = backing
|
||||
.pull_all()?
|
||||
.into_iter()
|
||||
.find(|entry| entry.key == "immutable-companion")
|
||||
.expect("companion persisted");
|
||||
|
||||
let mut retry = open_coordinator_state_transaction(&store, &state)?;
|
||||
let retry_companion = companion(&mut retry, "immutable-companion", "thread-1", &state)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut retry,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![retry_companion],
|
||||
Vec::new(),
|
||||
)?;
|
||||
let persisted_after = backing
|
||||
.pull_all()?
|
||||
.into_iter()
|
||||
.find(|entry| entry.key == "immutable-companion")
|
||||
.expect("companion remains persisted");
|
||||
assert_eq!(persisted_after, persisted_before);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_companion_retry_writes_nothing() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut first = open_coordinator_state_transaction(&store, &state)?;
|
||||
let first_companion = companion(&mut first, "immutable-companion", "thread-1", &state)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut first,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![first_companion],
|
||||
Vec::new(),
|
||||
)?;
|
||||
let bytes_before = std::fs::read(&store)?;
|
||||
|
||||
let mut collision = open_coordinator_state_transaction(&store, &state)?;
|
||||
let conflicting = companion(
|
||||
&mut collision,
|
||||
"immutable-companion",
|
||||
"different-thread",
|
||||
&state,
|
||||
)?;
|
||||
let error = commit_coordinator_state_transaction(
|
||||
&mut collision,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![conflicting],
|
||||
Vec::new(),
|
||||
)
|
||||
.expect_err("immutable companion identity collision must be refused");
|
||||
assert!(error.to_string().contains("identity collision"));
|
||||
assert_eq!(std::fs::read(&store)?, bytes_before);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_replacement_has_one_exact_snapshot_winner() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut seed = open_coordinator_state_transaction(&store, &state)?;
|
||||
let original = companion(&mut seed, "mutable-companion", "original", &state)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut seed,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![original],
|
||||
Vec::new(),
|
||||
)?;
|
||||
|
||||
let mut first = open_coordinator_state_transaction(&store, &state)?;
|
||||
let mut second = open_coordinator_state_transaction(&store, &state)?;
|
||||
let first_replacement = companion(&mut first, "mutable-companion", "winner", &state)?;
|
||||
let second_replacement = companion(&mut second, "mutable-companion", "loser", &state)?;
|
||||
commit_coordinator_state_transaction(
|
||||
&mut first,
|
||||
"thread-1",
|
||||
&state,
|
||||
Vec::new(),
|
||||
vec![first_replacement],
|
||||
)?;
|
||||
let error = commit_coordinator_state_transaction(
|
||||
&mut second,
|
||||
"thread-1",
|
||||
&state,
|
||||
Vec::new(),
|
||||
vec![second_replacement],
|
||||
)
|
||||
.expect_err("a captured replacement cannot overwrite a newer envelope");
|
||||
assert!(error.to_string().contains("compare-and-swap"));
|
||||
|
||||
let cache = coordinator_acceptance_cache(&store)?;
|
||||
let persisted = cache.get_required::<EpiphanyThreadStateEntry>("mutable-companion")?;
|
||||
assert_eq!(persisted.thread_id, "winner");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_identity_is_polymorphic_type_and_key() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("state.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut transaction = open_coordinator_state_transaction(&store, &state)?;
|
||||
let typed = companion(&mut transaction, "shared-key", "thread-1", &state)?;
|
||||
let other_type = CultCacheEnvelope {
|
||||
key: "shared-key".into(),
|
||||
r#type: "test.other_type".into(),
|
||||
payload: vec![1, 2, 3],
|
||||
stored_at: "2026-07-14T00:00:00Z".into(),
|
||||
schema_id: Some("test.other_type.v0".into()),
|
||||
};
|
||||
commit_coordinator_state_transaction(
|
||||
&mut transaction,
|
||||
"thread-1",
|
||||
&state,
|
||||
vec![typed, other_type],
|
||||
Vec::new(),
|
||||
)?;
|
||||
let shared = SingleFileMessagePackBackingStore::new(&store)
|
||||
.pull_all()?
|
||||
.into_iter()
|
||||
.filter(|entry| entry.key == "shared-key")
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(shared.len(), 2);
|
||||
assert_ne!(shared[0].r#type, shared[1].r#type);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -980,8 +980,11 @@ pub use surfaces::EpiphanyWorkerLaunchDocument;
|
|||
pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
|
||||
pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT;
|
||||
pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION;
|
||||
pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT;
|
||||
pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION;
|
||||
pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID;
|
||||
pub use surfaces::RepoFrontierProposalModelingContextProjection;
|
||||
pub use surfaces::RepoModelClaimRepairContextProjection;
|
||||
pub use surfaces::build_reorient_acceptance_bundle;
|
||||
pub use surfaces::build_reorient_job_launch_request;
|
||||
pub use surfaces::build_role_acceptance_bundle;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ pub const REPO_MODEL_CLAIM_REPAIR_REQUEST_SCHEMA_VERSION: &str =
|
|||
"epiphany.modeling.repo_model_claim_repair_request.v0";
|
||||
pub const REPO_MODEL_CLAIM_REPAIR_REQUEST_CONTRACT: &str =
|
||||
"epiphany.repo_model_claim_repair_request.v0";
|
||||
pub const REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_SCHEMA_VERSION: &str =
|
||||
"epiphany.coordinator.repo_model_claim_repair_launch_binding.v0";
|
||||
pub const REPO_MODEL_CLAIM_REPAIR_LAUNCH_BINDING_CONTRACT: &str =
|
||||
"epiphany.repo_model_claim_repair_launch_binding.v0";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
|
@ -152,6 +156,38 @@ pub struct RepoModelClaimRepairRequest {
|
|||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "epiphany.coordinator.repo_model_claim_repair_launch_binding",
|
||||
schema = "RepoModelClaimRepairLaunchBinding"
|
||||
)]
|
||||
pub struct RepoModelClaimRepairLaunchBinding {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema_version: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub binding_record_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub repair_request_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub challenge_id: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub challenge_sha256: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub job_id: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub binding_id: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub runtime_id: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub thread_id: String,
|
||||
#[cultcache(key = 9)]
|
||||
pub launched_at: String,
|
||||
#[cultcache(key = 10)]
|
||||
pub worker_launch_document_sha256: String,
|
||||
#[cultcache(key = 11)]
|
||||
pub contract: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RepoModelClaimRepairFrontierRef {
|
||||
pub frontier_item_id: String,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ use crate::repo_model_gateway::{
|
|||
RepoFrontierProposalModelingLaunchBinding, RepoFrontierProposalModelingRequest,
|
||||
RepoFrontierRoute, RepoFrontierVerdictDisposition, RepoFrontierWorkProposal,
|
||||
RepoModelAdmissionReceipt, RepoModelAdmissionReview, RepoModelClaimChallenge,
|
||||
RepoModelClaimRepairFrontierRef, RepoModelClaimRepairLaunchBinding,
|
||||
RepoModelClaimRepairRequest, RepoModelMigrationReceipt,
|
||||
};
|
||||
use crate::soul_gateway::SoulVerdictReceipt;
|
||||
|
|
@ -325,6 +326,8 @@ pub struct EpiphanyRuntimeWorkerLaunchRequest {
|
|||
pub organ_launch_contract: EpiphanyLaunchOrganContract,
|
||||
#[cultcache(key = 11, default)]
|
||||
pub proposal_modeling_request_id: Option<String>,
|
||||
#[cultcache(key = 12, default)]
|
||||
pub claim_repair_request_id: Option<String>,
|
||||
}
|
||||
|
||||
impl EpiphanyRuntimeWorkerLaunchRequest {
|
||||
|
|
@ -677,6 +680,7 @@ pub struct RuntimeSpineHeartbeatJobOptions {
|
|||
pub output_contract_id: String,
|
||||
pub organ_launch_contract: EpiphanyLaunchOrganContract,
|
||||
pub proposal_modeling_request_id: Option<String>,
|
||||
pub claim_repair_request_id: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
|
|
@ -725,6 +729,7 @@ pub struct EpiphanyJobLaunchRequest {
|
|||
pub organ_launch_contract: EpiphanyLaunchOrganContract,
|
||||
pub max_runtime_seconds: Option<u64>,
|
||||
pub proposal_modeling_request_id: Option<String>,
|
||||
pub claim_repair_request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
@ -771,6 +776,7 @@ pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
|
|||
cache.register_entry_type::<RepoModelMigrationReceipt>()?;
|
||||
cache.register_entry_type::<RepoModelClaimChallenge>()?;
|
||||
cache.register_entry_type::<RepoModelClaimRepairRequest>()?;
|
||||
cache.register_entry_type::<RepoModelClaimRepairLaunchBinding>()?;
|
||||
cache.register_entry_type::<RepoFrontierRoute>()?;
|
||||
cache.register_entry_type::<RepoFrontierHandsAuthority>()?;
|
||||
cache.register_entry_type::<RepoFrontierModelingRequest>()?;
|
||||
|
|
@ -1040,6 +1046,12 @@ pub fn open_runtime_spine_heartbeat_job(
|
|||
options.proposal_modeling_request_id.as_deref(),
|
||||
&options.launch_document,
|
||||
)?;
|
||||
validate_claim_repair_launch_carrier(
|
||||
&options.role,
|
||||
&options.binding_id,
|
||||
options.claim_repair_request_id.as_deref(),
|
||||
&options.launch_document,
|
||||
)?;
|
||||
validate_non_empty(&options.binding_id, "binding id")?;
|
||||
validate_non_empty(&options.authority_scope, "authority scope")?;
|
||||
validate_non_empty(&options.instruction, "instruction")?;
|
||||
|
|
@ -1124,6 +1136,7 @@ pub fn open_runtime_spine_heartbeat_job(
|
|||
metadata: BTreeMap::new(),
|
||||
organ_launch_contract,
|
||||
proposal_modeling_request_id: options.proposal_modeling_request_id,
|
||||
claim_repair_request_id: options.claim_repair_request_id,
|
||||
};
|
||||
cache.put(&job_id, &request)?;
|
||||
Ok(job)
|
||||
|
|
@ -1145,6 +1158,12 @@ pub fn prepare_runtime_spine_heartbeat_job(
|
|||
options.proposal_modeling_request_id.as_deref(),
|
||||
&options.launch_document,
|
||||
)?;
|
||||
validate_claim_repair_launch_carrier(
|
||||
&options.role,
|
||||
&options.binding_id,
|
||||
options.claim_repair_request_id.as_deref(),
|
||||
&options.launch_document,
|
||||
)?;
|
||||
validate_non_empty(&options.binding_id, "binding id")?;
|
||||
validate_non_empty(&options.authority_scope, "authority scope")?;
|
||||
validate_non_empty(&options.instruction, "instruction")?;
|
||||
|
|
@ -1255,6 +1274,7 @@ pub fn prepare_runtime_spine_heartbeat_job(
|
|||
metadata: BTreeMap::new(),
|
||||
organ_launch_contract: options.organ_launch_contract,
|
||||
proposal_modeling_request_id: options.proposal_modeling_request_id,
|
||||
claim_repair_request_id: options.claim_repair_request_id,
|
||||
};
|
||||
let envelopes = vec![
|
||||
cache.prepare_entry(RUNTIME_IDENTITY_KEY, &identity)?.0,
|
||||
|
|
@ -1299,6 +1319,39 @@ fn validate_proposal_modeling_launch_carrier(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_claim_repair_launch_carrier(
|
||||
role: &str,
|
||||
binding_id: &str,
|
||||
claim_repair_request_id: Option<&str>,
|
||||
launch_document: &EpiphanyWorkerLaunchDocument,
|
||||
) -> Result<()> {
|
||||
let projection = match launch_document {
|
||||
EpiphanyWorkerLaunchDocument::Role(document) => document.claim_repair_context.as_ref(),
|
||||
EpiphanyWorkerLaunchDocument::Reorient(_) => None,
|
||||
};
|
||||
let Some(request_id) = claim_repair_request_id else {
|
||||
if projection.is_some() {
|
||||
return Err(anyhow!(
|
||||
"claim repair context requires its typed request id"
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
validate_non_empty(request_id, "claim repair request id")?;
|
||||
if role != EPIPHANY_MODELING_OWNER_ROLE || binding_id != EPIPHANY_MODELING_ROLE_BINDING_ID {
|
||||
return Err(anyhow!(
|
||||
"claim repair request id may only be transported by the Modeling role launch"
|
||||
));
|
||||
}
|
||||
let projection = projection.ok_or_else(|| {
|
||||
anyhow!("claim repair request id requires coordinator-owned typed context")
|
||||
})?;
|
||||
if projection.request_id != request_id {
|
||||
return Err(anyhow!("claim repair context/request mismatch"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn runtime_job_snapshot(
|
||||
store_path: impl AsRef<Path>,
|
||||
job_id: &str,
|
||||
|
|
@ -2852,6 +2905,7 @@ pub fn commit_repo_model_claim_repair_request(
|
|||
requested_at: requested_at.to_string(),
|
||||
contract: REPO_MODEL_CLAIM_REPAIR_REQUEST_CONTRACT.into(),
|
||||
};
|
||||
validate_current_repo_model_claim_repair_request(&cache, &request)?;
|
||||
if let Some(existing) = cache.get::<RepoModelClaimRepairRequest>(&request_id)? {
|
||||
return if existing == request {
|
||||
Ok(existing)
|
||||
|
|
@ -2931,6 +2985,105 @@ pub fn commit_repo_model_claim_repair_request(
|
|||
Ok(request)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_current_repo_model_claim_repair_request(
|
||||
cache: &CultCache,
|
||||
request: &RepoModelClaimRepairRequest,
|
||||
) -> Result<()> {
|
||||
chrono::DateTime::parse_from_rfc3339(&request.requested_at)
|
||||
.map_err(|_| anyhow!("claim repair request timestamp must be RFC3339"))?;
|
||||
let entry = cache
|
||||
.get::<crate::EpiphanyMemoryGraphEntry>(crate::MEMORY_GRAPH_KEY)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires canonical model"))?;
|
||||
crate::validate_memory_graph_entry(&entry)?;
|
||||
let model = entry.snapshot()?;
|
||||
let model_hash = crate::memory_graph_model_hash(&model)?;
|
||||
let challenge = cache
|
||||
.get::<RepoModelClaimChallenge>(&request.challenge_id)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires exact challenge"))?;
|
||||
validate_repo_model_claim_challenge_chain(cache, &model, &model_hash, &challenge, false)?;
|
||||
let packet = cache
|
||||
.get::<EyesEvidencePacket>(&challenge.eyes_evidence_packet_id)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires exact Eyes packet"))?;
|
||||
let original_admission = cache
|
||||
.get::<RepoModelAdmissionReceipt>(&challenge.admission_receipt_id)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires original challenge admission"))?;
|
||||
let identity = cache
|
||||
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires runtime identity"))?;
|
||||
let thread = cache
|
||||
.get::<crate::EpiphanyThreadStateEntry>(crate::THREAD_STATE_KEY)?
|
||||
.ok_or_else(|| anyhow!("claim repair request requires authoritative thread"))?;
|
||||
let claim = model
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == challenge.target_claim_id)
|
||||
.ok_or_else(|| anyhow!("claim repair target is no longer present"))?;
|
||||
if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(claim)?))
|
||||
!= challenge.target_claim_sha256
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"claim repair challenge is already resolved by a changed claim"
|
||||
));
|
||||
}
|
||||
let receipts = cache
|
||||
.get_all::<RepoModelAdmissionReceipt>()?
|
||||
.into_iter()
|
||||
.filter(|receipt| {
|
||||
receipt.schema_version == REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION
|
||||
&& receipt.contract == REPO_MODEL_ADMISSION_CONTRACT
|
||||
&& receipt.admitted_revision == model.model_revision
|
||||
&& receipt.admitted_hash == model_hash
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if receipts.len() != 1 {
|
||||
return Err(anyhow!(
|
||||
"claim repair request requires unique current admission receipt"
|
||||
));
|
||||
}
|
||||
let mut affected_frontier = model
|
||||
.frontier
|
||||
.iter()
|
||||
.filter(|item| item.target_claim_ids.contains(&challenge.target_claim_id))
|
||||
.map(|item| {
|
||||
Ok(RepoModelClaimRepairFrontierRef {
|
||||
frontier_item_id: item.id.clone(),
|
||||
frontier_item_sha256: format!(
|
||||
"{:x}",
|
||||
Sha256::digest(rmp_serde::to_vec_named(item)?)
|
||||
),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
affected_frontier.sort_by(|a, b| a.frontier_item_id.cmp(&b.frontier_item_id));
|
||||
let expected = RepoModelClaimRepairRequest {
|
||||
schema_version: REPO_MODEL_CLAIM_REPAIR_REQUEST_SCHEMA_VERSION.into(),
|
||||
request_id: format!("repo-model-claim-repair-{}", challenge.challenge_id),
|
||||
challenge_id: challenge.challenge_id.clone(),
|
||||
challenge_sha256: format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&challenge)?)),
|
||||
eyes_evidence_packet_id: packet.packet_id.clone(),
|
||||
eyes_evidence_packet_sha256: challenge.eyes_evidence_packet_sha256.clone(),
|
||||
source_result_id: challenge.source_result_id.clone(),
|
||||
source_job_id: challenge.source_job_id.clone(),
|
||||
original_admission_receipt_id: original_admission.receipt_id.clone(),
|
||||
current_admission_receipt_id: receipts[0].receipt_id.clone(),
|
||||
model_revision: model.model_revision,
|
||||
model_hash,
|
||||
target_claim_id: challenge.target_claim_id.clone(),
|
||||
target_claim_sha256: challenge.target_claim_sha256.clone(),
|
||||
runtime_id: identity.runtime_id.clone(),
|
||||
thread_id: thread.thread_id.clone(),
|
||||
affected_frontier,
|
||||
requested_at: request.requested_at.clone(),
|
||||
contract: REPO_MODEL_CLAIM_REPAIR_REQUEST_CONTRACT.into(),
|
||||
};
|
||||
if *request != expected {
|
||||
return Err(anyhow!(
|
||||
"claim repair request does not match the current canonical causal chain"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn put_repo_frontier_plan_candidate(
|
||||
store_path: impl AsRef<Path>,
|
||||
candidate: &RepoFrontierPlanCandidate,
|
||||
|
|
@ -6295,7 +6448,7 @@ fn merge_refs(existing: &[String], incoming: &[String]) -> Vec<String> {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use cultnet_rs::CultNetWireContract;
|
||||
use cultnet_rs::decode_cultnet_message_from_slice;
|
||||
|
|
@ -7103,7 +7256,7 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
fn claim_challenge_fixture(
|
||||
pub(crate) fn claim_challenge_fixture(
|
||||
root: &Path,
|
||||
suffix: &str,
|
||||
next_organ: &str,
|
||||
|
|
@ -9139,6 +9292,7 @@ mod tests {
|
|||
objective: Some("Run heartbeat worker.".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
proposal_modeling_context: None,
|
||||
claim_repair_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: vec!["node-model".to_string()],
|
||||
|
|
@ -9161,6 +9315,7 @@ mod tests {
|
|||
crate::ROLE_WORKER_OUTPUT_CONTRACT_ID,
|
||||
),
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
created_at: "2026-05-06T00:02:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
|
|
@ -9230,6 +9385,7 @@ mod tests {
|
|||
objective: Some("keep state typed".to_string()),
|
||||
dynamic_prompt_context: None,
|
||||
proposal_modeling_context: None,
|
||||
claim_repair_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: vec!["runtime-spine".to_string()],
|
||||
|
|
@ -9315,6 +9471,7 @@ mod tests {
|
|||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
proposal_modeling_context: None,
|
||||
claim_repair_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ mod tests {
|
|||
objective: None,
|
||||
dynamic_prompt_context: None,
|
||||
proposal_modeling_context: None,
|
||||
claim_repair_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: Vec::new(),
|
||||
|
|
@ -273,6 +274,7 @@ mod tests {
|
|||
),
|
||||
max_runtime_seconds: None,
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
};
|
||||
let grant = substrate_gate_repo_access_grant_for_launch(
|
||||
"grant-1".to_string(),
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ pub use worker_launch::EpiphanyWorkerLaunchDocument;
|
|||
pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
|
||||
pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT;
|
||||
pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION;
|
||||
pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT;
|
||||
pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION;
|
||||
pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID;
|
||||
pub use worker_launch::RepoFrontierProposalModelingContextProjection;
|
||||
pub use worker_launch::RepoModelClaimRepairContextProjection;
|
||||
pub use worker_launch::build_reorient_job_launch_request;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str =
|
|||
"epiphany.worker.repo_frontier_proposal_modeling_context.v0";
|
||||
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT: &str =
|
||||
"epiphany.repo_frontier_proposal_modeling_context.v0";
|
||||
pub const REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION: &str =
|
||||
"epiphany.worker.repo_model_claim_repair_context.v0";
|
||||
pub const REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT: &str =
|
||||
"epiphany.repo_model_claim_repair_context.v0";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "documentKind")]
|
||||
|
|
@ -83,6 +87,8 @@ pub struct EpiphanyRoleWorkerLaunchDocument {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proposal_modeling_context: Option<RepoFrontierProposalModelingContextProjection>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub claim_repair_context: Option<RepoModelClaimRepairContextProjection>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub active_subgoal_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub active_subgoals: Vec<EpiphanySubgoal>,
|
||||
|
|
@ -200,6 +206,30 @@ pub struct RepoFrontierProposalModelingContextProjection {
|
|||
pub model_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RepoModelClaimRepairContextProjection {
|
||||
pub schema_version: String,
|
||||
pub contract: String,
|
||||
pub request_id: String,
|
||||
pub challenge_id: String,
|
||||
pub challenge_sha256: String,
|
||||
pub eyes_evidence_packet_id: String,
|
||||
pub eyes_evidence_packet_sha256: String,
|
||||
pub source_result_id: String,
|
||||
pub source_job_id: String,
|
||||
pub original_admission_receipt_id: String,
|
||||
pub current_admission_receipt_id: String,
|
||||
pub model_revision: u64,
|
||||
pub model_hash: String,
|
||||
pub target_claim_id: String,
|
||||
pub target_claim_sha256: String,
|
||||
pub runtime_id: String,
|
||||
pub thread_id: String,
|
||||
pub affected_frontier: Vec<crate::RepoModelClaimRepairFrontierRef>,
|
||||
pub requested_at: String,
|
||||
}
|
||||
|
||||
pub fn build_reorient_job_launch_request(
|
||||
input: EpiphanyReorientLaunchRequestInput<'_>,
|
||||
) -> EpiphanyJobLaunchRequest {
|
||||
|
|
@ -241,6 +271,7 @@ pub fn build_reorient_job_launch_request(
|
|||
organ_launch_contract,
|
||||
max_runtime_seconds: input.max_runtime_seconds,
|
||||
proposal_modeling_request_id: None,
|
||||
claim_repair_request_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,33 @@
|
|||
# Fresh Workspace Handoff
|
||||
|
||||
## Modeling claim-repair launch authority landed - 2026-07-14
|
||||
|
||||
The inert `RepoModelClaimRepairRequest` now reaches Modeling through one
|
||||
coordinator-owned typed context projection and one immutable, request-keyed
|
||||
launch binding. The coordinator reconstructs the projection from current
|
||||
canonical state; callers cannot prepopulate it, combine it with proposal
|
||||
Modeling authority, substitute any causal field, or spend one repair request
|
||||
twice. The launch document hash binds the exact effective document delivered to
|
||||
the worker. This slice still grants no RepoModel write authority.
|
||||
|
||||
The launch race exposed a deeper state-transaction fault: two coordinators could
|
||||
validate the same logical state and both publish. `CoordinatorStateTransaction`
|
||||
now captures one exact typed cache image, validates the canonical state by
|
||||
polymorphic `(type, key)` identity, and commits state plus companions through one
|
||||
backing-store CAS. Immutable companions are absent-only or byte-equivalent
|
||||
retries with the persisted timestamp preserved. Explicit captured replacements
|
||||
may replace only the exact envelope observed at open. Imported nondefault host
|
||||
state may seed an absent store once. Focused hostile tests prove one race winner,
|
||||
no losing job/request/event/runtime link, immutable collision no-write behavior,
|
||||
captured-replacement contention, and same-key/different-type coexistence. Soul
|
||||
re-reviewed the repaired proof surface and passed it; the full core library is
|
||||
green at 314 passed, one ignored.
|
||||
|
||||
Next: make the Modeling result echo this repair request exclusively, then add a
|
||||
dedicated narrow patch purpose and Mind admission that replays the full chain and
|
||||
requires the exact challenged claim bytes to change. No other result or model
|
||||
revision may clear challenge pressure.
|
||||
|
||||
## Modeling name authority correction - 2026-07-14
|
||||
|
||||
Modeling is the standing embodied organ that models the Body. Proprioception is
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -714,4 +714,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`. swarm overview 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: Complete the Aetheria-shaped corrective nerve from the landed inert RepoModelClaimRepairRequest. The request now survives unrelated model revisions and exactly binds challenge, Eyes packet, original/current admissions, model, claim, affected frontier, runtime, and thread without any model-write authority. Next make the coordinator launch transaction the sole writer of a typed Modeling repair projection and immutable launch-document binding; make the Modeling result echo that request exclusively; then add a dedicated narrow patch purpose and Mind full-chain admission that requires the exact challenged claim bytes to change. No ambient Modeling result, ordinary Evolution, prompt prose, evidence intersection, UI state, or unrelated model revision may clear pressure. After that, bind the existing Self planning request to a real Imagination worker result and Mind Adopt/Refuse/Hold path. Add the production semantic index over typed graph claims. Do not revive Proprioception, epiphany-work, metadata adapters, challenge ids inside RepoModel, or generic Modeling work-order routers. Keep closure's zero-substring invariant.
|
||||
next_action: Complete the Aetheria-shaped corrective nerve from the landed RepoModelClaimRepairRequest and coordinator-owned Modeling launch binding. The request survives unrelated model revisions and exactly binds challenge, Eyes packet, original/current admissions, model, claim, affected frontier, runtime, thread, effective worker document, and one atomic single-use launch without any model-write authority. CoordinatorStateTransaction now owns one exact opening cache image and one state-plus-companions CAS; immutable companions converge, explicit captured replacements compare against their opened envelope, and no losing launch artifacts survive contention. Next make the Modeling result echo that repair request exclusively, then add a dedicated narrow patch purpose and Mind full-chain admission that requires the exact challenged claim bytes to change. No ambient Modeling result, ordinary Evolution, prompt prose, evidence intersection, UI state, unrelated model revision, or losing concurrent launch may clear pressure. After that, bind the existing Self planning request to a real Imagination worker result and Mind Adopt/Refuse/Hold path. Add the production semantic index over typed graph claims. Do not revive Proprioception, epiphany-work, metadata adapters, challenge ids inside RepoModel, or generic Modeling work-order routers. Keep closure's zero-substring invariant.
|
||||
|
|
|
|||
2
vendor/cultcache-rs
vendored
2
vendor/cultcache-rs
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 141f2aa89a25016166b53fc47b0b48b6beabca39
|
||||
Subproject commit 1383a74becfd17f49879e574c48995b0e2b502aa
|
||||
Reference in a new issue