Make coordinator launch atomic
This commit is contained in:
parent
e27d0dfcfe
commit
41f5a3dceb
8 changed files with 386 additions and 103 deletions
|
|
@ -5,7 +5,6 @@ use std::path::PathBuf;
|
|||
|
||||
use epiphany_core::CONTINUITY_RECOVERY_RECEIPT_TYPE;
|
||||
use epiphany_core::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
|
||||
use epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID;
|
||||
use epiphany_core::EYES_EVIDENCE_PACKET_TYPE;
|
||||
use epiphany_core::EpiphanyJobInterruptRequest;
|
||||
use epiphany_core::EpiphanyJobInterruptResult;
|
||||
|
|
@ -25,7 +24,6 @@ use epiphany_core::EpiphanyStateUpdate;
|
|||
use epiphany_core::EpiphanyStateUpdatedField;
|
||||
use epiphany_core::EpiphanyTokenUsageSnapshot;
|
||||
use epiphany_core::MIND_GATEWAY_REVIEW_TYPE;
|
||||
use epiphany_core::RuntimeSpineHeartbeatJobOptions;
|
||||
use epiphany_core::SOUL_VERDICT_RECEIPT_TYPE;
|
||||
use epiphany_core::SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE;
|
||||
use epiphany_core::apply_coordinator_state_update_to_state;
|
||||
|
|
@ -37,11 +35,8 @@ use epiphany_core::epiphany_role_label;
|
|||
use epiphany_core::evaluate_promotion;
|
||||
use epiphany_core::eyes_evidence_packet_from_research_finding;
|
||||
use epiphany_core::mind_state_commit_receipt;
|
||||
use epiphany_core::open_runtime_spine_heartbeat_job;
|
||||
use epiphany_core::put_substrate_gate_repo_access_grant_receipt;
|
||||
use epiphany_core::runtime_substrate_gate_repo_access_grant_receipt;
|
||||
use epiphany_core::soul_verdict_receipt_from_verification_finding;
|
||||
use epiphany_core::substrate_gate_repo_access_grant_for_launch;
|
||||
use epiphany_state_model::EpiphanyEvidenceRecord;
|
||||
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
|
||||
use epiphany_state_model::EpiphanyRetrievalState;
|
||||
|
|
@ -213,27 +208,16 @@ pub async fn launch_epiphany_job_on_thread(
|
|||
Uuid::new_v4().to_string(),
|
||||
)
|
||||
.map_err(|err| EpiphanyBridgeError::InvalidRequest(err.to_string()))?;
|
||||
open_epiphany_runtime_spine_job(
|
||||
let thread_id = thread.epiphany_thread_id().await;
|
||||
epiphany_core::commit_coordinator_job_launch(
|
||||
runtime_store.as_path(),
|
||||
&launch_plan.planning_state,
|
||||
&thread_id,
|
||||
¤t_state,
|
||||
&request,
|
||||
launch_plan.backend_job_id.as_str(),
|
||||
)?;
|
||||
maybe_put_substrate_gate_launch_grant(
|
||||
runtime_store.as_path(),
|
||||
&request,
|
||||
launch_plan.backend_job_id.as_str(),
|
||||
)?;
|
||||
|
||||
let epiphany_state =
|
||||
apply_epiphany_state_update_to_thread(thread, launch_plan.state_update).await?;
|
||||
|
||||
Ok(EpiphanyJobLaunchResult {
|
||||
epiphany_state,
|
||||
binding_id: request.binding_id,
|
||||
launcher_job_id: launch_plan.launcher_job_id,
|
||||
backend_job_id: launch_plan.backend_job_id,
|
||||
})
|
||||
&launch_plan,
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
)
|
||||
.map_err(|error| EpiphanyBridgeError::Fatal(error.to_string()))
|
||||
}
|
||||
|
||||
pub async fn interrupt_epiphany_job_on_thread(
|
||||
|
|
@ -753,46 +737,6 @@ pub async fn launch_thread_epiphany_reorient(
|
|||
})
|
||||
}
|
||||
|
||||
fn open_epiphany_runtime_spine_job(
|
||||
store_path: &Path,
|
||||
state: &EpiphanyThreadState,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
backend_job_id: &str,
|
||||
) -> BridgeResult<()> {
|
||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
open_runtime_spine_heartbeat_job(
|
||||
store_path,
|
||||
RuntimeSpineHeartbeatJobOptions {
|
||||
runtime_id: "epiphany-local".to_string(),
|
||||
display_name: "Epiphany Local".to_string(),
|
||||
session_id: "epiphany-main".to_string(),
|
||||
objective: state
|
||||
.objective
|
||||
.clone()
|
||||
.filter(|objective| !objective.trim().is_empty())
|
||||
.unwrap_or_else(|| "Epiphany heartbeat activation".to_string()),
|
||||
coordinator_note: "App-server launch opened this typed runtime session.".to_string(),
|
||||
job_id: backend_job_id.to_string(),
|
||||
role: request.owner_role.clone(),
|
||||
binding_id: request.binding_id.clone(),
|
||||
authority_scope: request.authority_scope.clone(),
|
||||
instruction: request.instruction.clone(),
|
||||
launch_document: request.launch_document.clone(),
|
||||
output_contract_id: request.output_contract_id.clone(),
|
||||
organ_launch_contract: request.organ_launch_contract.clone(),
|
||||
created_at: now,
|
||||
},
|
||||
)
|
||||
.map_err(|err| {
|
||||
EpiphanyBridgeError::Fatal(format!(
|
||||
"failed to open Epiphany runtime spine job {:?} in {}: {err}",
|
||||
backend_job_id,
|
||||
store_path.display()
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_expected_revision(
|
||||
expected_revision: Option<u64>,
|
||||
actual_revision: u64,
|
||||
|
|
@ -837,27 +781,6 @@ fn enforce_current_receipt_proofs(
|
|||
.map_err(map_coordinator_admission_error)
|
||||
}
|
||||
|
||||
fn maybe_put_substrate_gate_launch_grant(
|
||||
runtime_store_path: &Path,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
backend_job_id: &str,
|
||||
) -> BridgeResult<()> {
|
||||
if request.binding_id != EPIPHANY_RESEARCH_ROLE_BINDING_ID {
|
||||
return Ok(());
|
||||
}
|
||||
let grant = substrate_gate_repo_access_grant_for_launch(
|
||||
substrate_gate_grant_receipt_id(backend_job_id),
|
||||
backend_job_id.to_string(),
|
||||
request,
|
||||
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
);
|
||||
put_substrate_gate_repo_access_grant_receipt(runtime_store_path, &grant).map_err(|err| {
|
||||
EpiphanyBridgeError::Fatal(format!(
|
||||
"failed to persist Substrate Gate access grant for research launch: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn persisted_substrate_gate_grant_types(
|
||||
runtime_store_path: &Path,
|
||||
receipt_id: &str,
|
||||
|
|
@ -920,11 +843,15 @@ mod acceptance_architecture_tests {
|
|||
.unwrap();
|
||||
let launch = &source[launch_start..interrupt_start];
|
||||
assert_eq!(launch.matches("plan_coordinator_job_launch").count(), 1);
|
||||
assert_eq!(launch.matches("commit_coordinator_job_launch").count(), 1);
|
||||
for forbidden in [
|
||||
["plan", "runtime", "spine", "heartbeat", "launch"].join("_"),
|
||||
["replace", "or", "append", "epiphany", "job", "binding"].join("_"),
|
||||
["runtime", "job", "snapshot"].join("_"),
|
||||
["epiphany", "state", "update", "validation", "errors"].join("_"),
|
||||
["open", "epiphany", "runtime", "spine", "job"].join("_"),
|
||||
["maybe", "put", "substrate", "gate", "launch", "grant"].join("_"),
|
||||
["apply", "epiphany", "state", "update", "to", "thread"].join("_"),
|
||||
] {
|
||||
assert!(
|
||||
!launch.contains(&forbidden),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::*;
|
||||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope};
|
||||
use epiphany_state_model::{EpiphanyRuntimeLink, EpiphanyThreadState};
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -12,6 +13,91 @@ pub struct EpiphanyCoordinatorJobLaunchPlan {
|
|||
pub state_update: EpiphanyStateUpdate,
|
||||
}
|
||||
|
||||
pub fn commit_coordinator_job_launch(
|
||||
store: &Path,
|
||||
thread_id: &str,
|
||||
current_state: &EpiphanyThreadState,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
plan: &EpiphanyCoordinatorJobLaunchPlan,
|
||||
created_at: String,
|
||||
) -> Result<EpiphanyJobLaunchResult> {
|
||||
if let Some(persisted) = read_accepted_coordinator_state(store)?
|
||||
&& persisted != *current_state
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"authoritative coordinator state changed before launch commit"
|
||||
));
|
||||
}
|
||||
let mut cache = coordinator_acceptance_cache(store)?;
|
||||
commit_coordinator_job_launch_in_cache(
|
||||
&mut cache,
|
||||
thread_id,
|
||||
current_state,
|
||||
request,
|
||||
plan,
|
||||
created_at,
|
||||
Vec::new(),
|
||||
)
|
||||
}
|
||||
|
||||
fn commit_coordinator_job_launch_in_cache(
|
||||
cache: &mut CultCache,
|
||||
thread_id: &str,
|
||||
current_state: &EpiphanyThreadState,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
plan: &EpiphanyCoordinatorJobLaunchPlan,
|
||||
created_at: String,
|
||||
injected_envelopes: Vec<CultCacheEnvelope>,
|
||||
) -> Result<EpiphanyJobLaunchResult> {
|
||||
let next_state =
|
||||
apply_coordinator_state_update_to_state(current_state, plan.state_update.clone(), None)?;
|
||||
let prepared = prepare_runtime_spine_heartbeat_job(
|
||||
&cache,
|
||||
RuntimeSpineHeartbeatJobOptions {
|
||||
runtime_id: "epiphany-local".to_string(),
|
||||
display_name: "Epiphany Local".to_string(),
|
||||
session_id: "epiphany-main".to_string(),
|
||||
objective: plan
|
||||
.planning_state
|
||||
.objective
|
||||
.clone()
|
||||
.filter(|objective| !objective.trim().is_empty())
|
||||
.unwrap_or_else(|| "Epiphany heartbeat activation".to_string()),
|
||||
coordinator_note: "Native coordinator launch transaction opened this session."
|
||||
.to_string(),
|
||||
job_id: plan.backend_job_id.clone(),
|
||||
role: request.owner_role.clone(),
|
||||
binding_id: request.binding_id.clone(),
|
||||
authority_scope: request.authority_scope.clone(),
|
||||
instruction: request.instruction.clone(),
|
||||
launch_document: request.launch_document.clone(),
|
||||
output_contract_id: request.output_contract_id.clone(),
|
||||
organ_launch_contract: request.organ_launch_contract.clone(),
|
||||
created_at: created_at.clone(),
|
||||
},
|
||||
)?;
|
||||
let mut batch = prepared.envelopes;
|
||||
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),
|
||||
plan.backend_job_id.clone(),
|
||||
request,
|
||||
created_at,
|
||||
);
|
||||
batch.push(cache.prepare_entry(&grant.receipt_id, &grant)?.0);
|
||||
}
|
||||
let state_entry = EpiphanyThreadStateEntry::from_state(thread_id, &next_state)?;
|
||||
batch.push(cache.prepare_entry(THREAD_STATE_KEY, &state_entry)?.0);
|
||||
batch.extend(injected_envelopes);
|
||||
cache.put_prepared_batch(batch)?;
|
||||
Ok(EpiphanyJobLaunchResult {
|
||||
epiphany_state: next_state,
|
||||
binding_id: request.binding_id.clone(),
|
||||
launcher_job_id: plan.launcher_job_id.clone(),
|
||||
backend_job_id: plan.backend_job_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn plan_coordinator_job_launch(
|
||||
state: &EpiphanyThreadState,
|
||||
request: &EpiphanyJobLaunchRequest,
|
||||
|
|
@ -113,6 +199,17 @@ fn terminal_runtime_link_for_binding(
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest {
|
||||
build_epiphany_role_launch_request(
|
||||
"thread-1",
|
||||
EpiphanyRoleResultRoleId::Research,
|
||||
Some(state.revision),
|
||||
Some(60),
|
||||
state,
|
||||
)
|
||||
.expect("research launch request")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_revision_refuses_launch_before_runtime_writes() {
|
||||
let state = EpiphanyThreadState {
|
||||
|
|
@ -137,4 +234,122 @@ mod tests {
|
|||
.expect_err("stale launch must fail");
|
||||
assert!(error.to_string().contains("revision mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_documents_state_and_grant_share_one_snapshot() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("launch.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let request = research_launch(&state);
|
||||
let plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
&request,
|
||||
&store,
|
||||
"launcher-1".to_string(),
|
||||
"backend-1".to_string(),
|
||||
)?;
|
||||
let result = commit_coordinator_job_launch(
|
||||
&store,
|
||||
"thread-1",
|
||||
&state,
|
||||
&request,
|
||||
&plan,
|
||||
"2026-07-11T00:00:00Z".to_string(),
|
||||
)?;
|
||||
assert_eq!(result.epiphany_state.revision, 1);
|
||||
let cache = coordinator_acceptance_cache(&store)?;
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeSession>("epiphany-main")?
|
||||
.is_some()
|
||||
);
|
||||
assert!(cache.get::<EpiphanyRuntimeJob>("backend-1")?.is_some());
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeWorkerLaunchRequest>("backend-1")?
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeEvent>("event-job-opened-backend-1")?
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<SubstrateGateRepoAccessGrantReceipt>("substrate-grant-backend-1")?
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
cache
|
||||
.get_required::<EpiphanyThreadStateEntry>(THREAD_STATE_KEY)?
|
||||
.state()?,
|
||||
result.epiphany_state
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_batch_preserves_prior_snapshot_without_partial_launch() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("launch-refused.cc");
|
||||
let state = EpiphanyThreadState::default();
|
||||
let mut cache = coordinator_acceptance_cache(&store)?;
|
||||
let initial = EpiphanyThreadStateEntry::from_state("thread-1", &state)?;
|
||||
cache.put(THREAD_STATE_KEY, &initial)?;
|
||||
let request = research_launch(&state);
|
||||
let plan = plan_coordinator_job_launch(
|
||||
&state,
|
||||
&request,
|
||||
&store,
|
||||
"launcher-refused".to_string(),
|
||||
"backend-refused".to_string(),
|
||||
)?;
|
||||
let poison = CultCacheEnvelope {
|
||||
key: "injected-refusal".to_string(),
|
||||
r#type: "unregistered.injected_failure".to_string(),
|
||||
payload: Vec::new(),
|
||||
stored_at: "2026-07-11T00:00:00Z".to_string(),
|
||||
schema_id: None,
|
||||
};
|
||||
assert!(
|
||||
commit_coordinator_job_launch_in_cache(
|
||||
&mut cache,
|
||||
"thread-1",
|
||||
&state,
|
||||
&request,
|
||||
&plan,
|
||||
"2026-07-11T00:00:00Z".to_string(),
|
||||
vec![poison],
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let cache = coordinator_acceptance_cache(&store)?;
|
||||
assert_eq!(
|
||||
cache
|
||||
.get_required::<EpiphanyThreadStateEntry>(THREAD_STATE_KEY)?
|
||||
.state()?,
|
||||
state
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeJob>("backend-refused")?
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<EpiphanyRuntimeWorkerLaunchRequest>("backend-refused")?
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.get::<SubstrateGateRepoAccessGrantReceipt>("substrate-grant-backend-refused")?
|
||||
.is_none()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ pub use coordinator_acceptance::reorient_acceptance_claimed_effects;
|
|||
pub use coordinator_acceptance::role_acceptance_claimed_effects;
|
||||
pub use coordinator_acceptance::role_acceptance_enforceable_receipts;
|
||||
pub use coordinator_launch::EpiphanyCoordinatorJobLaunchPlan;
|
||||
pub use coordinator_launch::commit_coordinator_job_launch;
|
||||
pub use coordinator_launch::plan_coordinator_job_launch;
|
||||
pub use coordinator_results::EpiphanyCoordinatorReorientResultSnapshot;
|
||||
pub use coordinator_results::EpiphanyCoordinatorRoleResultSnapshot;
|
||||
|
|
@ -719,6 +720,7 @@ pub use runtime_spine::EpiphanyRuntimeSessionStatus;
|
|||
pub use runtime_spine::EpiphanyRuntimeSpineStatus;
|
||||
pub use runtime_spine::EpiphanyRuntimeWorkerLaunchRequest;
|
||||
pub use runtime_spine::EpiphanyToolInvocationStatus;
|
||||
pub use runtime_spine::PreparedRuntimeSpineHeartbeatJob;
|
||||
pub use runtime_spine::RUNTIME_EVENT_TYPE;
|
||||
pub use runtime_spine::RUNTIME_IDENTITY_KEY;
|
||||
pub use runtime_spine::RUNTIME_IDENTITY_TYPE;
|
||||
|
|
@ -750,6 +752,7 @@ pub use runtime_spine::ensure_runtime_session;
|
|||
pub use runtime_spine::initialize_runtime_spine;
|
||||
pub use runtime_spine::open_runtime_spine_heartbeat_job;
|
||||
pub use runtime_spine::plan_runtime_spine_heartbeat_launch;
|
||||
pub use runtime_spine::prepare_runtime_spine_heartbeat_job;
|
||||
pub use runtime_spine::put_continuity_recovery_receipt;
|
||||
pub use runtime_spine::put_coordinator_run_receipt;
|
||||
pub use runtime_spine::put_eyes_evidence_packet;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ use anyhow::Context;
|
|||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use cultcache_rs::CultCache;
|
||||
use cultcache_rs::CultCacheEnvelope;
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use cultcache_rs::SingleFileMessagePackBackingStore;
|
||||
use cultnet_rs::CultNetDocumentMutationContract;
|
||||
|
|
@ -632,6 +633,12 @@ pub struct RuntimeSpineHeartbeatJobOptions {
|
|||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PreparedRuntimeSpineHeartbeatJob {
|
||||
pub job: EpiphanyRuntimeJob,
|
||||
pub envelopes: Vec<CultCacheEnvelope>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RuntimeSpineHeartbeatLaunchPlanOptions {
|
||||
pub binding_id: String,
|
||||
|
|
@ -1050,6 +1057,136 @@ pub fn open_runtime_spine_heartbeat_job(
|
|||
Ok(job)
|
||||
}
|
||||
|
||||
pub fn prepare_runtime_spine_heartbeat_job(
|
||||
cache: &CultCache,
|
||||
options: RuntimeSpineHeartbeatJobOptions,
|
||||
) -> Result<PreparedRuntimeSpineHeartbeatJob> {
|
||||
validate_non_empty(&options.runtime_id, "runtime id")?;
|
||||
validate_non_empty(&options.display_name, "display name")?;
|
||||
validate_non_empty(&options.session_id, "session id")?;
|
||||
validate_non_empty(&options.objective, "objective")?;
|
||||
validate_non_empty(&options.job_id, "job id")?;
|
||||
validate_non_empty(&options.role, "role")?;
|
||||
validate_non_empty(&options.binding_id, "binding id")?;
|
||||
validate_non_empty(&options.authority_scope, "authority scope")?;
|
||||
validate_non_empty(&options.instruction, "instruction")?;
|
||||
validate_non_empty(
|
||||
options.launch_document.thread_id(),
|
||||
"worker launch document thread id",
|
||||
)?;
|
||||
validate_non_empty(&options.output_contract_id, "output contract id")?;
|
||||
if options.output_contract_id != options.launch_document.output_contract_id() {
|
||||
return Err(anyhow!(
|
||||
"worker launch output_contract_id must match the typed launch document"
|
||||
));
|
||||
}
|
||||
validate_launch_organ_contract(
|
||||
&options.organ_launch_contract,
|
||||
&options.authority_scope,
|
||||
options.launch_document.document_kind(),
|
||||
&options.output_contract_id,
|
||||
)?;
|
||||
validate_non_empty(&options.created_at, "created at")?;
|
||||
|
||||
let existing_identity = cache.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?;
|
||||
let identity = EpiphanyRuntimeIdentity {
|
||||
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: options.runtime_id,
|
||||
display_name: options.display_name,
|
||||
runtime_kind: "epiphany.native".to_string(),
|
||||
created_at: existing_identity
|
||||
.as_ref()
|
||||
.map(|value| value.created_at.clone())
|
||||
.unwrap_or_else(|| options.created_at.clone()),
|
||||
updated_at: options.created_at.clone(),
|
||||
supported_document_types: supported_runtime_document_types(),
|
||||
metadata: BTreeMap::from([("codexEvacuationBridge".to_string(), "temporary".to_string())]),
|
||||
};
|
||||
let session = match cache.get::<EpiphanyRuntimeSession>(&options.session_id)? {
|
||||
Some(existing)
|
||||
if matches!(
|
||||
existing.status,
|
||||
EpiphanyRuntimeSessionStatus::Completed | EpiphanyRuntimeSessionStatus::Archived
|
||||
) =>
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"runtime session {:?} is terminal and cannot accept jobs",
|
||||
options.session_id
|
||||
));
|
||||
}
|
||||
Some(existing) => existing,
|
||||
None => EpiphanyRuntimeSession {
|
||||
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
||||
session_id: options.session_id.clone(),
|
||||
objective: options.objective,
|
||||
status: EpiphanyRuntimeSessionStatus::Active,
|
||||
created_at: options.created_at.clone(),
|
||||
updated_at: options.created_at.clone(),
|
||||
coordinator_note: options.coordinator_note,
|
||||
metadata: BTreeMap::new(),
|
||||
},
|
||||
};
|
||||
if cache.get::<EpiphanyRuntimeJob>(&options.job_id)?.is_some() {
|
||||
return Err(anyhow!("runtime job {:?} already exists", options.job_id));
|
||||
}
|
||||
if cache
|
||||
.get::<EpiphanyRuntimeWorkerLaunchRequest>(&options.job_id)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"runtime worker launch request {:?} already exists",
|
||||
options.job_id
|
||||
));
|
||||
}
|
||||
let job = EpiphanyRuntimeJob {
|
||||
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
||||
job_id: options.job_id.clone(),
|
||||
session_id: options.session_id.clone(),
|
||||
role: options.role.clone(),
|
||||
status: EpiphanyRuntimeJobStatus::Queued,
|
||||
created_at: options.created_at.clone(),
|
||||
updated_at: options.created_at.clone(),
|
||||
summary: format!(
|
||||
"Heartbeat activation queued for binding {} with authority {}.",
|
||||
options.binding_id, options.authority_scope
|
||||
),
|
||||
artifact_refs: Vec::new(),
|
||||
metadata: BTreeMap::new(),
|
||||
};
|
||||
let event = EpiphanyRuntimeEvent {
|
||||
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
|
||||
event_id: format!("event-job-opened-{}", options.job_id),
|
||||
occurred_at: options.created_at,
|
||||
event_type: "job.opened".to_string(),
|
||||
source: "runtime-spine".to_string(),
|
||||
session_id: Some(options.session_id),
|
||||
job_id: Some(options.job_id.clone()),
|
||||
summary: "Native runtime job opened.".to_string(),
|
||||
metadata: BTreeMap::new(),
|
||||
};
|
||||
let request = EpiphanyRuntimeWorkerLaunchRequest {
|
||||
schema_version: RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.to_string(),
|
||||
job_id: options.job_id.clone(),
|
||||
binding_id: options.binding_id,
|
||||
role: options.role,
|
||||
authority_scope: options.authority_scope,
|
||||
instruction: options.instruction,
|
||||
output_contract_id: options.output_contract_id,
|
||||
document_kind: worker_launch_document_kind(&options.launch_document).to_string(),
|
||||
launch_document_msgpack: encode_worker_launch_document(&options.launch_document)?,
|
||||
metadata: BTreeMap::new(),
|
||||
organ_launch_contract: options.organ_launch_contract,
|
||||
};
|
||||
let envelopes = vec![
|
||||
cache.prepare_entry(RUNTIME_IDENTITY_KEY, &identity)?.0,
|
||||
cache.prepare_entry(&session.session_id, &session)?.0,
|
||||
cache.prepare_entry(&job.job_id, &job)?.0,
|
||||
cache.prepare_entry(&event.event_id, &event)?.0,
|
||||
cache.prepare_entry(&request.job_id, &request)?.0,
|
||||
];
|
||||
Ok(PreparedRuntimeSpineHeartbeatJob { job, envelopes })
|
||||
}
|
||||
|
||||
pub fn runtime_job_snapshot(
|
||||
store_path: impl AsRef<Path>,
|
||||
job_id: &str,
|
||||
|
|
|
|||
|
|
@ -1820,15 +1820,16 @@ proof `.epiphany-smoke\repo-work-readiness-20260621-113714` and wrapper proof
|
|||
The machine is no longer cleared to move outward. Free it from the Codex organ
|
||||
first.
|
||||
|
||||
Native worker-launch planning has been extracted into
|
||||
`epiphany-core/src/coordinator_launch.rs`. It owns revision refusal, prior
|
||||
terminal-link discovery, heartbeat launch planning, binding replacement, and
|
||||
the derived state update. The bridge delegates those decisions and a source
|
||||
guard prevents the policy from growing back. The remaining launch wound is
|
||||
transactional: runtime documents, the optional Substrate Gate grant, and
|
||||
thread-state linkage are still separate compatibility writes. The next cut is
|
||||
one prepared atomic launch commit with injected-failure proof; do not remove
|
||||
the coordinator JSON-RPC dependency before that invariant is real.
|
||||
Native worker launch is now one transaction. `coordinator_launch` owns revision
|
||||
refusal, prior terminal-link discovery, heartbeat planning, binding replacement,
|
||||
derived state, and atomic publication of runtime identity/session/job/event,
|
||||
worker request, optional Research Substrate Gate grant, and thread state. The
|
||||
bridge delegates planning and commit; its former runtime/grant/state writers are
|
||||
deleted and guarded against regrowth. Focused proof reads every document from
|
||||
the shared snapshot, while an injected batch refusal preserves the prior state
|
||||
with no partial job, worker request, or grant. The next cut is direct native
|
||||
`epiphany-mvp-coordinator` operation and removal of its `thread/epiphany/*`
|
||||
JSON-RPC dependency.
|
||||
|
||||
## Immediate Re-entry Instruction
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ interruption. Best-effort rollback across two files is not atomicity. A negative
|
|||
smoke must inject failure after prerequisite receipts and after state write and
|
||||
prove no unwitnessed accepted state becomes authoritative.
|
||||
|
||||
## Launch Transaction Wound
|
||||
## Launch Transaction Law
|
||||
|
||||
Launch planning now belongs to `coordinator_launch`: it validates the expected
|
||||
thread revision, inspects the prior runtime binding, prepares the heartbeat
|
||||
|
|
@ -178,13 +178,13 @@ launch documents, and derives the new thread-state linkage without writing.
|
|||
The Codex bridge is a compatibility actuator for that plan; it no longer owns
|
||||
those policy decisions.
|
||||
|
||||
Persistence is not yet a coherent launch transaction. Runtime identity,
|
||||
session, job, event, worker request, optional Substrate Gate grant, and thread
|
||||
state linkage are still published through separate writes. None may become the
|
||||
authoritative sign of a completed launch until one native commit primitive
|
||||
publishes the prepared set atomically, or a typed prepared transaction gives
|
||||
interruption a deterministic finish-or-refuse law. Rollback and reconciliation
|
||||
are not owners.
|
||||
`commit_coordinator_job_launch` is the single launch publication owner. It
|
||||
prepares runtime identity, session, job, opened event, worker request, optional
|
||||
Research Substrate Gate grant, and linked thread state against one loaded
|
||||
CultCache snapshot, then publishes the heterogeneous set in one atomic batch.
|
||||
The compatibility bridge cannot write any member separately. Injected batch
|
||||
refusal proves the prior snapshot survives with no job, worker request, grant,
|
||||
or new state linkage. Rollback and reconciliation are not owners.
|
||||
|
||||
## Invariants
|
||||
|
||||
|
|
|
|||
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: Complete the native launch transaction described in `notes/native-coordinator-liberation-map.md`. Pure launch planning now belongs to `coordinator_launch`; the compatibility bridge only actuates its plan, but runtime documents, optional Substrate Gate grant, and thread-state linkage remain separate writes. Create one atomic prepared launch commit and negative failure proof before removing `thread/epiphany/*` JSON-RPC from `epiphany-mvp-coordinator`.
|
||||
next_action: Remove `thread/epiphany/*` JSON-RPC from `epiphany-mvp-coordinator` by wiring it directly to the native coordinator service. Launch and acceptance now each have one native atomic CultCache commit; the compatibility bridge delegates both and is source-guarded against regrowing individual writers.
|
||||
|
|
|
|||
Reference in a new issue