Move launch planning into core

This commit is contained in:
Meta Vens 2026-07-11 20:16:28 +01:00
commit e27d0dfcfe
7 changed files with 205 additions and 105 deletions

View file

@ -26,7 +26,6 @@ use epiphany_core::EpiphanyStateUpdatedField;
use epiphany_core::EpiphanyTokenUsageSnapshot;
use epiphany_core::MIND_GATEWAY_REVIEW_TYPE;
use epiphany_core::RuntimeSpineHeartbeatJobOptions;
use epiphany_core::RuntimeSpineHeartbeatLaunchPlanOptions;
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;
@ -35,22 +34,17 @@ use epiphany_core::build_epiphany_role_launch_request_with_dynamic_context;
use epiphany_core::clear_epiphany_job_binding_backend;
use epiphany_core::continuity_recovery_receipt_from_reorient_finding;
use epiphany_core::epiphany_role_label;
use epiphany_core::epiphany_state_update_validation_errors;
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::plan_runtime_spine_heartbeat_launch;
use epiphany_core::put_substrate_gate_repo_access_grant_receipt;
use epiphany_core::replace_or_append_epiphany_job_binding;
use epiphany_core::runtime_job_snapshot;
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;
use epiphany_state_model::EpiphanyRuntimeLink;
use epiphany_state_model::EpiphanyThreadState;
use crate::cultnet::EpiphanySurfaceSource;
@ -210,125 +204,38 @@ pub async fn launch_epiphany_job_on_thread(
// runtime-spine opens the typed job. The bridge stitches those typed
// documents into Codex's JSON-RPC thread shell until CultNet owns the route.
let current_state = thread.epiphany_state().await.unwrap_or_default();
validate_expected_revision(request.expected_revision, current_state.revision)?;
let runtime_store = thread.epiphany_runtime_spine_store_path().await;
let completed_prior_link = terminal_runtime_link_for_binding(
let launch_plan = epiphany_core::plan_coordinator_job_launch(
&current_state,
request.binding_id.as_str(),
&request,
runtime_store.as_path(),
)?;
let mut planning_state = current_state.clone();
if let Some(link) = completed_prior_link.clone() {
planning_state.runtime_links.insert(0, link);
}
let launcher_job_id = format!("epiphany-heartbeat-launch-{}", Uuid::new_v4());
let backend_job_id = Uuid::new_v4().to_string();
let launch_plan = plan_runtime_spine_heartbeat_launch(
&planning_state,
RuntimeSpineHeartbeatLaunchPlanOptions {
binding_id: request.binding_id.clone(),
kind: request.kind,
scope: request.scope.clone(),
owner_role: request.owner_role.clone(),
authority_scope: request.authority_scope.clone(),
linked_subgoal_ids: request.linked_subgoal_ids.clone(),
linked_graph_node_ids: request.linked_graph_node_ids.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(),
max_runtime_seconds: request.max_runtime_seconds,
runtime_job_id: backend_job_id.clone(),
},
format!("epiphany-heartbeat-launch-{}", Uuid::new_v4()),
Uuid::new_v4().to_string(),
)
.map_err(|err| EpiphanyBridgeError::InvalidRequest(err.to_string()))?;
let next_job_bindings = replace_or_append_epiphany_job_binding(
current_state.job_bindings.clone(),
launch_plan.binding,
);
let mut runtime_links = Vec::new();
runtime_links.push(launch_plan.runtime_link.clone());
if let Some(link) = completed_prior_link {
runtime_links.push(link);
}
let validation_errors = epiphany_state_update_validation_errors(
&current_state,
&EpiphanyStateUpdate {
job_bindings: Some(next_job_bindings.clone()),
runtime_links: runtime_links.clone(),
..Default::default()
},
);
if !validation_errors.is_empty() {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"invalid Epiphany job launch patch: {}",
validation_errors.join("; ")
)));
}
open_epiphany_runtime_spine_job(
runtime_store.as_path(),
&planning_state,
&launch_plan.planning_state,
&request,
backend_job_id.as_str(),
launch_plan.backend_job_id.as_str(),
)?;
maybe_put_substrate_gate_launch_grant(
runtime_store.as_path(),
&request,
backend_job_id.as_str(),
launch_plan.backend_job_id.as_str(),
)?;
let epiphany_state = apply_epiphany_state_update_to_thread(
thread,
EpiphanyStateUpdate {
expected_revision: request.expected_revision,
job_bindings: Some(next_job_bindings),
runtime_links,
..Default::default()
},
)
.await?;
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,
backend_job_id,
launcher_job_id: launch_plan.launcher_job_id,
backend_job_id: launch_plan.backend_job_id,
})
}
fn terminal_runtime_link_for_binding(
state: &EpiphanyThreadState,
binding_id: &str,
runtime_store: &Path,
) -> BridgeResult<Option<EpiphanyRuntimeLink>> {
let Some(link) = state
.runtime_links
.iter()
.find(|link| link.binding_id == binding_id && !link.runtime_job_id.trim().is_empty())
else {
return Ok(None);
};
if link.runtime_result_id.is_some() {
return Ok(None);
}
let Some(snapshot) = runtime_job_snapshot(runtime_store, link.runtime_job_id.as_str())
.map_err(|err| EpiphanyBridgeError::Fatal(err.to_string()))?
else {
return Ok(None);
};
let Some(result) = snapshot.result else {
return Ok(None);
};
let mut terminal = link.clone();
terminal.id = format!("{}-{}", link.id, result.result_id);
terminal.surface = "runtimeResult".to_string();
terminal.runtime_result_id = Some(result.result_id);
Ok(Some(terminal))
}
pub async fn interrupt_epiphany_job_on_thread(
thread: &impl EpiphanyMutationHost,
request: EpiphanyJobInterruptRequest,
@ -1002,6 +909,30 @@ fn map_coordinator_admission_error(
#[cfg(test)]
mod acceptance_architecture_tests {
#[test]
fn launch_route_delegates_planning_to_the_native_organ() {
let source = include_str!("mutation_service.rs");
let launch_start = source
.find("pub async fn launch_epiphany_job_on_thread")
.unwrap();
let interrupt_start = source
.find("pub async fn interrupt_epiphany_job_on_thread")
.unwrap();
let launch = &source[launch_start..interrupt_start];
assert_eq!(launch.matches("plan_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("_"),
] {
assert!(
!launch.contains(&forbidden),
"launch route regrew native planning policy {forbidden:?}"
);
}
}
#[test]
fn acceptance_routes_have_no_individual_or_post_state_receipt_writes() {
let source = include_str!("mutation_service.rs");

View file

@ -0,0 +1,140 @@
use crate::*;
use anyhow::{Result, anyhow};
use epiphany_state_model::{EpiphanyRuntimeLink, EpiphanyThreadState};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct EpiphanyCoordinatorJobLaunchPlan {
pub launcher_job_id: String,
pub backend_job_id: String,
pub planning_state: EpiphanyThreadState,
pub heartbeat_plan: RuntimeSpineHeartbeatLaunchPlan,
pub state_update: EpiphanyStateUpdate,
}
pub fn plan_coordinator_job_launch(
state: &EpiphanyThreadState,
request: &EpiphanyJobLaunchRequest,
runtime_store: &Path,
launcher_job_id: String,
backend_job_id: String,
) -> Result<EpiphanyCoordinatorJobLaunchPlan> {
if let Some(expected) = request.expected_revision
&& expected != state.revision
{
return Err(anyhow!(
"epiphany state revision mismatch: expected {expected}, found {}",
state.revision
));
}
let completed_prior_link =
terminal_runtime_link_for_binding(state, &request.binding_id, runtime_store)?;
let mut planning_state = state.clone();
if let Some(link) = completed_prior_link.clone() {
planning_state.runtime_links.insert(0, link);
}
let heartbeat_plan = plan_runtime_spine_heartbeat_launch(
&planning_state,
RuntimeSpineHeartbeatLaunchPlanOptions {
binding_id: request.binding_id.clone(),
kind: request.kind,
scope: request.scope.clone(),
owner_role: request.owner_role.clone(),
authority_scope: request.authority_scope.clone(),
linked_subgoal_ids: request.linked_subgoal_ids.clone(),
linked_graph_node_ids: request.linked_graph_node_ids.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(),
max_runtime_seconds: request.max_runtime_seconds,
runtime_job_id: backend_job_id.clone(),
},
)?;
let job_bindings = replace_or_append_epiphany_job_binding(
state.job_bindings.clone(),
heartbeat_plan.binding.clone(),
);
let mut runtime_links = vec![heartbeat_plan.runtime_link.clone()];
if let Some(link) = completed_prior_link {
runtime_links.push(link);
}
let state_update = EpiphanyStateUpdate {
expected_revision: request.expected_revision,
job_bindings: Some(job_bindings),
runtime_links,
..Default::default()
};
let errors = epiphany_state_update_validation_errors(state, &state_update);
if !errors.is_empty() {
return Err(anyhow!(
"invalid Epiphany job launch patch: {}",
errors.join("; ")
));
}
Ok(EpiphanyCoordinatorJobLaunchPlan {
launcher_job_id,
backend_job_id,
planning_state,
heartbeat_plan,
state_update,
})
}
fn terminal_runtime_link_for_binding(
state: &EpiphanyThreadState,
binding_id: &str,
runtime_store: &Path,
) -> Result<Option<EpiphanyRuntimeLink>> {
let Some(link) = state
.runtime_links
.iter()
.find(|link| link.binding_id == binding_id && !link.runtime_job_id.trim().is_empty())
else {
return Ok(None);
};
if link.runtime_result_id.is_some() {
return Ok(None);
}
let Some(snapshot) = runtime_job_snapshot(runtime_store, &link.runtime_job_id)? else {
return Ok(None);
};
let Some(result) = snapshot.result else {
return Ok(None);
};
let mut terminal = link.clone();
terminal.id = format!("{}-{}", link.id, result.result_id);
terminal.surface = "runtimeResult".to_string();
terminal.runtime_result_id = Some(result.result_id);
Ok(Some(terminal))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stale_revision_refuses_launch_before_runtime_writes() {
let state = EpiphanyThreadState {
revision: 2,
..Default::default()
};
let request = build_epiphany_role_launch_request(
"thread-1",
EpiphanyRoleResultRoleId::Modeling,
Some(1),
Some(60),
&state,
)
.expect("role launch request");
let error = plan_coordinator_job_launch(
&state,
&request,
Path::new("missing.cc"),
"launcher".to_string(),
"backend".to_string(),
)
.expect_err("stale launch must fail");
assert!(error.to_string().contains("revision mismatch"));
}
}

View file

@ -3,6 +3,7 @@ mod agent_memory;
mod agent_utterance_state;
mod continuity_gateway;
mod coordinator_acceptance;
mod coordinator_launch;
mod coordinator_results;
mod coordinator_service;
mod coordinator_state;
@ -139,6 +140,8 @@ pub use coordinator_acceptance::read_accepted_coordinator_state;
pub use coordinator_acceptance::reorient_acceptance_claimed_effects;
pub use coordinator_acceptance::role_acceptance_claimed_effects;
pub use coordinator_acceptance::role_acceptance_enforceable_receipts;
pub use coordinator_launch::EpiphanyCoordinatorJobLaunchPlan;
pub use coordinator_launch::plan_coordinator_job_launch;
pub use coordinator_results::EpiphanyCoordinatorReorientResultSnapshot;
pub use coordinator_results::EpiphanyCoordinatorRoleResultSnapshot;
pub use coordinator_results::read_reorient_result_snapshot;

View file

@ -1820,6 +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.
## Immediate Re-entry Instruction
After compaction, first rehydrate and reorient from the listed files and git

View file

@ -170,6 +170,22 @@ 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 planning now belongs to `coordinator_launch`: it validates the expected
thread revision, inspects the prior runtime binding, prepares the heartbeat
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.
## Invariants
- There is one durable thread-state owner and one runtime lifecycle owner.

Binary file not shown.

View file

@ -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: Extract worker launch into its own native organ, following `notes/native-coordinator-liberation-map.md`. Role/reorient acceptance now publishes state and every witness atomically through the unified store; obsolete cross-store writers are deleted and guarded. Move typed dynamic context, launch document construction, runtime-spine opening/interruption, and state linkage into the launch organ, then remove `thread/epiphany/*` JSON-RPC from `epiphany-mvp-coordinator`.
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`.