From 4d0db2c7b439c62ab50ceb6d1b63d86397e5e31f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 23 Aug 2026 06:21:38 +0200 Subject: [PATCH] Delete the heartbeat scheduler authority --- Cargo.toml | 4 - docs/epiphany_body_whitepaper.tex | 13 +- epiphany-core/src/agent_launch.rs | 9 - .../src/bin/epiphany-daemon-supervisor.rs | 77 +- .../src/bin/epiphany-heartbeat-store.rs | 601 -------- .../src/bin/epiphany-mvp-coordinator.rs | 60 - epiphany-core/src/bin/epiphany-mvp-status.rs | 16 - epiphany-core/src/bin/epiphany-swarm.rs | 143 +- epiphany-core/src/cultmesh_integration.rs | 18 - epiphany-core/src/current_work.rs | 24 +- epiphany-core/src/heartbeat_state.rs | 1210 ----------------- .../heartbeat_state/heartbeat_documents.rs | 264 ---- .../heartbeat_state/heartbeat_projection.rs | 275 ---- .../heartbeat_state/heartbeat_retention.rs | 472 ------- .../src/heartbeat_state/heartbeat_roles.rs | 119 -- .../src/heartbeat_state/heartbeat_store.rs | 330 ----- epiphany-core/src/lib.rs | 38 - epiphany-core/src/reorientation_work.rs | 20 - epiphany-core/src/resident_readiness.rs | 220 +-- epiphany-core/src/resident_self.rs | 372 ++--- epiphany-core/src/runtime_spine.rs | 16 - .../src/semantic_projector_service.rs | 13 - .../src/surfaces/coordinator_decision.rs | 19 - epiphany-core/src/surfaces/jobs.rs | 15 - .../src/surfaces/research_decision.rs | 16 - epiphany-core/src/surfaces/role_board.rs | 18 - epiphany-release-construction/src/lib.rs | 7 - notes/epiphany-current-algorithmic-map.md | 29 +- notes/fresh-workspace-handoff.md | 30 +- schemas/README.md | 6 +- .../epiphany.agent-heartbeat.schema.json | 50 - schemas/cultnet/index.json | 11 - schemas/heartbeat-state-schema.md | 41 - state/ledgers.msgpack | Bin 655046 -> 655520 bytes state/map.yaml | 10 +- state/scratch.md | 17 +- 36 files changed, 280 insertions(+), 4303 deletions(-) delete mode 100644 epiphany-core/src/bin/epiphany-heartbeat-store.rs delete mode 100644 epiphany-core/src/heartbeat_state.rs delete mode 100644 epiphany-core/src/heartbeat_state/heartbeat_documents.rs delete mode 100644 epiphany-core/src/heartbeat_state/heartbeat_projection.rs delete mode 100644 epiphany-core/src/heartbeat_state/heartbeat_retention.rs delete mode 100644 epiphany-core/src/heartbeat_state/heartbeat_roles.rs delete mode 100644 epiphany-core/src/heartbeat_state/heartbeat_store.rs delete mode 100644 schemas/cultnet/epiphany.agent-heartbeat.schema.json delete mode 100644 schemas/heartbeat-state-schema.md diff --git a/Cargo.toml b/Cargo.toml index 4fade533..bf8dbc82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,10 +131,6 @@ path = "epiphany-core/src/bin/epiphany-host-identity.rs" name = "epiphany-swarm" path = "epiphany-core/src/bin/epiphany-swarm.rs" -[[bin]] -name = "epiphany-heartbeat-store" -path = "epiphany-core/src/bin/epiphany-heartbeat-store.rs" - [[bin]] name = "epiphany-runtime-spine" path = "epiphany-core/src/bin/epiphany-runtime-spine.rs" diff --git a/docs/epiphany_body_whitepaper.tex b/docs/epiphany_body_whitepaper.tex index 92ca3147..4deeda44 100644 --- a/docs/epiphany_body_whitepaper.tex +++ b/docs/epiphany_body_whitepaper.tex @@ -73,7 +73,7 @@ Domain policy & \code{epiphany-core} & Verdicts, launch policy, acceptance rules Vendored Codex & \code{vendor/codex} & Retain host/app-server/auth/model compatibility where useful; do not let Codex own Epiphany prompts, scheduler, or state. \\ Runtime spine & \code{epiphany-core/src/runtime_spine.rs} and \code{state/runtime-spine.cc} & Runtime identity, jobs, events, receipts, tool/model documents are typed CultCache records. \\ Persona memory & keyed Mind Persona-memory documents & Persona decisions admit bounded memories by semantic identity; no parallel role-memory aggregate or generation head exists. \\ -Heartbeat physiology & \code{state/agent-heartbeats.cc} via \code{epiphany-heartbeat-store} & Scheduling, cooldown, explicit pressure, and active-turn exclusion only. Heartbeat owns no cognition, personality, or memory mutation. \\ +Resident Self physiology & Resident Self CultCache documents via \code{epiphany-swarm} & Typed pressure, exact grant creation, launch exclusion, leases, terminal receipts, retry, and bounded retention have one owner. No scheduler singleton or acknowledgement loop exists. \\ Local Verse & CultMesh over \code{state/local-verse.cc} & Local Verse context is compact prompt/operator context; private state does not become public by flipping a flag. \\ Schema catalog & \code{schemas/cultnet/index.json} & JSON schema publishes contracts; typed Rust documents remain runtime authority. \\ Operator artifacts & \code{.epiphany-*}, \code{tools/epiphany_local_run.ps1} & Artifacts witness and lower state; they do not become state owners. \\ @@ -85,7 +85,7 @@ The checkout has nine primary Epiphany Rust crates: \begin{itemize} \item \code{epiphany-core}: the main domain organ. It owns state-update - validation, launch documents, runtime spine, heartbeat, memory graph, Persona + validation, launch documents, runtime spine, Resident Self, Persona projection, Substrate Gate, Mind, Eyes, Hands, Soul, Continuity, CultMesh integration, surface derivation, smokes, and local operator binaries. \item \code{epiphany-state-model}: the durable \code{EpiphanyThreadState} @@ -133,10 +133,9 @@ implementation, not file authority. \hline \code{state/map.yaml} & Canonical slow project map: objective, constraints, invariants, dataflow, active subgoals, current status, guardrails, and rejected paths. \\ \code{state/ledgers.cc} & CultCache-shaped evidence and branch ledger. Future behavior-changing lessons go here, not in an endless activity feed. \\ -\code{state/agents.cc} & Organ self-memory and Persona projection source. Resident work organs use \code{work_organ}; public/person-shaped agents use \code{persona}. \\ -\code{state/agent-heartbeats.cc} & Heartbeat initiative, cooldown, pending turns, cognition lanes, routine physiology, and scheduler pressure. \\ -\code{state/runtime-spine.cc} & Runtime identity, sessions, jobs, job results, events, worker launch requests, role/reorient findings, receipts, model/tool documents, the admitted RepoModel, and schema catalog surfaces. \\ -\code{state/thread-state.cc} & Native thread-state store. \\ +Resident Self store & Keyed pressure, grants, prepared and active launch authority, terminal receipts, and retention head. \\ +Persona social store & Keyed mentions, immutable turn requests, terminal receipts, quarantine records, and retention plans. \\ +\code{state/runtime-spine.cc} & Runtime identity, sessions, jobs, results, worker launch requests, receipts, and model/tool transport documents. Durable decisions and keyed RepoModel documents live in Mind. \\ \code{state/local-verse.cc} & Local CultMesh Verse context for compact operator and prompt-context packets. \\ \code{state/semantic-projection.cc} & Typed Qdrant projection receipts. Cache health lives here, outside the canonical RepoModel. \\ \code{state/persona-discord.toml} & Persona Discord boundary configuration. \\ @@ -260,7 +259,7 @@ Current operator access is partly native and partly witness-artifact based: \begin{itemize} \item Native binaries such as \code{epiphany-state}, \code{epiphany-mvp-coordinator}, \code{epiphany-runtime-spine}, - \code{epiphany-heartbeat-store}, and the family-specific worker services + \code{epiphany-swarm}, and the family-specific worker services inspect or update their owned typed state. \item Local artifact directories such as \code{.epiphany-run/}, \code{.epiphany-dogfood/}, \code{.epiphany-smoke/}, diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index a48de283..9f8683da 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -998,15 +998,6 @@ mod tests { ); } - #[test] - fn live_worker_contracts_contain_no_codex_route_vocabulary() { - let source = include_str!("agent_launch.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - assert!(!production.contains("thread/epiphany/")); - assert!(production.contains("epiphany.coordinator.reorient.launch")); - assert!(production.contains("researchDecision")); - } - #[test] fn bundled_epiphany_agent_prompts_do_not_name_codex_as_prompt_authority() { let prompts = epiphany_specialist_prompt_config(); diff --git a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs index 1dd54895..8a161134 100644 --- a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs +++ b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs @@ -21,7 +21,7 @@ use epiphany_core::EpiphanyCultMeshSwarmBrakeEntry; use epiphany_core::EpiphanyLocalVerseContext; use epiphany_core::EpiphanyProcessObservation as ProcessObservation; use epiphany_core::authenticate_epiphany_cultmesh_semantic_projector_launch; -use epiphany_core::authenticate_resident_provider_pair; +use epiphany_core::authenticate_resident_provider; use epiphany_core::epiphany_cultmesh_daemon_poke_intent_from_status; use epiphany_core::epiphany_cultmesh_daemon_poke_receipt_for_intent; use epiphany_core::idunn_recover_memory_semantic_projection_from_cultmesh; @@ -934,25 +934,17 @@ fn publish_managed_service_iteration_health( let mut terminal_current = 0_usize; let mut warming = 0_usize; let mut contradictions = Vec::new(); - if let (Some(heartbeat_store), Some(resident_store)) = ( - args.resident_heartbeat_store.as_deref(), - args.resident_self_store.as_deref(), - ) { - expected += 2; - let pair = authenticate_resident_provider_pair( + if let Some(resident_store) = args.resident_self_store.as_deref() { + expected += 1; + let provider = authenticate_resident_provider( release, authenticated_release_witness_sha256, - heartbeat_store, resident_store, args.resident_provider_stale_seconds.saturating_mul(1000), ); - terminal_current += pair.terminal_current; - warming += pair.warming; - contradictions.extend(pair.contradictions); - } else if args.resident_heartbeat_store.is_some() || args.resident_self_store.is_some() { - expected += 2; - contradictions - .push("resident health requires both heartbeat and Self provider stores".into()); + terminal_current += provider.terminal_current; + warming += provider.warming; + contradictions.extend(provider.contradictions); } for &service_id in required { let Some(policy) = policies @@ -3897,17 +3889,6 @@ mod provider_status_ownership_tests { assert_eq!(unresolved_attempt_failure_count(3), 4); } - #[test] - fn supervisor_source_has_no_provider_status_writer() { - let source = include_str!("epiphany-daemon-supervisor.rs"); - - let provider_writer = ["write_epiphany_cultmesh_", "daemon_status"].concat(); - let heartbeat_assignment = ["next_status.", "last_heartbeat_utc"].concat(); - let action_assignment = ["next_status.", "operator_action"].concat(); - assert!(!source.contains(&provider_writer)); - assert!(!source.contains(&heartbeat_assignment)); - assert!(!source.contains(&action_assignment)); - } } #[cfg(test)] @@ -3970,40 +3951,6 @@ mod service_lifecycle_brake_authority_tests { } } - #[test] - fn supervisor_physiology_survives_the_lifecycle_brake_without_reconciliation() { - let source = include_str!("epiphany-daemon-supervisor.rs"); - let start = source.find("fn managed_service_serve(").unwrap(); - let tail = &source[start..]; - let end = tail.find("\nfn ").unwrap(); - let body = &tail[..end]; - let brake = body.find("load_epiphany_cultmesh_swarm_brake").unwrap(); - let guard = body.find("if !lifecycle_braked").unwrap(); - let reconcile = body.find("managed_service_reconcile").unwrap(); - let health = body - .find("publish_managed_service_iteration_health") - .unwrap(); - assert!(brake < guard && guard < reconcile && reconcile < health); - assert!(body.contains("\"reconciledPolicyCount\"")); - } - - #[test] - fn direct_managed_reconcile_checks_the_brake_before_any_lifecycle_mutation() { - let source = include_str!("epiphany-daemon-supervisor.rs"); - let start = source.find("fn managed_service_reconcile(").unwrap(); - let tail = &source[start..]; - let end = tail.find("\nfn ").unwrap(); - let body = &tail[..end]; - let brake = body.find("load_epiphany_cultmesh_swarm_brake").unwrap(); - let gate = body - .find("assert_swarm_brake_allows_service_lifecycle_entry") - .unwrap(); - let policy = body - .find("load_epiphany_cultmesh_managed_service_policy") - .unwrap(); - assert!(brake < gate && gate < policy); - } - #[test] fn daemon_poke_reads_the_narrow_brake_without_weakening_scope() { let daemon = daemon(); @@ -4807,7 +4754,6 @@ struct Args { idunn_deployment_request_id: Option, idunn_provider_health_identity_store: Option, idunn_provider_health_public_anchor: Option, - resident_heartbeat_store: Option, resident_self_store: Option, resident_provider_stale_seconds: u64, } @@ -4865,7 +4811,6 @@ impl Args { let mut idunn_deployment_request_id = None; let mut idunn_provider_health_identity_store = None; let mut idunn_provider_health_public_anchor = None; - let mut resident_heartbeat_store = None; let mut resident_self_store = None; let mut resident_provider_stale_seconds = 180_u64; @@ -5078,13 +5023,6 @@ impl Args { "missing --idunn-provider-health-public-anchor value", )?)); } - "--resident-heartbeat-store" => { - resident_heartbeat_store = Some(PathBuf::from( - values - .next() - .context("missing --resident-heartbeat-store value")?, - )); - } "--resident-self-store" => { resident_self_store = Some(PathBuf::from( values @@ -5273,7 +5211,6 @@ impl Args { idunn_deployment_request_id, idunn_provider_health_identity_store, idunn_provider_health_public_anchor, - resident_heartbeat_store, resident_self_store, resident_provider_stale_seconds, }) diff --git a/epiphany-core/src/bin/epiphany-heartbeat-store.rs b/epiphany-core/src/bin/epiphany-heartbeat-store.rs deleted file mode 100644 index 59af1bab..00000000 --- a/epiphany-core/src/bin/epiphany-heartbeat-store.rs +++ /dev/null @@ -1,601 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use epiphany_core::EpiphanyCultMeshSwarmBrakeEntry; -use epiphany_core::EpiphanyHeartbeatArtifactRetentionReceipt; -use epiphany_core::HeartbeatStaleTurnRepairOptions; -use epiphany_core::heartbeat_local_provider_status; -use epiphany_core::heartbeat_status_projection; -use epiphany_core::initialize_heartbeat_store; -use epiphany_core::load_epiphany_cultmesh_swarm_brake; -use epiphany_core::load_heartbeat_state_entry; -use epiphany_core::pulse_resident_self_heartbeat; -use epiphany_core::recover_stale_heartbeat_store; -use epiphany_core::resident_cognitive_runtime_id; -use epiphany_core::retain_heartbeat_pulse_artifacts; -use epiphany_core::{ - ResidentProviderReadiness, acquire_resident_process_singleton, - authenticate_epiphany_packaged_release, capture_process_instance, - publish_resident_provider_readiness, -}; -use std::env; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; -use std::thread; -use std::time::Duration; - -fn main() -> Result<()> { - let mut args = env::args().skip(1); - let Some(command) = args.next() else { - return usage(); - }; - let mut store_path: Option = None; - let mut artifact_dir: Option = None; - let mut local_verse_store: Option = None; - let mut target_heartbeat_rate = 1.0_f64; - let mut schedule_id = "epiphany-heartbeat".to_string(); - let mut source_scene_ref = "epiphany/coordinator".to_string(); - let mut limit = 8_usize; - let mut resident_self_store: Option = None; - let mut resident_runtime_store: Option = None; - let mut repair_reason: Option = None; - let mut max_age_seconds = 1800_i64; - let mut now_utc: Option = None; - let mut interval_seconds = 120_u64; - let mut max_iterations = 0_u64; - let mut retain_pulse_artifacts = 256_usize; - let mut retention_batch_size = 64_usize; - let mut release_store: Option = None; - let mut release_runtime_id: Option = None; - let mut release_id: Option = None; - let mut release_witness_sha256: Option = None; - - while let Some(arg) = args.next() { - match arg.as_str() { - "--store" => store_path = Some(next_path(&mut args, "--store")?), - "--artifact-dir" => artifact_dir = Some(next_path(&mut args, "--artifact-dir")?), - "--local-verse-store" => { - local_verse_store = Some(next_path(&mut args, "--local-verse-store")?) - } - "--target-heartbeat-rate" => { - target_heartbeat_rate = next_value(&mut args, "--target-heartbeat-rate")?.parse()? - } - "--schedule-id" => schedule_id = next_value(&mut args, "--schedule-id")?, - "--source-scene-ref" => source_scene_ref = next_value(&mut args, "--source-scene-ref")?, - "--limit" => limit = next_value(&mut args, "--limit")?.parse()?, - "--resident-self-store" => { - resident_self_store = Some(next_path(&mut args, "--resident-self-store")?) - } - "--resident-runtime-store" => { - resident_runtime_store = Some(next_path(&mut args, "--resident-runtime-store")?) - } - "--reason" => repair_reason = Some(next_value(&mut args, "--reason")?), - "--max-age-seconds" => { - max_age_seconds = next_value(&mut args, "--max-age-seconds")?.parse()? - } - "--now-utc" => now_utc = Some(next_value(&mut args, "--now-utc")?), - "--interval-seconds" => { - interval_seconds = next_value(&mut args, "--interval-seconds")?.parse()? - } - "--max-iterations" => { - max_iterations = next_value(&mut args, "--max-iterations")?.parse()? - } - "--retain-pulse-artifacts" => { - retain_pulse_artifacts = - next_value(&mut args, "--retain-pulse-artifacts")?.parse()? - } - "--retention-batch-size" => { - retention_batch_size = next_value(&mut args, "--retention-batch-size")?.parse()? - } - "--release-store" => release_store = Some(next_path(&mut args, "--release-store")?), - "--release-runtime-id" => { - release_runtime_id = Some(next_value(&mut args, "--release-runtime-id")?) - } - "--release-id" => release_id = Some(next_value(&mut args, "--release-id")?), - "--release-witness-sha256" => { - release_witness_sha256 = Some(next_value(&mut args, "--release-witness-sha256")?) - } - _ => return Err(anyhow!("unknown argument {arg:?}")), - } - } - - match command.as_str() { - "init" => { - let store_path = store_path.ok_or_else(|| anyhow!("init requires --store"))?; - let state = initialize_heartbeat_store(&store_path, target_heartbeat_rate)?; - println!( - "{}", - serde_json::json!({ - "ok": true, - "command": "init", - "storeFile": store_path, - "schemaVersion": state.schema_version, - "participants": state.participants.len(), - "history": state.history.len(), - }) - ); - } - "repair-stale" => { - let store_path = store_path.ok_or_else(|| anyhow!("repair-stale requires --store"))?; - let artifact_dir = - artifact_dir.ok_or_else(|| anyhow!("repair-stale requires --artifact-dir"))?; - let result = recover_stale_heartbeat_store( - &store_path, - &artifact_dir, - HeartbeatStaleTurnRepairOptions { - max_age_seconds, - now_utc, - reason: repair_reason.unwrap_or_else(|| { - "Operator requested stale heartbeat turn recovery.".to_string() - }), - }, - )?; - println!("{}", result); - } - "retain-artifacts" => { - let store_path = - store_path.ok_or_else(|| anyhow!("retain-artifacts requires --store"))?; - let artifact_dir = - artifact_dir.ok_or_else(|| anyhow!("retain-artifacts requires --artifact-dir"))?; - let receipt = retain_heartbeat_pulse_artifacts( - &store_path, - &artifact_dir, - retain_pulse_artifacts, - retention_batch_size, - &chrono::Utc::now().to_rfc3339(), - )?; - println!( - "{}", - serde_json::json!({ - "schemaVersion": "epiphany.heartbeat.artifact_retention_command.v0", - "status": if receipt.is_some() { "completed" } else { "within-bound" }, - "receipt": receipt.as_ref().map(retention_receipt_projection), - "privateStateExposed": false - }) - ); - } - "status" => { - let store_path = store_path.ok_or_else(|| anyhow!("status requires --store"))?; - if let Some(artifact_dir) = artifact_dir { - println!( - "{}", - heartbeat_status_projection( - &store_path, - artifact_dir, - target_heartbeat_rate, - limit - )? - ); - } else { - let state = load_heartbeat_state_entry(&store_path)?; - println!( - "{}", - serde_json::json!({ - "ok": true, - "command": "status", - "store": store_path, - "present": state.is_some(), - "schemaVersion": state.as_ref().map(|value| value.schema_version.as_str()), - "participants": state.as_ref().map(|value| value.participants.len()), - "history": state.as_ref().map(|value| value.history.len()), - }) - ); - } - } - "serve" => { - if interval_seconds == 0 { - return Err(anyhow!( - "serve requires --interval-seconds greater than zero" - )); - } - let store_path = store_path.ok_or_else(|| anyhow!("serve requires --store"))?; - let _singleton = acquire_resident_process_singleton("heartbeat", &store_path)?; - let artifact_dir = - artifact_dir.ok_or_else(|| anyhow!("serve requires --artifact-dir"))?; - let provider_release = if resident_self_store.is_some() { - let release_store = release_store - .as_deref() - .ok_or_else(|| anyhow!("resident heartbeat serve requires --release-store"))?; - let runtime_id = release_runtime_id.as_deref().ok_or_else(|| { - anyhow!("resident heartbeat serve requires --release-runtime-id") - })?; - let release_id = release_id - .as_deref() - .ok_or_else(|| anyhow!("resident heartbeat serve requires --release-id"))?; - let witness = release_witness_sha256.as_deref().ok_or_else(|| { - anyhow!("resident heartbeat serve requires --release-witness-sha256") - })?; - Some(authenticate_epiphany_packaged_release( - release_store, - runtime_id, - release_id, - witness, - )?) - } else { - None - }; - let provider_process = if provider_release.is_some() { - Some(capture_process_instance(std::process::id())?) - } else { - None - }; - let brake_runtime_id = heartbeat_brake_runtime_id( - &resident_self_store, - &resident_runtime_store, - release_runtime_id.as_deref(), - )?; - fs::create_dir_all(&artifact_dir).with_context(|| { - format!( - "failed to create heartbeat serve artifact directory {}", - artifact_dir.display() - ) - })?; - let mut completed_iterations = highest_pulse_artifact_sequence(&artifact_dir)?; - let mut refused_pulses = 0_u64; - let shutdown_requested = install_shutdown_signal_owner()?; - loop { - if heartbeat_serve_should_stop( - &shutdown_requested, - max_iterations, - completed_iterations, - ) { - break; - } - let iteration = completed_iterations + 1; - if let (Some(release), Some(process)) = (&provider_release, &provider_process) { - publish_resident_provider_readiness( - &store_path, - ResidentProviderReadiness { - schema_version: - epiphany_core::RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION.into(), - provider: "heartbeat".into(), - runtime_id: release.runtime_id.clone(), - release_id: release.release_id.clone(), - release_witness_sha256: release_witness_sha256 - .clone() - .expect("authenticated witness"), - source_commit: release.source_commit_sha.clone(), - publisher_sequence: 0, - observed_at_millis: chrono::Utc::now().timestamp_millis().max(0) as u64, - process_id: process.process_id, - process_creation_token: process.creation_token, - process_executable_path: process.executable_path.display().to_string(), - status: heartbeat_local_provider_status( - &store_path, - resident_self_store.as_deref().expect("resident store"), - ) - .into(), - private_state_exposed: false, - }, - )?; - } - let iteration_dir = artifact_dir.join(format!("pulse-{iteration:06}")); - let brake = active_swarm_brake(&local_verse_store, brake_runtime_id.as_deref())?; - if let Some(resident_store) = resident_self_store.as_deref() { - let pulse = pulse_resident_self_heartbeat( - &store_path, - resident_store, - &iteration_dir, - brake.is_some(), - &format!("{schedule_id}.serve-{iteration:06}"), - &source_scene_ref, - )?; - if pulse.status != "idle" { - completed_iterations = iteration; - refused_pulses += u64::from(brake.is_some()); - let retention = retain_heartbeat_pulse_artifacts( - &store_path, - &artifact_dir, - retain_pulse_artifacts, - retention_batch_size, - &chrono::Utc::now().to_rfc3339(), - )?; - println!( - "{}", - serde_json::json!({ - "schemaVersion":"epiphany.heartbeat.serve_pulse.v0", "status":pulse.status, - "owner":"heartbeat", "iteration":iteration, "artifactDir":iteration_dir, - "acknowledgedTerminalId":pulse.acknowledged_terminal_id, "grantId":pulse.grant_id, - "brakeId":brake.as_ref().map(|value| &value.brake_id), - "retention":retention.as_ref().map(retention_receipt_projection), - "privateStateExposed":false - }) - ); - if max_iterations > 0 && completed_iterations >= max_iterations { - break; - } - wait_for_shutdown( - &shutdown_requested, - Duration::from_secs(interval_seconds), - ); - continue; - } - } - if let Some(brake) = brake { - completed_iterations = iteration; - refused_pulses += 1; - let retention = retain_heartbeat_pulse_artifacts( - &store_path, - &artifact_dir, - retain_pulse_artifacts, - retention_batch_size, - &chrono::Utc::now().to_rfc3339(), - )?; - println!( - "{}", - serde_json::json!({ - "schemaVersion": "epiphany.heartbeat.serve_pulse.v0", - "status": "refused-by-swarm-brake", - "owner": "heartbeat", - "lifecycleOwner": "Idunn", - "iteration": iteration, - "artifactDir": iteration_dir, - "brakeId": brake.brake_id, - "brakeScope": brake.scope, - "reason": brake.reason, - "retention": retention.as_ref().map(retention_receipt_projection), - "privateStateExposed": false - }) - ); - if max_iterations > 0 && completed_iterations >= max_iterations { - break; - } - wait_for_shutdown(&shutdown_requested, Duration::from_secs(interval_seconds)); - continue; - } - completed_iterations = iteration; - let retention = retain_heartbeat_pulse_artifacts( - &store_path, - &artifact_dir, - retain_pulse_artifacts, - retention_batch_size, - &chrono::Utc::now().to_rfc3339(), - )?; - println!( - "{}", - serde_json::json!({ - "schemaVersion": "epiphany.heartbeat.serve_pulse.v0", - "status": "idle", - "owner": "heartbeat", - "lifecycleOwner": "Idunn", - "iteration": iteration, - "artifactDir": iteration_dir, - "retention": retention.as_ref().map(retention_receipt_projection), - "privateStateExposed": false - }) - ); - if max_iterations > 0 && completed_iterations >= max_iterations { - break; - } - wait_for_shutdown(&shutdown_requested, Duration::from_secs(interval_seconds)); - } - println!( - "{}", - serde_json::json!({ - "schemaVersion": "epiphany.heartbeat.serve_receipt.v0", - "status": "stopped-cleanly", - "owner": "heartbeat", - "lifecycleOwner": "Idunn", - "store": store_path, - "artifactDir": artifact_dir, - "intervalSeconds": interval_seconds, - "completedIterations": completed_iterations, - "refusedPulses": refused_pulses, - "bounded": max_iterations > 0, - "shutdownRequested": shutdown_requested.load(Ordering::SeqCst), - "privateStateExposed": false - }) - ); - } - _ => return usage(), - } - - Ok(()) -} - -fn install_shutdown_signal_owner() -> Result> { - let requested = Arc::new(AtomicBool::new(false)); - let signal = Arc::clone(&requested); - ctrlc::try_set_handler(move || { - signal.store(true, Ordering::SeqCst); - }) - .context("failed to install Heartbeat shutdown signal owner")?; - Ok(requested) -} - -fn heartbeat_serve_should_stop( - shutdown_requested: &AtomicBool, - max_iterations: u64, - completed_iterations: u64, -) -> bool { - shutdown_requested.load(Ordering::SeqCst) - || (max_iterations > 0 && completed_iterations >= max_iterations) -} - -fn wait_for_shutdown(requested: &AtomicBool, duration: Duration) { - let deadline = std::time::Instant::now() + duration; - while !requested.load(Ordering::SeqCst) { - let remaining = deadline.saturating_duration_since(std::time::Instant::now()); - if remaining.is_zero() { - break; - } - thread::sleep(remaining.min(Duration::from_millis(50))); - } -} - -fn next_path(args: &mut impl Iterator, name: &str) -> Result { - Ok(PathBuf::from(next_value(args, name)?)) -} - -fn retention_receipt_projection( - receipt: &EpiphanyHeartbeatArtifactRetentionReceipt, -) -> serde_json::Value { - serde_json::json!({ - "schemaVersion": receipt.schema_version, - "receiptId": receipt.receipt_id, - "planId": receipt.plan_id, - "status": receipt.status, - "deletedDirectories": receipt.deleted_directories, - "deletedFileCount": receipt.deleted_file_count, - "deletedByteCount": receipt.deleted_byte_count, - "completedAtUtc": receipt.completed_at_utc, - "privateStateExposed": receipt.private_state_exposed, - }) -} - -fn highest_pulse_artifact_sequence(artifact_root: &Path) -> Result { - let mut highest = 0_u64; - for entry in fs::read_dir(artifact_root)? { - let entry = entry?; - let file_type = entry.file_type()?; - if file_type.is_symlink() { - return Err(anyhow!("heartbeat artifact root contains a symlink")); - } - if !file_type.is_dir() { - continue; - } - let name = entry - .file_name() - .into_string() - .map_err(|_| anyhow!("heartbeat artifact directory name is not UTF-8"))?; - let digits = name - .strip_prefix("pulse-") - .filter(|digits| digits.len() == 6 && digits.bytes().all(|byte| byte.is_ascii_digit())) - .ok_or_else(|| { - anyhow!("heartbeat artifact root contains unknown directory {name:?}") - })?; - highest = highest.max(digits.parse()?); - } - if highest >= 999_999 { - return Err(anyhow!("heartbeat pulse artifact sequence is exhausted")); - } - Ok(highest) -} - -fn next_value(args: &mut impl Iterator, name: &str) -> Result { - args.next() - .ok_or_else(|| anyhow!("{name} requires a value")) -} - -fn usage() -> Result<()> { - Err(anyhow!(concat!( - "usage: epiphany-heartbeat-store init --store [--target-heartbeat-rate ]\n", - " epiphany-heartbeat-store repair-stale --store --artifact-dir [options]\n", - " epiphany-heartbeat-store retain-artifacts --store --artifact-dir [--retain-pulse-artifacts ] [--retention-batch-size ]\n", - " epiphany-heartbeat-store status --store [--artifact-dir ]\n", - " epiphany-heartbeat-store serve --store --artifact-dir [--local-verse-store ] [--resident-self-store --resident-runtime-store ] [--interval-seconds ] [--max-iterations ]" - ))) -} - -fn heartbeat_brake_runtime_id( - resident_self_store: &Option, - resident_runtime_store: &Option, - release_runtime_id: Option<&str>, -) -> Result> { - if resident_self_store.is_some() { - let runtime_store = resident_runtime_store - .as_deref() - .ok_or_else(|| anyhow!("resident heartbeat serve requires --resident-runtime-store"))?; - return Ok(Some(resident_cognitive_runtime_id(runtime_store)?)); - } - Ok(release_runtime_id.map(str::to_string)) -} - -fn active_swarm_brake( - local_verse_store: &Option, - runtime_id: Option<&str>, -) -> Result> { - let Some(local_verse_store) = local_verse_store else { - return Ok(None); - }; - if !local_verse_store.exists() { - return Ok(None); - } - let runtime_id = runtime_id - .ok_or_else(|| anyhow!("heartbeat local Verse brake lookup requires runtime identity"))?; - Ok( - load_epiphany_cultmesh_swarm_brake(local_verse_store, runtime_id)? - .filter(|brake| brake.status == "engaged"), - ) -} - -#[cfg(test)] -mod brake_tests { - use super::*; - - #[test] - fn heartbeat_serve_stops_for_signal_or_bounded_completion() { - let requested = AtomicBool::new(false); - assert!(!heartbeat_serve_should_stop(&requested, 0, 42)); - assert!(!heartbeat_serve_should_stop(&requested, 43, 42)); - assert!(heartbeat_serve_should_stop(&requested, 42, 42)); - requested.store(true, Ordering::SeqCst); - assert!(heartbeat_serve_should_stop(&requested, 0, 42)); - } - - #[test] - fn heartbeat_shutdown_interrupts_interval_wait() { - let requested = AtomicBool::new(true); - let started = std::time::Instant::now(); - wait_for_shutdown(&requested, Duration::from_secs(1)); - assert!(started.elapsed() < Duration::from_millis(100)); - } - - #[test] - fn heartbeat_brake_lookup_uses_the_exact_requested_namespace() -> Result<()> { - let temp = tempfile::tempdir()?; - let store = temp.path().join("verse.cc"); - let store_arg = Some(store.clone()); - let mut brake = - epiphany_core::default_epiphany_cultmesh_swarm_brake("2026-07-18T00:00:00Z"); - brake.status = "engaged".into(); - brake.reason = "test".into(); - let mut legacy_node = - epiphany_core::open_epiphany_cultmesh_node(&store, "legacy-writer-runtime")?; - legacy_node.put("epiphany-local/swarm-brake", &brake)?; - legacy_node.flush()?; - assert!(active_swarm_brake(&store_arg, Some("epiphany-yggdrasil"))?.is_some()); - epiphany_core::write_epiphany_cultmesh_swarm_brake(&store, "wrong-runtime", brake.clone())?; - assert!(active_swarm_brake(&store_arg, Some("epiphany-yggdrasil"))?.is_none()); - epiphany_core::write_epiphany_cultmesh_swarm_brake(&store, "epiphany-yggdrasil", brake)?; - assert!(active_swarm_brake(&store_arg, Some("epiphany-yggdrasil"))?.is_some()); - Ok(()) - } - - #[test] - fn resident_heartbeat_uses_mounted_cognitive_runtime_for_braking() -> Result<()> { - let temp = tempfile::tempdir()?; - let runtime_store = temp.path().join("runtime.cc"); - epiphany_core::initialize_runtime_spine( - &runtime_store, - epiphany_core::RuntimeSpineInitOptions { - runtime_id: "cognitive-runtime".into(), - display_name: "Cognitive runtime".into(), - created_at: "2026-08-10T00:00:00Z".into(), - }, - )?; - let selected = heartbeat_brake_runtime_id( - &Some(temp.path().join("resident.cc")), - &Some(runtime_store), - Some("deployment-runtime"), - )?; - assert_eq!(selected.as_deref(), Some("cognitive-runtime")); - Ok(()) - } - - #[test] - fn resident_heartbeat_resumes_after_highest_artifact_sequence() -> Result<()> { - let temp = tempfile::tempdir()?; - fs::create_dir(temp.path().join("pulse-000003"))?; - fs::create_dir(temp.path().join("pulse-012557"))?; - assert_eq!(highest_pulse_artifact_sequence(temp.path())?, 12_557); - fs::create_dir(temp.path().join("alien"))?; - assert!(highest_pulse_artifact_sequence(temp.path()).is_err()); - Ok(()) - } -} diff --git a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs index 2637af59..187379db 100644 --- a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs +++ b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs @@ -2215,21 +2215,6 @@ fn home_dir() -> PathBuf { mod tests { use super::*; - #[test] - fn resident_required_action_refusal_precedes_the_action_match() { - let source = include_str!("epiphany-mvp-coordinator.rs"); - let refusal = source - .find("requiredActionRefusal") - .expect("required-action refusal"); - let action_match = source - .find("match action.as_str()") - .expect("coordinator action match"); - assert!(refusal < action_match); - let refusal_body = &source[refusal..action_match]; - assert!(refusal_body.contains("consequence\": \"none")); - assert!(refusal_body.contains("break;")); - } - #[test] fn proposal_cli_argument_is_only_an_assertion_over_self_derived_authority() { let request = "repo-frontier-proposal-modeling-request"; @@ -2338,51 +2323,6 @@ mod tests { Ok(()) } - #[test] - fn coordinator_binary_has_no_codex_host_or_epiphany_json_rpc_dependency() { - let source = include_str!("epiphany-mvp-coordinator.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for forbidden in ["AppServerClient", "thread/epiphany/", "codex-app-server"] { - assert!( - !production.contains(forbidden), - "native coordinator regrew host dependency {forbidden:?}" - ); - } - let compatibility_field = ["epiphany", "State"].concat(); - assert!(!production.contains(&compatibility_field)); - assert!(!production.contains("--thread-state-store")); - } - - #[test] - fn coordinator_uses_keyed_family_owners_without_generic_role_routing() { - let source = include_str!("epiphany-mvp-coordinator.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for required in [ - "launch_current_imagination_consideration_work", - "launch_current_admitted_model_direction_consideration_work", - "launch_current_frontier_research_work", - "launch_current_frontier_verification_work", - ] { - assert!( - production.contains(required), - "missing keyed owner {required:?}" - ); - } - for forbidden in [ - "fn launch_role(", - "fn accept_role(", - "fn supersede_role_result(", - ".apply_state_update(", - ".accept_role(", - "stateRevision", - ] { - assert!( - !production.contains(forbidden), - "generic aggregate role authority survived: {forbidden:?}" - ); - } - } - #[test] fn continue_implementation_is_not_a_passive_stop_action() { assert!(!is_stop_action("continueImplementation")); diff --git a/epiphany-core/src/bin/epiphany-mvp-status.rs b/epiphany-core/src/bin/epiphany-mvp-status.rs index 5584436e..df7c7bad 100644 --- a/epiphany-core/src/bin/epiphany-mvp-status.rs +++ b/epiphany-core/src/bin/epiphany-mvp-status.rs @@ -623,20 +623,4 @@ mod tests { assert_eq!(sanitized["decision"]["action"], "continue"); } - #[test] - fn source_has_no_thread_aggregate_or_generic_interrupt() { - let source = include_str!("epiphany-mvp-status.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for forbidden in [ - "EpiphanyThreadState", - "read_accepted_coordinator_state", - "latest_runtime_job_id_for_binding", - "derive_coordinator_finding_signals", - "expected_revision", - "interrupt_job", - "stateRevision", - ] { - assert!(!production.contains(forbidden)); - } - } } diff --git a/epiphany-core/src/bin/epiphany-swarm.rs b/epiphany-core/src/bin/epiphany-swarm.rs index 913fbb6f..bc9c104c 100644 --- a/epiphany-core/src/bin/epiphany-swarm.rs +++ b/epiphany-core/src/bin/epiphany-swarm.rs @@ -9,9 +9,10 @@ use epiphany_core::{ bind_runtime_repository_domain, capture_process_instance, coordinator_run_receipts, derive_resident_cognition_readiness, enqueue_resident_self_pressure_idempotent, import_bifrost_persona_feedback_deliveries, ingest_resident_self_current_work_pressure, - live_resident_self_typed_request_ids, load_epiphany_cultmesh_swarm_brake, + issue_resident_self_grant, live_resident_self_typed_request_ids, + load_epiphany_cultmesh_swarm_brake, load_resident_self_state, materialize_resident_self_domain_obligations, - observe_process_instance, pending_resident_self_acks, prepare_resident_self_launch, + observe_process_instance, prepare_resident_self_launch, resident_self_terminal_receipts, publish_resident_provider_readiness, reap_exited_child_process, recover_dead_runtime_worker_attempts, resident_cognitive_runtime_id, resident_prepared_launch_thread_id, resident_self_child_claim, @@ -53,15 +54,9 @@ fn main() -> Result<()> { &args.policy.release_store, ], )?; - if args.heartbeat_store == args.state_store { - return Err(anyhow!( - "heartbeat and resident Self stores must be physically separate" - )); - } if matches!(args.command, CommandKind::Status) { let projection = derive_resident_cognition_readiness(ResidentReadinessRequest { release_store: &args.policy.release_store, - heartbeat_store: &args.heartbeat_store, resident_store: &args.state_store, policy: &args.policy, release_runtime_id: &args.policy.release_runtime_id, @@ -73,7 +68,7 @@ fn main() -> Result<()> { return Ok(()); } require_resident_git("git")?; - let _singleton = acquire_resident_process_singleton("resident-self", &args.state_store)?; + let _singleton = acquire_resident_process_singleton(&args.state_store)?; if let Some(pressure) = args.pressure.as_ref() { enqueue_resident_self_pressure_idempotent(&args.state_store, pressure)?; } @@ -96,11 +91,9 @@ fn main() -> Result<()> { loop { let shutting_down = shutdown_requested.load(Ordering::SeqCst); let outcome = cycle(&args, &mut state, &mut ports, shutting_down)?; - if resident_maintenance_required( - state.revision, - last_maintained_revision, - std::time::Instant::now() >= next_maintenance_at, - ) { + if last_maintained_revision != Some(state.revision) + || std::time::Instant::now() >= next_maintenance_at + { maintain_resident_runtime(&args, &state)?; last_maintained_revision = Some(state.revision); next_maintenance_at = std::time::Instant::now() + RESIDENT_MAINTENANCE_INTERVAL; @@ -151,22 +144,11 @@ fn maintain_resident_runtime(args: &Args, state: &ResidentSelfState) -> Result<( Ok(()) } -fn resident_maintenance_required( - revision: u64, - last_maintained_revision: Option, - interval_due: bool, -) -> bool { - interval_due || last_maintained_revision != Some(revision) -} - fn retain_runtime_receipts(args: &Args, state: &ResidentSelfState) -> Result<()> { - if !runtime_receipt_retention_allowed( - state.active_turn.is_some(), - state.prepared_launch.is_some(), - ) { + if state.active_turn.is_some() || state.prepared_launch.is_some() { return Ok(()); } - let mut preserved = pending_resident_self_acks(&args.state_store)? + let mut preserved = resident_self_terminal_receipts(&args.state_store)? .into_iter() .map(|ack| ack.coordinator_receipt_id) .collect::>(); @@ -207,10 +189,6 @@ fn retain_runtime_worker_attempts(args: &Args) -> Result<()> { Ok(()) } -fn runtime_receipt_retention_allowed(has_active_turn: bool, has_prepared_launch: bool) -> bool { - !has_active_turn && !has_prepared_launch -} - fn publish_self_readiness(args: &Args) -> Result<()> { let process = capture_process_instance(std::process::id())?; publish_resident_provider_readiness( @@ -234,26 +212,6 @@ fn publish_self_readiness(args: &Args) -> Result<()> { Ok(()) } -fn run_feedback_import_if_released( - brake_engaged: bool, - import: impl FnOnce() -> Result<()>, -) -> Result<()> { - if brake_engaged { - return Ok(()); - } - import() -} - -fn run_cycle_ingress_if_released( - blocked: bool, - ingress: impl FnOnce() -> Result<()>, -) -> Result<()> { - if blocked { - return Ok(()); - } - ingress() -} - fn install_shutdown_signal_owner() -> Result> { let requested = Arc::new(AtomicBool::new(false)); let signal = Arc::clone(&requested); @@ -308,7 +266,7 @@ fn cycle( recover_dead_runtime_worker_attempts(&args.policy.runtime_store, now)?; let brake_engaged = ports.brake_engaged()?; let cognitive_runtime_id = ports.cognitive_runtime_id().to_string(); - run_feedback_import_if_released(brake_engaged || shutdown_requested, || { + if !brake_engaged && !shutdown_requested { import_bifrost_persona_feedback_deliveries( &args.persona_feedback_source_store, &args.persona_feedback_store, @@ -319,14 +277,13 @@ fn cycle( )?; admit_persona_feedback_to_social( &args.persona_feedback_store, - &args.heartbeat_store, + &args.persona_social_store, &cognitive_runtime_id, &args.policy.model_provider, &args.persona_model_allowed_data_classifications, )?; - Ok(()) - })?; - run_cycle_ingress_if_released(brake_engaged || shutdown_requested, || { + } + if !brake_engaged && !shutdown_requested { bind_runtime_repository_domain( &args.policy.runtime_store, &args.feedback_target_repository, @@ -345,8 +302,7 @@ fn cycle( &args.policy.runtime_store, now, )?; - Ok(()) - })?; + } *state = load_resident_self_state(&args.state_store)?; if let Some(prepared) = state.prepared_launch.clone() { if let Some(claim) = resident_self_child_claim(&args.state_store, &prepared.preparation_id)? @@ -442,6 +398,7 @@ fn cycle( if shutdown_requested || ports.brake_engaged()? { return Ok(ResidentSelfOutcome::Braked); } + issue_resident_self_grant(&args.state_store, now)?; let Some(prepared) = prepare_resident_self_launch(&args.state_store, &args.policy, now)? else { return Ok(ResidentSelfOutcome::Sleeping); }; @@ -479,7 +436,7 @@ fn summary( "activeTurnId": state.active_turn.as_ref().map(|turn| &turn.turn_id), "shutdownRequested": shutdown_requested, "nextEligibleAtMillis": state.next_eligible_at_millis, - "wakeAuthority": "standard heartbeat consumes typed operator, admitted Modeling-map direction consideration, Persona feedback, or Imagination proposal pressure and emits one single-consumption Self grant", + "wakeAuthority": "Resident Self claims one typed pending pressure and emits one single-consumption grant", "preparedRecovery": if state.prepared_launch.is_some() { "fail-closed-awaiting-exact-child-claim-or-witnessed-recovery" } else { "not-required" }, "authority": "Self may launch one bounded coordinator turn; it cannot directly invoke model/tools, mutate Mind/Hands, review, release, or deploy", "privateStateExposed": false @@ -505,7 +462,7 @@ fn validate_command_turn_bound(command: CommandKind, max_steps: u64) -> Result<( struct Args { command: CommandKind, state_store: PathBuf, - heartbeat_store: PathBuf, + persona_social_store: PathBuf, provider_freshness_seconds: u64, retained_closed_lifecycles: usize, retained_coordinator_receipts: usize, @@ -615,7 +572,7 @@ impl Args { Ok(Self { command, state_store: path("--state-store")?, - heartbeat_store: path("--heartbeat-store")?, + persona_social_store: path("--persona-social-store")?, provider_freshness_seconds: u64v("--provider-freshness-seconds", 180)?, retained_closed_lifecycles: u64v("--retained-closed-lifecycles", 256)? .try_into() @@ -877,68 +834,4 @@ mod brake_tests { Ok(()) } - #[test] - fn engaged_brake_does_not_touch_the_bifrost_provider_store() -> Result<()> { - let touched = std::cell::Cell::new(false); - run_feedback_import_if_released(true, || { - touched.set(true); - anyhow::bail!("absent Bifrost delivery store was touched") - })?; - assert!(!touched.get()); - assert!( - run_feedback_import_if_released(false, || { - anyhow::bail!("released cognition requires Bifrost") - }) - .is_err() - ); - Ok(()) - } - - #[test] - fn terminal_fulfillment_has_explicit_operator_projection() { - assert_eq!( - ResidentSelfOutcome::AwaitingFulfillment.operator_status(), - "awaiting-fulfillment" - ); - } - - #[test] - fn shutdown_wait_returns_immediately_when_already_requested() { - let requested = AtomicBool::new(true); - let started = std::time::Instant::now(); - wait_for_shutdown(&requested, Duration::from_secs(1)); - assert!(started.elapsed() < Duration::from_millis(100)); - } - - #[test] - fn brake_or_shutdown_does_not_admit_new_cycle_ingress() -> Result<()> { - let touched = std::cell::Cell::new(false); - run_cycle_ingress_if_released(true, || { - touched.set(true); - Ok(()) - })?; - assert!(!touched.get()); - run_cycle_ingress_if_released(false, || { - touched.set(true); - Ok(()) - })?; - assert!(touched.get()); - Ok(()) - } - - #[test] - fn runtime_receipt_retention_refuses_active_or_prepared_launch_authority() { - assert!(runtime_receipt_retention_allowed(false, false)); - assert!(!runtime_receipt_retention_allowed(true, false)); - assert!(!runtime_receipt_retention_allowed(false, true)); - assert!(!runtime_receipt_retention_allowed(true, true)); - } - - #[test] - fn maintenance_follows_revision_and_deadline_not_poll_outcome() { - assert!(resident_maintenance_required(7, None, false,)); - assert!(!resident_maintenance_required(7, Some(7), false,)); - assert!(resident_maintenance_required(8, Some(7), false,)); - assert!(resident_maintenance_required(7, Some(7), true,)); - } } diff --git a/epiphany-core/src/cultmesh_integration.rs b/epiphany-core/src/cultmesh_integration.rs index cbebba33..08183768 100644 --- a/epiphany-core/src/cultmesh_integration.rs +++ b/epiphany-core/src/cultmesh_integration.rs @@ -11118,22 +11118,4 @@ mod tests { ); Ok(()) } - - #[test] - fn bifrost_mirrors_name_arrival_order_without_rewriting_storage_keys() { - let source = include_str!("cultmesh_integration.rs"); - let old_loader = ["load_", "latest_epiphany_cultmesh_bifrost_"].concat(); - let old_field = ["pub ", "latest_bifrost_"].concat(); - assert!(!source.contains(&old_loader)); - assert!(!source.contains(&old_field)); - assert_eq!( - source - .lines() - .filter(|line| { - line.contains("\"gamecult-local/bifrost/") && line.contains("/latest\"") - }) - .count(), - 6 - ); - } } diff --git a/epiphany-core/src/current_work.rs b/epiphany-core/src/current_work.rs index a6a515e3..a8324da0 100644 --- a/epiphany-core/src/current_work.rs +++ b/epiphany-core/src/current_work.rs @@ -3849,13 +3849,8 @@ mod tests { &store, 1, )?); - let current_work_grant = crate::heartbeat_issue_resident_self_grant( - &resident_store, - "current-work-schedule", - "current-work-action", - 2, - )? - .expect("the exact current-work projection should receive one grant"); + let current_work_grant = crate::issue_resident_self_grant(&resident_store, 2)? + .expect("the exact current-work projection should receive one grant"); assert_eq!( current_work_grant.pressure_kind, crate::RESIDENT_SELF_CURRENT_WORK_PRESSURE_KIND @@ -3921,9 +3916,9 @@ mod tests { "a grant derived from the prior Mind projection must not launch" ); assert!(crate::pending_resident_self_grant(&resident_store)?.is_none()); - let supersession_acks = crate::pending_resident_self_acks(&resident_store)?; - assert_eq!(supersession_acks.len(), 1); - assert_eq!(supersession_acks[0].terminal_status, "superseded"); + let terminal_receipts = crate::resident_self_terminal_receipts(&resident_store)?; + assert_eq!(terminal_receipts.len(), 1); + assert_eq!(terminal_receipts[0].terminal_status, "superseded"); assert_current_work_reentry_is_read_only(&store, &scheduled_work)?; let mut scheduled_cache = crate::runtime_spine_cache(&store)?; scheduled_cache.pull_all_backing_stores()?; @@ -4059,13 +4054,8 @@ mod tests { !crate::ingest_resident_self_current_work_pressure(&resident_store, &store, 5)?, "unchanged failed-attempt state must remain idempotent" ); - let retry_grant = crate::heartbeat_issue_resident_self_grant( - &resident_store, - "current-work-retry-schedule", - "current-work-retry-action", - 6, - )? - .expect("failed Body attempt must receive a fresh exact-state grant"); + let retry_grant = crate::issue_resident_self_grant(&resident_store, 6)? + .expect("failed Body attempt must receive a fresh exact-state grant"); assert_eq!( retry_grant.provenance_ref, format!( diff --git a/epiphany-core/src/heartbeat_state.rs b/epiphany-core/src/heartbeat_state.rs deleted file mode 100644 index e418feea..00000000 --- a/epiphany-core/src/heartbeat_state.rs +++ /dev/null @@ -1,1210 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use chrono::DateTime; -use chrono::Utc; -use serde_json::Value; -use std::fs; -use std::path::Path; - -mod heartbeat_documents; -mod heartbeat_projection; -mod heartbeat_retention; -mod heartbeat_roles; -mod heartbeat_store; -pub use heartbeat_documents::*; -pub use heartbeat_projection::heartbeat_status_projection; -use heartbeat_projection::history_event_json; -use heartbeat_projection::schedule_participant_json; -use heartbeat_projection::selection_policy_json; -pub use heartbeat_retention::retain_heartbeat_pulse_artifacts; -use heartbeat_roles::ROLE_ORDER; -pub use heartbeat_roles::default_heartbeat_state; -use heartbeat_roles::default_participant; -pub use heartbeat_roles::initialize_heartbeat_store; -pub use heartbeat_store::commit_heartbeat_state_transaction; -pub use heartbeat_store::heartbeat_state_cache; -pub use heartbeat_store::load_heartbeat_state_entry; -pub use heartbeat_store::load_heartbeat_state_transaction; -pub use heartbeat_store::load_latest_heartbeat_stale_turn_repair_receipt; -pub use heartbeat_store::validate_heartbeat_state; -pub use heartbeat_store::write_heartbeat_stale_turn_repair_receipt; -pub use heartbeat_store::write_heartbeat_state_entry; - -pub(super) const HEARTBEAT_ARENA_MAINTENANCE: &str = "maintenance"; -pub(super) const PARTICIPANT_KIND_AGENT: &str = "agent"; - -#[derive(Clone, Debug, PartialEq)] -struct HeartbeatAction { - action_id: String, - action_type: &'static str, - action_scale: &'static str, - base_recovery: f64, - initiative_cost: f64, - interruptibility: f64, - commitment: f64, - local_affordance_basis: Vec, -} - -fn tick_heartbeat_store( - store_path: impl AsRef, - artifact_dir: impl AsRef, - options: HeartbeatTickOptions, -) -> Result { - let store_path = store_path.as_ref(); - if options.resident_self_store.as_deref() == Some(store_path) { - return Err(anyhow!( - "heartbeat and resident Self require physically separate CultCache stores" - )); - } - let (loaded_state, expected_state) = load_heartbeat_state_transaction(store_path)?; - let mut state = - loaded_state.unwrap_or_else(|| default_heartbeat_state(options.target_heartbeat_rate)); - if options.target_heartbeat_rate > 0.0 { - state.target_heartbeat_rate = options.target_heartbeat_rate; - } - patch_missing_participants(&mut state); - if let Some(resident_store) = options.resident_self_store.as_deref() - && crate::pending_resident_self_pressure(resident_store)? - && let Some(pending) = state - .participants - .iter() - .find(|participant| participant.role_id == "coordinator") - .and_then(|participant| participant.pending_turn.as_ref()) - { - crate::heartbeat_issue_resident_self_grant( - resident_store, - &pending.schedule_id, - &pending.action_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - } - let mut resident_ack_to_consume = None; - if let Some(resident_store) = options.resident_self_store.as_deref() - && let Some(pending) = state - .participants - .iter() - .find(|participant| participant.role_id == "coordinator") - .and_then(|participant| participant.pending_turn.as_ref()) - && let Some(ack) = crate::pending_resident_self_ack_for( - resident_store, - &pending.schedule_id, - &pending.action_id, - )? - { - let index = participant_index_by_role(&state, "coordinator")?; - complete_pending_turn(&mut state, index)?; - resident_ack_to_consume = Some((resident_store.to_path_buf(), ack.ack_id)); - } - let resident_pressure_pending = - if let Some(resident_store) = options.resident_self_store.as_deref() { - crate::pending_resident_self_pressure(resident_store)? - } else { - false - }; - let result = tick_once(&mut state, &options, resident_pressure_pending)?; - commit_heartbeat_state_transaction(store_path, expected_state, &state)?; - if result["event"]["selectedRole"] == "coordinator" - && let Some(resident_store) = options.resident_self_store.as_deref() - { - let schedule_id = result["event"]["schedule_id"] - .as_str() - .or_else(|| result["event"]["scheduleId"].as_str()) - .unwrap_or(&options.schedule_id); - let action_id = result["event"]["action_id"] - .as_str() - .or_else(|| result["event"]["actionId"].as_str()) - .unwrap_or_default(); - crate::heartbeat_issue_resident_self_grant( - resident_store, - schedule_id, - action_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - } - if let Some((resident_store, ack_id)) = resident_ack_to_consume { - crate::heartbeat_consume_resident_self_ack( - &resident_store, - &ack_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - } - - let artifact_dir = artifact_dir.as_ref(); - fs::create_dir_all(artifact_dir) - .with_context(|| format!("failed to create {}", artifact_dir.display()))?; - write_json_artifact( - artifact_dir.join(format!("{}.initiative.json", options.schedule_id)), - &result["schedule"], - )?; - write_json_artifact( - artifact_dir.join(format!("{}.event.json", options.schedule_id)), - &result["event"], - )?; - - Ok(serde_json::json!({ - "ok": true, - "storeFile": store_path, - "artifactDir": artifact_dir, - "stateFile": null, - "event": result["event"].clone(), - "schedule": result["schedule"].clone(), - })) -} - -pub fn reconcile_resident_self_heartbeat_ack( - heartbeat_store: impl AsRef, - resident_store: impl AsRef, -) -> Result> { - let heartbeat_store = heartbeat_store.as_ref(); - let resident_store = resident_store.as_ref(); - if heartbeat_store == resident_store { - return Err(anyhow!("heartbeat and resident Self stores must differ")); - } - let (state, expected) = load_heartbeat_state_transaction(heartbeat_store)?; - let Some(mut state) = state else { - return Ok(None); - }; - let Some((index, pending)) = state - .participants - .iter() - .enumerate() - .find(|(_, participant)| participant.role_id == "coordinator") - .and_then(|(index, participant)| { - participant - .pending_turn - .clone() - .map(|pending| (index, pending)) - }) - else { - // Recovery for a crash after heartbeat completion committed but before the - // acknowledgement was marked consumed in the separate resident store. - let completed = crate::pending_resident_self_acks(resident_store)? - .into_iter() - .find(|ack| { - state.history.iter().any(|event| { - event.selected_role == "coordinator" - && event.schedule_id == ack.heartbeat_schedule_id - && event.action_id == ack.heartbeat_action_id - && event.turn_status.as_deref() == Some("completed") - }) - }); - if let Some(ack) = completed { - crate::heartbeat_consume_resident_self_ack( - resident_store, - &ack.ack_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - return Ok(Some(ack.ack_id)); - } - return Ok(None); - }; - let Some(ack) = crate::pending_resident_self_ack_for( - resident_store, - &pending.schedule_id, - &pending.action_id, - )? - else { - return Ok(None); - }; - let completed = complete_pending_turn(&mut state, index)?; - let participant = &state.participants[index]; - state.history.push(HeartbeatHistoryEvent { - ts: now_iso(), - schedule_id: completed.schedule_id.clone(), - selected_role: participant.role_id.clone(), - selected_agent_id: participant.agent_id.clone(), - action_id: completed.action_id.clone(), - action_type: completed.action_type.clone(), - arena: participant_arena(participant).to_string(), - participant_kind: participant_kind(participant).to_string(), - action_scale: completed.action_scale.clone(), - coordinator_action: None, - target_role: None, - work_role: None, - scene_clock: Some(state.scene_clock), - next_ready_at: Some(participant.next_ready_at), - turn_status: Some("completed".into()), - cooldown_started_after_completion: Some(true), - }); - trim_history(&mut state); - commit_heartbeat_state_transaction(heartbeat_store, expected, &state)?; - crate::heartbeat_consume_resident_self_ack( - resident_store, - &ack.ack_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - Ok(Some(ack.ack_id)) -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ResidentSelfHeartbeatPulse { - pub status: String, - pub acknowledged_terminal_id: Option, - pub grant_id: Option, -} - -pub fn pulse_resident_self_heartbeat( - heartbeat_store: impl AsRef, - resident_store: impl AsRef, - artifact_dir: impl AsRef, - brake_engaged: bool, - schedule_id: &str, - source_scene_ref: &str, -) -> Result { - let heartbeat_store = heartbeat_store.as_ref(); - let resident_store = resident_store.as_ref(); - let acknowledged_terminal_id = - reconcile_resident_self_heartbeat_ack(heartbeat_store, resident_store)?; - if brake_engaged { - return Ok(ResidentSelfHeartbeatPulse { - status: "braked-after-ack-reconciliation".into(), - acknowledged_terminal_id, - grant_id: None, - }); - } - let state = load_heartbeat_state_entry(heartbeat_store)?; - let active = state - .as_ref() - .and_then(|state| { - state - .participants - .iter() - .find(|participant| participant.role_id == "coordinator") - }) - .and_then(|participant| participant.pending_turn.as_ref()) - .filter(|turn| turn.status == "running") - .cloned(); - if let Some(active) = active { - if crate::pending_resident_self_pressure(resident_store)? { - let grant = crate::heartbeat_issue_resident_self_grant( - resident_store, - &active.schedule_id, - &active.action_id, - Utc::now().timestamp_millis().max(0) as u64, - )?; - return Ok(ResidentSelfHeartbeatPulse { - status: "recovered-committed-grant".into(), - acknowledged_terminal_id, - grant_id: grant.map(|grant| grant.grant_id), - }); - } - return Ok(ResidentSelfHeartbeatPulse { - status: "active-coordinator-turn".into(), - acknowledged_terminal_id, - grant_id: None, - }); - } - if !crate::pending_resident_self_pressure(resident_store)? { - return Ok(ResidentSelfHeartbeatPulse { - status: "idle".into(), - acknowledged_terminal_id, - grant_id: None, - }); - } - tick_heartbeat_store( - heartbeat_store, - artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - schedule_id: schedule_id.into(), - source_scene_ref: source_scene_ref.into(), - resident_self_store: Some(resident_store.to_path_buf()), - }, - )?; - let grant = crate::pending_resident_self_grant(resident_store)? - .ok_or_else(|| anyhow!("heartbeat selected resident Self pressure but emitted no grant"))?; - Ok(ResidentSelfHeartbeatPulse { - status: "granted".into(), - acknowledged_terminal_id, - grant_id: Some(grant.grant_id), - }) -} - -pub fn recover_stale_heartbeat_store( - store_path: impl AsRef, - artifact_dir: impl AsRef, - options: HeartbeatStaleTurnRepairOptions, -) -> Result { - if options.max_age_seconds < 0 { - return Err(anyhow!( - "stale heartbeat repair max_age_seconds must be non-negative" - )); - } - if options.reason.trim().is_empty() { - return Err(anyhow!("stale heartbeat repair requires a reason")); - } - let store_path = store_path.as_ref(); - let mut state = load_heartbeat_state_entry(store_path)?.ok_or_else(|| { - anyhow!( - "CultCache store {} has no heartbeat state entry", - store_path.display() - ) - })?; - let repaired_at = options.now_utc.clone().unwrap_or_else(now_iso); - let repaired_at_time = parse_heartbeat_time("repair time", &repaired_at)?; - let mut receipts = Vec::new(); - - for participant in &mut state.participants { - let pending = match participant.pending_turn.clone() { - Some(pending) if pending.status == "running" => pending, - _ => continue, - }; - let started_at = parse_heartbeat_time("pending turn start time", &pending.started_at)?; - let stale_age_seconds = repaired_at_time - .signed_duration_since(started_at) - .num_seconds(); - if stale_age_seconds < options.max_age_seconds { - continue; - } - participant.pending_turn = None; - participant.current_load = 0.0; - participant.next_ready_at = round6(state.scene_clock.max(pending.started_scene_clock)); - let receipt = EpiphanyHeartbeatStaleTurnRepairReceipt { - schema_version: HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION.to_string(), - receipt_id: format!( - "heartbeat-stale-turn-repair-{}-{}", - participant.role_id, - now_stamp() - ), - repaired_at_utc: repaired_at.clone(), - role_id: participant.role_id.clone(), - agent_id: participant.agent_id.clone(), - action_id: pending.action_id.clone(), - schedule_id: pending.schedule_id.clone(), - started_at_utc: pending.started_at.clone(), - stale_age_seconds, - reason: options.reason.clone(), - resulting_status: "repaired".to_string(), - next_ready_at: participant.next_ready_at, - private_state_exposed: false, - notes: vec![ - "Stale heartbeat repair is a Continuity-facing operator-safe receipt, not silent scheduler cleanup.".to_string(), - "The repaired lane becomes schedulable only through normal heartbeat selection after the receipt is written.".to_string(), - ], - }; - let event = HeartbeatHistoryEvent { - ts: repaired_at.clone(), - schedule_id: pending.schedule_id.clone(), - selected_role: participant.role_id.clone(), - selected_agent_id: participant.agent_id.clone(), - action_id: pending.action_id.clone(), - action_type: pending.action_type.clone(), - arena: participant_arena(participant).to_string(), - participant_kind: participant_kind(participant).to_string(), - action_scale: pending.action_scale.clone(), - coordinator_action: None, - target_role: None, - work_role: None, - scene_clock: Some(state.scene_clock), - next_ready_at: Some(participant.next_ready_at), - turn_status: Some("stale_repaired".to_string()), - cooldown_started_after_completion: Some(false), - }; - state.history.push(event); - receipts.push(receipt); - } - - if receipts.is_empty() { - return Ok(serde_json::json!({ - "ok": true, - "storeFile": store_path, - "repaired": 0, - "receipts": [], - "reviewNotes": [ - "No running heartbeat turns exceeded the stale repair threshold." - ], - })); - } - - trim_history(&mut state); - write_heartbeat_state_entry(store_path, &state)?; - let mut written_receipts = Vec::new(); - for receipt in receipts { - written_receipts.push(write_heartbeat_stale_turn_repair_receipt( - store_path, &receipt, - )?); - } - - let artifact_dir = artifact_dir.as_ref(); - fs::create_dir_all(artifact_dir) - .with_context(|| format!("failed to create {}", artifact_dir.display()))?; - write_json_artifact( - artifact_dir.join(format!("heartbeat-stale-repair-{}.json", now_stamp())), - &serde_json::json!({ - "schemaVersion": "epiphany.heartbeat.stale_repair_artifact.v0", - "storeFile": store_path, - "repairedAtUtc": repaired_at, - "maxAgeSeconds": options.max_age_seconds, - "receipts": written_receipts, - }), - )?; - - Ok(serde_json::json!({ - "ok": true, - "storeFile": store_path, - "repaired": written_receipts.len(), - "receipts": written_receipts, - })) -} - -fn tick_once( - state: &mut EpiphanyHeartbeatStateEntry, - options: &HeartbeatTickOptions, - resident_pressure_pending: bool, -) -> Result { - let rate = state.target_heartbeat_rate.max(0.001); - let work_role = resident_pressure_pending.then_some("coordinator"); - let urgency = if resident_pressure_pending { 1.0 } else { 0.0 }; - let (selected_index, selection_kind, selection_reason) = - select_participant(state, work_role, urgency, resident_pressure_pending)?; - let selected = state.participants[selected_index].clone(); - let action = action_for_selection(state, &selected, work_role, rate); - let scene_clock = state.scene_clock.max(selected.next_ready_at); - let recovery = action.base_recovery - / selected - .initiative_speed - .max(state.selection_policy.minimum_speed); - let pending = HeartbeatPendingTurn { - status: "running".to_string(), - schedule_id: options.schedule_id.clone(), - action_id: action.action_id.clone(), - action_type: action.action_type.to_string(), - action_scale: action.action_scale.to_string(), - local_affordance_basis: action.local_affordance_basis.clone(), - started_at: now_iso(), - started_scene_clock: round6(scene_clock), - base_recovery: round6(action.base_recovery), - recovery: round6(recovery), - cooldown_policy: "after_turn_completion".to_string(), - completed_at: None, - completed_scene_clock: None, - next_ready_at: None, - initiative_frozen: true, - initiative_freeze_reason: Some( - "Participant has an active heartbeat turn; initiative cannot queue it again until the turn completes." - .to_string(), - ), - }; - state.participants[selected_index].pending_turn = Some(pending.clone()); - state.participants[selected_index].last_action_id = Some(action.action_id.clone()); - state.participants[selected_index].last_woke_at = Some(now_iso()); - state.participants[selected_index].current_load = - round6((state.participants[selected_index].current_load * 0.75).clamp(0.0, 1.0)); - state.scene_clock = round6(scene_clock); - let selected_after = state.participants[selected_index].clone(); - let event = HeartbeatHistoryEvent { - ts: now_iso(), - schedule_id: options.schedule_id.clone(), - selected_role: selected_after.role_id.clone(), - selected_agent_id: selected_after.agent_id.clone(), - action_id: action.action_id.clone(), - action_type: action.action_type.to_string(), - arena: participant_arena(&selected_after).to_string(), - participant_kind: participant_kind(&selected_after).to_string(), - action_scale: action.action_scale.to_string(), - coordinator_action: None, - target_role: work_role.map(str::to_string), - work_role: work_role.map(str::to_string), - scene_clock: Some(state.scene_clock), - next_ready_at: Some(selected_after.next_ready_at), - turn_status: Some("running".to_string()), - cooldown_started_after_completion: Some(true), - }; - state.history.push(event.clone()); - trim_history(state); - - let readiness_snapshot = readiness_snapshot(state, work_role, urgency); - let schedule = serde_json::json!({ - "schema_version": INITIATIVE_SCHEMA_VERSION, - "schedule_id": options.schedule_id, - "source_scene_ref": options.source_scene_ref, - "scene_clock": state.scene_clock, - "participants": state.participants.iter().map(schedule_participant_json).collect::>(), - "action_catalog": [{ - "action_id": action.action_id, - "actor_id": selected_after.agent_id, - "arena": participant_arena(&selected_after), - "participant_kind": participant_kind(&selected_after), - "action_type": action.action_type, - "action_scale": action.action_scale, - "base_recovery": action.base_recovery, - "initiative_cost": action.initiative_cost, - "interruptibility": action.interruptibility, - "commitment": action.commitment, - "local_affordance_basis": action.local_affordance_basis, - }], - "reaction_windows": if work_role.is_some() { - serde_json::json!([{ - "window_id": format!("{}.pending-work", options.schedule_id), - "trigger_event_ref": options.source_scene_ref, - "urgency": urgency, - "eligible_actor_ids": [selected_after.agent_id.clone()], - "allowed_action_scales": ["short", "standard"], - "expires_at": round6(state.scene_clock + 1.0), - "notes": "Pending coordinator work can pull its owning lane forward only if readiness clears threshold." - }]) - } else { - serde_json::json!([]) - }, - "selection_policy": selection_policy_json(&state.selection_policy), - "next_actor_selection": { - "selection_kind": selection_kind, - "selected_actor_id": selected_after.agent_id, - "selected_action_ids": [event.action_id.clone()], - "scene_clock_after_selection": state.scene_clock, - "selection_reason": selection_reason, - "override_reason": null, - "readiness_snapshot": readiness_snapshot, - }, - "review_notes": [ - "Epiphany heartbeat records Resident Self scheduling receipts.", - "A selected idle lane records scheduling opportunity; it does not invent project work or mutate durable memory.", - "When no coordinator work is active, cooldown and sleep pacing keep the swarm from thrashing." - ], - }); - Ok(serde_json::json!({ - "event": history_event_json(event), - "schedule": schedule, - })) -} - -fn complete_pending_turn( - state: &mut EpiphanyHeartbeatStateEntry, - participant_index: usize, -) -> Result { - let pending = state.participants[participant_index] - .pending_turn - .clone() - .ok_or_else(|| { - anyhow!( - "{} has no running heartbeat turn", - state.participants[participant_index].role_id - ) - })?; - if pending.status != "running" { - return Err(anyhow!( - "{} has no running heartbeat turn", - state.participants[participant_index].role_id - )); - } - let scene_clock = state.scene_clock.max(pending.started_scene_clock); - state.participants[participant_index].next_ready_at = round6(scene_clock + pending.recovery); - state.participants[participant_index].last_finished_at = Some(now_iso()); - let mut completed = pending; - completed.status = "completed".to_string(); - completed.completed_at = state.participants[participant_index] - .last_finished_at - .clone(); - completed.completed_scene_clock = Some(round6(scene_clock)); - completed.next_ready_at = Some(state.participants[participant_index].next_ready_at); - state.participants[participant_index].pending_turn = None; - Ok(completed) -} - -fn select_participant( - state: &EpiphanyHeartbeatStateEntry, - work_role: Option<&str>, - urgency: f64, - force_work_role: bool, -) -> Result<(usize, &'static str, String)> { - let active: Vec = state - .participants - .iter() - .enumerate() - .filter_map(|(index, item)| { - (item.status == "active" && !is_turn_pending(item)).then_some(index) - }) - .collect(); - if active.is_empty() { - return Err(anyhow!("heartbeat has no active participants")); - } - if let Some(work_role) = work_role { - let index = participant_index_by_role(state, work_role)?; - let candidate = &state.participants[index]; - if is_turn_pending(candidate) { - let pending = candidate.pending_turn.as_ref(); - return Err(anyhow!( - "{} already has running heartbeat turn {}; complete it before scheduling another", - candidate.display_name, - pending - .map(|item| item.action_id.as_str()) - .unwrap_or("unknown") - )); - } - if force_work_role && candidate.status == "active" { - return Ok(( - index, - "admitted_work", - format!( - "Admitted {work_role} work selected its owning role before idle initiative." - ), - )); - } - let reaction_readiness = candidate.reaction_bias * urgency - candidate.current_load; - if candidate.status == "active" && reaction_readiness >= candidate.interrupt_threshold { - return Ok(( - index, - "reaction_interrupt", - format!( - "{} won a heartbeat reaction window for pending {} work.", - candidate.display_name, work_role - ), - )); - } - } - let selected = active - .into_iter() - .min_by(|left, right| { - let left_item = &state.participants[*left]; - let right_item = &state.participants[*right]; - left_item - .next_ready_at - .total_cmp(&right_item.next_ready_at) - .then_with(|| { - right_item - .initiative_speed - .total_cmp(&left_item.initiative_speed) - }) - .then_with(|| left_item.agent_id.cmp(&right_item.agent_id)) - }) - .expect("active participant exists"); - Ok(( - selected, - "scheduled_turn", - "No pending work cleared a reaction threshold; earliest ready active lane won the heartbeat slot." - .to_string(), - )) -} - -fn action_for_selection( - state: &EpiphanyHeartbeatStateEntry, - selected: &HeartbeatParticipant, - work_role: Option<&str>, - target_heartbeat_rate: f64, -) -> HeartbeatAction { - let minimum_rate = state.pacing_policy.minimum_effective_rate.max(0.001); - if Some(selected.role_id.as_str()) == work_role { - let heartbeat_rate = target_heartbeat_rate.max(minimum_rate); - let action_id = format!("heartbeat.{}.work", selected.role_id); - return HeartbeatAction { - action_id, - action_type: "role_work", - action_scale: "standard", - base_recovery: state.pacing_policy.work_base_recovery / heartbeat_rate, - initiative_cost: 4.0, - interruptibility: 0.45, - commitment: 0.65, - local_affordance_basis: vec![ - format!("Wake {} for admitted Resident Self pressure.", selected.display_name), - "Heartbeat slots control opportunity, not project authority.".to_string(), - "Cooldown starts only after the heartbeat turn completes, so an unfinished sub-agent thread cannot be heartbeaten again.".to_string(), - ], - }; - } - let sleep_multiplier = state - .pacing_policy - .sleep_heartbeat_rate_multiplier - .max(minimum_rate); - let heartbeat_rate = (target_heartbeat_rate * sleep_multiplier).max(minimum_rate); - let action_id = format!("heartbeat.{}.idle", selected.role_id); - HeartbeatAction { - action_id, - action_type: "idle", - action_scale: "short", - base_recovery: state.pacing_policy.idle_base_recovery / heartbeat_rate, - initiative_cost: 1.0, - interruptibility: 0.9, - commitment: 0.25, - local_affordance_basis: vec![ - format!( - "{} has no actionable lane work; preserve the idle scheduling slot without inventing work.", - selected.display_name - ), - "Heartbeat slots control opportunity, not project authority.".to_string(), - "When no coordinator work is active, cooldown and sleep pacing keep the swarm from thrashing.".to_string(), - ], - } -} - -fn patch_missing_participants(state: &mut EpiphanyHeartbeatStateEntry) { - let present: Vec = state - .participants - .iter() - .map(|item| item.role_id.clone()) - .collect(); - for role_id in ROLE_ORDER { - if !present.iter().any(|present| present == role_id) { - state.participants.push(default_participant(role_id)); - } - } - for participant in &mut state.participants { - participant - .pending_turn - .get_or_insert_with(|| HeartbeatPendingTurn { - status: String::new(), - ..HeartbeatPendingTurn::default() - }); - if participant - .pending_turn - .as_ref() - .is_some_and(|turn| turn.status.is_empty()) - { - participant.pending_turn = None; - } - } -} - -fn participant_index_by_role(state: &EpiphanyHeartbeatStateEntry, role_id: &str) -> Result { - state - .participants - .iter() - .position(|item| item.role_id == role_id) - .ok_or_else(|| anyhow!("heartbeat participant role {:?} is missing", role_id)) -} - -fn is_turn_pending(participant: &HeartbeatParticipant) -> bool { - participant - .pending_turn - .as_ref() - .is_some_and(|turn| turn.status == "running") -} - -fn readiness_snapshot( - state: &EpiphanyHeartbeatStateEntry, - work_role: Option<&str>, - urgency: f64, -) -> Vec { - state - .participants - .iter() - .filter(|item| item.status == "active") - .map(|item| { - let pending = is_turn_pending(item); - let eligible = - !pending && Some(item.role_id.as_str()) == work_role && work_role.is_some(); - let reaction_readiness = - eligible.then_some(round6(item.reaction_bias * urgency - item.current_load)); - serde_json::json!({ - "agent_id": item.agent_id, - "arena": participant_arena(item), - "participant_kind": participant_kind(item), - "next_ready_at": item.next_ready_at, - "initiative_frozen": pending, - "freeze_reason": pending.then_some("running_heartbeat_turn"), - "reaction_readiness": reaction_readiness, - "eligible_for_reaction": eligible, - }) - }) - .collect() -} - -pub(super) fn participant_arena(participant: &HeartbeatParticipant) -> &str { - if participant.arena.trim().is_empty() { - HEARTBEAT_ARENA_MAINTENANCE - } else { - participant.arena.as_str() - } -} - -pub(super) fn participant_kind(participant: &HeartbeatParticipant) -> &str { - if participant.participant_kind.trim().is_empty() { - PARTICIPANT_KIND_AGENT - } else { - participant.participant_kind.as_str() - } -} - -fn trim_history(state: &mut EpiphanyHeartbeatStateEntry) { - let len = state.history.len(); - if len > 128 { - state.history.drain(0..(len - 128)); - } -} - -fn write_json_artifact(path: impl AsRef, value: &Value) -> Result<()> { - let path = path.as_ref(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?)) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn parse_heartbeat_time(label: &str, value: &str) -> Result> { - DateTime::parse_from_rfc3339(value) - .map(|time| time.with_timezone(&Utc)) - .with_context(|| format!("failed to parse {label} {value:?} as RFC3339")) -} - -fn now_stamp() -> String { - chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string() -} - -fn now_iso() -> String { - Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) -} - -pub(super) fn round6(value: f64) -> f64 { - (value * 1_000_000.0).round() / 1_000_000.0 -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{PersonaTurnBlockedEvidence, PersonaTurnTerminalOptions}; - use pretty_assertions::assert_eq; - - #[test] - fn resident_pressure_selects_self_before_idle_lane_initiative() -> Result<()> { - let temp = tempfile::tempdir()?; - let heartbeat_store = temp.path().join("heartbeat.cc"); - let resident_store = temp.path().join("resident.cc"); - let artifact_dir = temp.path().join("artifacts"); - initialize_heartbeat_store(&heartbeat_store, 1.0)?; - let mut heartbeat = load_heartbeat_state_entry(&heartbeat_store)?.unwrap(); - heartbeat - .participants - .iter_mut() - .find(|participant| participant.role_id == "coordinator") - .unwrap() - .next_ready_at = 100.0; - write_heartbeat_state_entry(&heartbeat_store, &heartbeat)?; - crate::enqueue_resident_self_pressure( - &resident_store, - &crate::ResidentSelfPressure { - schema_version: crate::RESIDENT_SELF_PRESSURE_SCHEMA_VERSION.into(), - pressure_id: "wake-health".into(), - kind: "operator-objective".into(), - provenance_ref: "test://wake-health".into(), - objective: "Perform a bounded health check.".into(), - created_at_millis: 1, - status: "pending".into(), - consumed_by_grant_id: None, - private_state_exposed: false, - }, - )?; - - let pulse = pulse_resident_self_heartbeat( - &heartbeat_store, - &resident_store, - &artifact_dir, - false, - "wake-health", - "test://wake-health", - )?; - - assert_eq!(pulse.status, "granted"); - assert!(pulse.grant_id.is_some()); - let heartbeat = load_heartbeat_state_entry(&heartbeat_store)?.unwrap(); - assert_eq!( - heartbeat - .participants - .iter() - .find(|participant| participant.role_id == "coordinator") - .and_then(|participant| participant.pending_turn.as_ref()) - .map(|turn| turn.action_id.as_str()), - Some("heartbeat.coordinator.work") - ); - assert!(heartbeat.participants.iter().all(|participant| { - participant.role_id == "coordinator" || participant.pending_turn.is_none() - })); - Ok(()) - } - - #[test] - fn stale_heartbeat_repair_receipt_clears_running_turn() -> Result<()> { - let temp = tempfile::tempdir()?; - let heartbeat_store = temp.path().join("stale-heartbeats.msgpack"); - let resident_store = temp.path().join("resident.cc"); - let artifact_dir = temp.path().join("artifacts"); - initialize_heartbeat_store(&heartbeat_store, 1.0)?; - crate::enqueue_resident_self_pressure( - &resident_store, - &crate::ResidentSelfPressure { - schema_version: crate::RESIDENT_SELF_PRESSURE_SCHEMA_VERSION.into(), - pressure_id: "stale-work".into(), - kind: "operator-objective".into(), - provenance_ref: "test://stale".into(), - objective: "Exercise stale grant recovery.".into(), - created_at_millis: 1, - status: "pending".into(), - consumed_by_grant_id: None, - private_state_exposed: false, - }, - )?; - pulse_resident_self_heartbeat( - &heartbeat_store, - &resident_store, - &artifact_dir, - false, - "stale-work", - "test://stale", - )?; - - let mut state = load_heartbeat_state_entry(&heartbeat_store)?.expect("heartbeat state"); - let coordinator = state - .participants - .iter_mut() - .find(|participant| participant.role_id == "coordinator") - .expect("coordinator participant exists"); - coordinator - .pending_turn - .as_mut() - .expect("running coordinator turn exists") - .started_at = "2026-06-17T00:00:00+00:00".to_string(); - write_heartbeat_state_entry(&heartbeat_store, &state)?; - - let repaired = recover_stale_heartbeat_store( - &heartbeat_store, - &artifact_dir, - HeartbeatStaleTurnRepairOptions { - max_age_seconds: 60, - now_utc: Some("2026-06-17T00:05:00+00:00".to_string()), - reason: "Test stale Resident Self grant recovery.".to_string(), - }, - )?; - assert_eq!(repaired["repaired"], serde_json::json!(1)); - let receipt = load_latest_heartbeat_stale_turn_repair_receipt(&heartbeat_store)? - .expect("stale-turn repair receipt exists"); - assert_eq!(receipt.role_id, "coordinator"); - assert_eq!(receipt.action_id, "heartbeat.coordinator.work"); - assert_eq!(receipt.stale_age_seconds, 300); - assert!(!receipt.private_state_exposed); - - let state = load_heartbeat_state_entry(&heartbeat_store)?.expect("heartbeat state"); - assert!( - state - .participants - .iter() - .find(|participant| participant.role_id == "coordinator") - .and_then(|participant| participant.pending_turn.as_ref()) - .is_none() - ); - Ok(()) - } - - #[test] - fn pending_persona_mention_is_reserved_until_terminal_delivery() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("Persona-heartbeats.msgpack"); - initialize_heartbeat_store(&store_path, 1.0)?; - let queued = crate::queue_persona_social_mention( - &store_path, - crate::PersonaSocialQueueMentionOptions { - target_role_id: "Persona".to_string(), - source_surface: "discord".to_string(), - channel_id: "aquarium".to_string(), - message_id: "m1".to_string(), - author_id: "human".to_string(), - author_name: Some("Metacrat".to_string()), - content: "Epiphany, answer this through the Persona membrane.".to_string(), - visible_prompt: "answer this through the Persona membrane".to_string(), - reply_to_message_id: None, - queued_at: Some("2026-05-24T00:00:00+00:00".to_string()), - mention_id: Some("mention-Persona-test".to_string()), - source_visibility: "public".to_string(), - data_classification: "public_feedback".to_string(), - model_provider_id: "openai-codex".to_string(), - model_provider_disclosure_allowed: true, - }, - )?; - assert_eq!(queued["queued"], true); - - let pulse = crate::pulse_persona_social(&store_path, false)?; - - assert_eq!(pulse["status"], "reserved"); - assert_eq!( - pulse["schedule"]["action_catalog"][0]["pending_mentions"][0]["id"], - "mention-Persona-test" - ); - let mentions = crate::pending_persona_mentions(&store_path)?; - let requests = crate::persona_turn_requests(&store_path)?; - assert!( - mentions.is_empty(), - "reserved mentions are not pending work" - ); - assert_eq!(requests.len(), 1); - let request = &requests[0]; - assert_eq!(request.status, "reserved"); - assert_eq!(request.mentions[0].id, "mention-Persona-test"); - - let terminal = crate::complete_persona_social_turn( - &store_path, - PersonaTurnTerminalOptions { - request_id: request.request_id.clone(), - outcome: "dropped".to_string(), - delivery_evidence: None, - blocked_evidence: None, - }, - )?; - assert_eq!(terminal.mention_disposition, "consumed"); - let replay = crate::complete_persona_social_turn( - &store_path, - PersonaTurnTerminalOptions { - request_id: request.request_id.clone(), - outcome: "dropped".to_string(), - delivery_evidence: None, - blocked_evidence: None, - }, - )?; - assert_eq!(replay, terminal); - assert!(crate::pending_persona_mentions(&store_path)?.is_empty()); - let requests = crate::persona_turn_requests(&store_path)?; - assert_eq!(requests[0].status, "terminal"); - assert!(requests[0].terminal_receipt.is_some()); - Ok(()) - } - - #[test] - fn failed_persona_turn_releases_lane_but_retains_mention_for_retry() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("Persona-heartbeats.msgpack"); - initialize_heartbeat_store(&store_path, 1.0)?; - crate::queue_persona_social_mention( - &store_path, - crate::PersonaSocialQueueMentionOptions { - target_role_id: "Persona".to_string(), - source_surface: "discord".to_string(), - channel_id: "aquarium".to_string(), - message_id: "m-failure".to_string(), - author_id: "human".to_string(), - author_name: None, - content: "Retain this pressure across a failed turn.".to_string(), - visible_prompt: "retain this pressure across failure".to_string(), - reply_to_message_id: None, - queued_at: Some("2026-05-24T00:00:00+00:00".to_string()), - mention_id: Some("mention-Persona-failure".to_string()), - source_visibility: "public".to_string(), - data_classification: "public_feedback".to_string(), - model_provider_id: "openai-codex".to_string(), - model_provider_disclosure_allowed: true, - }, - )?; - crate::pulse_persona_social(&store_path, false)?; - let request_id = crate::persona_turn_requests(&store_path)?[0] - .request_id - .clone(); - let terminal = crate::complete_persona_social_turn( - &store_path, - PersonaTurnTerminalOptions { - request_id, - outcome: "failed".to_string(), - delivery_evidence: None, - blocked_evidence: None, - }, - )?; - assert_eq!(terminal.mention_disposition, "retained"); - assert_eq!(crate::pending_persona_mentions(&store_path)?.len(), 1); - assert_eq!( - crate::persona_turn_requests(&store_path)?[0].status, - "terminal" - ); - Ok(()) - } - - #[test] - fn indeterminate_local_effect_quarantines_pressure_and_cannot_reschedule() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("Persona-heartbeats.msgpack"); - initialize_heartbeat_store(&store_path, 1.0)?; - crate::queue_persona_social_mention( - &store_path, - crate::PersonaSocialQueueMentionOptions { - target_role_id: "Persona".to_string(), - source_surface: "bifrost-discord".to_string(), - channel_id: "aquarium".to_string(), - message_id: "m-unknown".to_string(), - author_id: "human".to_string(), - author_name: None, - content: "Do not answer this twice.".to_string(), - visible_prompt: "do not answer this twice".to_string(), - reply_to_message_id: Some("m-unknown".to_string()), - queued_at: Some("2026-05-24T00:00:00+00:00".to_string()), - mention_id: Some("mention-Persona-unknown".to_string()), - source_visibility: "public".to_string(), - data_classification: "public_feedback".to_string(), - model_provider_id: "openai-codex".to_string(), - model_provider_disclosure_allowed: true, - }, - )?; - let pulse = crate::pulse_persona_social(&store_path, false)?; - let request_id = pulse["requestId"].as_str().expect("request id").to_string(); - assert!( - crate::complete_persona_social_turn( - &store_path, - PersonaTurnTerminalOptions { - request_id: request_id.clone(), - outcome: "blocked".to_string(), - delivery_evidence: None, - blocked_evidence: None, - }, - ) - .is_err() - ); - let terminal = crate::complete_persona_social_turn( - &store_path, - PersonaTurnTerminalOptions { - request_id, - outcome: "blocked".to_string(), - delivery_evidence: None, - blocked_evidence: Some(PersonaTurnBlockedEvidence { - evidence_source: "local_effect".to_string(), - crossing_status: "unknown".to_string(), - reason: "Mind mutation may have occurred before the local journal committed" - .to_string(), - crossing_receipt_id: None, - bridge_receipt_sha256: None, - }), - }, - )?; - assert_eq!(terminal.mention_disposition, "quarantined"); - let next = crate::pulse_persona_social(&store_path, false)?; - assert_eq!(next["status"], "idle"); - assert!(crate::pending_persona_mentions(&store_path)?.is_empty()); - assert_eq!(crate::persona_turn_requests(&store_path)?.len(), 1); - let blocked = crate::blocked_persona_pressures(&store_path)?; - assert_eq!(blocked.len(), 1); - assert_eq!(blocked[0].mentions[0].id, "mention-Persona-unknown"); - Ok(()) - } - - #[test] - fn persona_social_pulse_reserves_once_and_obeys_brake() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("Persona-heartbeats.msgpack"); - initialize_heartbeat_store(&store_path, 1.0)?; - crate::queue_persona_social_mention( - &store_path, - crate::PersonaSocialQueueMentionOptions { - target_role_id: "Persona".to_string(), - source_surface: "bifrost-discord".to_string(), - channel_id: "aquarium".to_string(), - message_id: "m-pulse".to_string(), - author_id: "human".to_string(), - author_name: None, - content: "Wake the Persona exactly once.".to_string(), - visible_prompt: "wake the Persona exactly once".to_string(), - reply_to_message_id: Some("m-pulse".to_string()), - queued_at: Some("2026-05-24T00:00:00+00:00".to_string()), - mention_id: Some("mention-Persona-pulse".to_string()), - source_visibility: "public".to_string(), - data_classification: "public_feedback".to_string(), - model_provider_id: "openai-codex".to_string(), - model_provider_disclosure_allowed: true, - }, - )?; - let braked = crate::pulse_persona_social(&store_path, true)?; - assert_eq!(braked["status"], "refused-by-swarm-brake"); - assert!(crate::persona_turn_requests(&store_path)?.is_empty()); - - let first = crate::pulse_persona_social(&store_path, false)?; - assert_eq!(first["status"], "reserved"); - let second = crate::pulse_persona_social(&store_path, false)?; - assert_eq!(second["status"], "already-running"); - assert_eq!(crate::persona_turn_requests(&store_path)?.len(), 1); - Ok(()) - } -} diff --git a/epiphany-core/src/heartbeat_state/heartbeat_documents.rs b/epiphany-core/src/heartbeat_state/heartbeat_documents.rs deleted file mode 100644 index c245b166..00000000 --- a/epiphany-core/src/heartbeat_state/heartbeat_documents.rs +++ /dev/null @@ -1,264 +0,0 @@ -use cultcache_rs::DatabaseEntry; -use serde::Deserialize; -use serde::Serialize; - -pub const HEARTBEAT_STATE_TYPE: &str = "epiphany.agent_heartbeat"; -pub const HEARTBEAT_STATE_KEY: &str = "default"; -pub const HEARTBEAT_STATE_SCHEMA_VERSION: &str = "epiphany.agent_heartbeat.v1"; -pub const HEARTBEAT_STATUS_SCHEMA_VERSION: &str = "epiphany.agent_heartbeat_status.v0"; -pub const INITIATIVE_SCHEMA_VERSION: &str = "epiphany.heartbeat_schedule.v0"; -pub const HEARTBEAT_STALE_TURN_REPAIR_TYPE: &str = "epiphany.heartbeat.stale_turn_repair"; -pub const HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION: &str = - "epiphany.heartbeat.stale_turn_repair.v0"; -pub const HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY: &str = "heartbeat/stale-turn-repair/latest"; -pub const HEARTBEAT_ARTIFACT_RETENTION_PLAN_TYPE: &str = - "epiphany.heartbeat.artifact_retention_plan"; -pub const HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION: &str = - "epiphany.heartbeat.artifact_retention_plan.v0"; -pub const HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY: &str = - "heartbeat/artifact-retention-plan/latest"; -pub const HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_TYPE: &str = - "epiphany.heartbeat.artifact_retention_receipt"; -pub const HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_SCHEMA_VERSION: &str = - "epiphany.heartbeat.artifact_retention_receipt.v0"; -pub const HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY: &str = - "heartbeat/artifact-retention-receipt/latest"; - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "epiphany.agent_heartbeat", - schema = "EpiphanyHeartbeatStateEntry" -)] -pub struct EpiphanyHeartbeatStateEntry { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub target_heartbeat_rate: f64, - #[cultcache(key = 2)] - pub scene_clock: f64, - #[cultcache(key = 3)] - pub selection_policy: HeartbeatSelectionPolicy, - #[cultcache(key = 4)] - pub pacing_policy: HeartbeatPacingPolicy, - #[cultcache(key = 5)] - pub participants: Vec, - #[cultcache(key = 6)] - pub history: Vec, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "epiphany.heartbeat.stale_turn_repair", - schema = "EpiphanyHeartbeatStaleTurnRepairReceipt" -)] -pub struct EpiphanyHeartbeatStaleTurnRepairReceipt { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub receipt_id: String, - #[cultcache(key = 2)] - pub repaired_at_utc: String, - #[cultcache(key = 3)] - pub role_id: String, - #[cultcache(key = 4)] - pub agent_id: String, - #[cultcache(key = 5)] - pub action_id: String, - #[cultcache(key = 6)] - pub schedule_id: String, - #[cultcache(key = 7)] - pub started_at_utc: String, - #[cultcache(key = 8)] - pub stale_age_seconds: i64, - #[cultcache(key = 9)] - pub reason: String, - #[cultcache(key = 10)] - pub resulting_status: String, - #[cultcache(key = 11)] - pub next_ready_at: f64, - #[cultcache(key = 12)] - pub private_state_exposed: bool, - #[cultcache(key = 13)] - pub notes: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct HeartbeatArtifactRetentionMember { - pub directory_name: String, - pub manifest_sha256: String, - pub file_count: u64, - pub byte_count: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "epiphany.heartbeat.artifact_retention_plan", - schema = "EpiphanyHeartbeatArtifactRetentionPlan" -)] -pub struct EpiphanyHeartbeatArtifactRetentionPlan { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub plan_id: String, - #[cultcache(key = 2)] - pub artifact_root: String, - #[cultcache(key = 3)] - pub retain_pulse_count: u64, - #[cultcache(key = 4)] - pub batch_size: u64, - #[cultcache(key = 5)] - pub members: Vec, - #[cultcache(key = 6)] - pub planned_at_utc: String, - #[cultcache(key = 7)] - pub private_state_exposed: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "epiphany.heartbeat.artifact_retention_receipt", - schema = "EpiphanyHeartbeatArtifactRetentionReceipt" -)] -pub struct EpiphanyHeartbeatArtifactRetentionReceipt { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub receipt_id: String, - #[cultcache(key = 2)] - pub plan_id: String, - #[cultcache(key = 3)] - pub status: String, - #[cultcache(key = 4)] - pub deleted_directories: Vec, - #[cultcache(key = 5)] - pub deleted_file_count: u64, - #[cultcache(key = 6)] - pub deleted_byte_count: u64, - #[cultcache(key = 7)] - pub completed_at_utc: String, - #[cultcache(key = 8)] - pub private_state_exposed: bool, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatSelectionPolicy { - pub mode: String, - pub reaction_precedence: bool, - pub minimum_speed: f64, - pub tie_breakers: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatPacingPolicy { - pub cooldown_starts_after_turn_completion: bool, - pub work_base_recovery: f64, - pub idle_base_recovery: f64, - pub sleep_heartbeat_rate_multiplier: f64, - pub minimum_effective_rate: f64, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatParticipant { - pub agent_id: String, - pub role_id: String, - pub display_name: String, - #[serde(default)] - pub arena: String, - #[serde(default)] - pub participant_kind: String, - pub initiative_speed: f64, - pub next_ready_at: f64, - pub reaction_bias: f64, - pub interrupt_threshold: f64, - pub current_load: f64, - pub status: String, - pub constraints: Vec, - pub last_action_id: Option, - pub last_woke_at: Option, - pub last_finished_at: Option, - pub pending_turn: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatPendingTurn { - pub status: String, - #[serde(rename = "scheduleId")] - pub schedule_id: String, - #[serde(rename = "actionId")] - pub action_id: String, - #[serde(rename = "actionType")] - pub action_type: String, - #[serde(rename = "actionScale", default)] - pub action_scale: String, - #[serde(rename = "localAffordanceBasis", default)] - pub local_affordance_basis: Vec, - #[serde(rename = "startedAt")] - pub started_at: String, - #[serde(rename = "startedSceneClock")] - pub started_scene_clock: f64, - #[serde(rename = "baseRecovery")] - pub base_recovery: f64, - pub recovery: f64, - #[serde(rename = "cooldownPolicy")] - pub cooldown_policy: String, - #[serde(rename = "completedAt", default)] - pub completed_at: Option, - #[serde(rename = "completedSceneClock", default)] - pub completed_scene_clock: Option, - #[serde(rename = "nextReadyAt", default)] - pub next_ready_at: Option, - #[serde(rename = "initiativeFrozen", default)] - pub initiative_frozen: bool, - #[serde(rename = "initiativeFreezeReason", default)] - pub initiative_freeze_reason: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatHistoryEvent { - pub ts: String, - #[serde(rename = "scheduleId")] - pub schedule_id: String, - #[serde(rename = "selectedRole")] - pub selected_role: String, - #[serde(rename = "selectedAgentId")] - pub selected_agent_id: String, - #[serde(rename = "actionId")] - pub action_id: String, - #[serde(rename = "actionType")] - pub action_type: String, - #[serde(default)] - pub arena: String, - #[serde(rename = "participantKind", default)] - pub participant_kind: String, - #[serde(rename = "actionScale", default)] - pub action_scale: String, - #[serde(rename = "coordinatorAction", default)] - pub coordinator_action: Option, - #[serde(rename = "targetRole", default)] - pub target_role: Option, - #[serde(rename = "workRole", default)] - pub work_role: Option, - #[serde(rename = "sceneClock", default)] - pub scene_clock: Option, - #[serde(rename = "nextReadyAt", default)] - pub next_ready_at: Option, - #[serde(rename = "turnStatus", default)] - pub turn_status: Option, - #[serde(rename = "cooldownStartedAfterCompletion", default)] - pub cooldown_started_after_completion: Option, -} - -#[derive(Clone, Debug, PartialEq)] -pub(super) struct HeartbeatTickOptions { - pub(super) target_heartbeat_rate: f64, - pub(super) schedule_id: String, - pub(super) source_scene_ref: String, - pub(super) resident_self_store: Option, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct HeartbeatStaleTurnRepairOptions { - pub max_age_seconds: i64, - pub now_utc: Option, - pub reason: String, -} diff --git a/epiphany-core/src/heartbeat_state/heartbeat_projection.rs b/epiphany-core/src/heartbeat_state/heartbeat_projection.rs deleted file mode 100644 index a158ea36..00000000 --- a/epiphany-core/src/heartbeat_state/heartbeat_projection.rs +++ /dev/null @@ -1,275 +0,0 @@ -use super::HEARTBEAT_STATE_KEY; -use super::HEARTBEAT_STATE_TYPE; -use super::HEARTBEAT_STATUS_SCHEMA_VERSION; -use super::HeartbeatHistoryEvent; -use super::HeartbeatParticipant; -use super::HeartbeatPendingTurn; -use super::HeartbeatSelectionPolicy; -use super::load_heartbeat_state_entry; -use super::participant_arena; -use super::participant_kind; -use anyhow::Result; -use serde_json::Value; -use std::cmp::Reverse; -use std::fs; -use std::path::Path; - -pub fn heartbeat_status_projection( - store_path: impl AsRef, - artifact_dir: impl AsRef, - target_heartbeat_rate: f64, - artifact_limit: usize, -) -> Result { - let store_path = store_path.as_ref(); - let Some(state) = load_heartbeat_state_entry(store_path)? else { - return Ok(serde_json::json!({ - "schema_version": HEARTBEAT_STATUS_SCHEMA_VERSION, - "ok": true, - "status": "missing", - "schedulerStatus": "missing", - "stateFile": null, - "storeFile": store_path, - "cultCacheStore": cultcache_status(store_path), - "artifactDir": artifact_dir.as_ref(), - "targetHeartbeatRate": if target_heartbeat_rate > 0.0 { Some(target_heartbeat_rate) } else { None }, - "sceneClock": null, - "participants": [], - "latestEvent": null, - "history": [], - "latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit), - "availableActions": ["init", "repair-stale", "retain-artifacts", "status", "serve"], - })); - }; - let scheduler_status = if state.participants.is_empty() { - "unconfigured" - } else if state.participants.iter().all(|participant| { - participant.status == "active" - && participant - .pending_turn - .as_ref() - .is_none_or(|turn| turn.status == "running") - }) { - "active" - } else { - "attention" - }; - let history: Vec<_> = state - .history - .iter() - .rev() - .take(artifact_limit) - .cloned() - .collect::>() - .into_iter() - .rev() - .map(history_event_json) - .collect(); - Ok(serde_json::json!({ - "schema_version": HEARTBEAT_STATUS_SCHEMA_VERSION, - "ok": true, - "status": "loaded", - "schedulerStatus": scheduler_status, - "stateFile": null, - "storeFile": store_path, - "cultCacheStore": cultcache_status(store_path), - "artifactDir": artifact_dir.as_ref(), - "targetHeartbeatRate": state.target_heartbeat_rate, - "sceneClock": state.scene_clock, - "participants": state.participants.iter().map(participant_status_json).collect::>(), - "latestEvent": history.last().cloned(), - "history": history, - "latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit), - "availableActions": ["init", "repair-stale", "retain-artifacts", "status", "serve"], - })) -} - -fn latest_json_artifacts(artifact_dir: impl AsRef, limit: usize) -> Vec { - let artifact_dir = artifact_dir.as_ref(); - let Ok(read_dir) = fs::read_dir(artifact_dir) else { - return Vec::new(); - }; - let mut paths: Vec<_> = read_dir - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| { - path.extension() - .is_some_and(|extension| extension == "json") - }) - .filter_map(|path| { - let modified = path.metadata().and_then(|meta| meta.modified()).ok()?; - Some((path, modified)) - }) - .collect(); - paths.sort_by_key(|item| Reverse(item.1)); - paths - .into_iter() - .take(limit) - .filter_map(|(path, modified)| { - let raw = fs::read_to_string(&path).ok()?; - let payload: Value = serde_json::from_str(&raw).ok()?; - let modified_at: chrono::DateTime = modified.into(); - Some(serde_json::json!({ - "path": path, - "name": path.file_name().and_then(|name| name.to_str()), - "modifiedAt": modified_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), - "schemaVersion": payload.get("schema_version"), - "kind": path.file_stem().and_then(|stem| stem.to_str()).and_then(|stem| stem.rsplit('.').next()).unwrap_or("json"), - "summary": artifact_summary(&payload), - })) - }) - .collect() -} - -fn artifact_summary(payload: &Value) -> Value { - let event = if payload.get("actionId").is_some() { - Some(payload) - } else { - payload.get("event") - }; - if let Some(event) = event { - return serde_json::json!({ - "selectedRole": event.get("selectedRole"), - "actionType": event.get("actionType"), - "actionId": event.get("actionId"), - "coordinatorAction": event.get("coordinatorAction"), - }); - } - if let Some(selection) = payload - .get("next_actor_selection") - .or_else(|| payload.get("nextActorSelection")) - { - return serde_json::json!({ - "selectionKind": selection.get("selection_kind").or_else(|| selection.get("selectionKind")), - "selectedActorId": selection.get("selected_actor_id").or_else(|| selection.get("selectedActorId")), - }); - } - let keys = payload - .as_object() - .map(|object| object.keys().take(8).cloned().collect::>()) - .unwrap_or_default(); - serde_json::json!({ "keys": keys }) -} - -fn cultcache_status(store_path: &Path) -> Value { - let metadata = store_path.metadata().ok(); - let modified_at = metadata - .as_ref() - .and_then(|meta| meta.modified().ok()) - .map(|time| { - let time: chrono::DateTime = time.into(); - time.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) - }); - serde_json::json!({ - "storeFile": store_path, - "present": metadata.is_some(), - "sizeBytes": metadata.as_ref().map(|meta| meta.len()), - "modifiedAt": modified_at, - "entryType": HEARTBEAT_STATE_TYPE, - "entryKey": HEARTBEAT_STATE_KEY, - }) -} - -fn participant_status_json(participant: &HeartbeatParticipant) -> Value { - serde_json::json!({ - "agentId": participant.agent_id, - "roleId": participant.role_id, - "displayName": participant.display_name, - "arena": participant_arena(participant), - "participantKind": participant_kind(participant), - "initiativeSpeed": participant.initiative_speed, - "initiativeFrozen": participant - .pending_turn - .as_ref() - .is_some_and(|turn| turn.status == "running"), - "initiativeFreezeReason": participant - .pending_turn - .as_ref() - .and_then(|turn| turn.initiative_freeze_reason.as_ref()), - "nextReadyAt": participant.next_ready_at, - "reactionBias": participant.reaction_bias, - "interruptThreshold": participant.interrupt_threshold, - "currentLoad": participant.current_load, - "status": participant.status, - "lastActionId": participant.last_action_id, - "lastWokeAt": participant.last_woke_at, - "lastFinishedAt": participant.last_finished_at, - "pendingTurn": participant.pending_turn.as_ref().map(pending_turn_json), - }) -} - -pub(super) fn schedule_participant_json(participant: &HeartbeatParticipant) -> Value { - serde_json::json!({ - "agent_id": participant.agent_id, - "role_id": participant.role_id, - "display_name": participant.display_name, - "arena": participant_arena(participant), - "participant_kind": participant_kind(participant), - "initiative_speed": participant.initiative_speed, - "initiative_frozen": participant - .pending_turn - .as_ref() - .is_some_and(|turn| turn.status == "running"), - "initiative_freeze_reason": participant - .pending_turn - .as_ref() - .and_then(|turn| turn.initiative_freeze_reason.as_ref()), - "next_ready_at": participant.next_ready_at, - "reaction_bias": participant.reaction_bias, - "interrupt_threshold": participant.interrupt_threshold, - "current_load": participant.current_load, - "status": participant.status, - "pending_turn": participant.pending_turn.as_ref().map(pending_turn_json), - "constraints": participant.constraints, - }) -} - -pub(super) fn selection_policy_json(policy: &HeartbeatSelectionPolicy) -> Value { - serde_json::json!({ - "mode": policy.mode, - "reaction_precedence": policy.reaction_precedence, - "minimum_speed": policy.minimum_speed, - "tie_breakers": policy.tie_breakers, - }) -} - -pub(super) fn pending_turn_json(turn: &HeartbeatPendingTurn) -> Value { - serde_json::json!({ - "status": turn.status, - "scheduleId": turn.schedule_id, - "actionId": turn.action_id, - "actionType": turn.action_type, - "actionScale": turn.action_scale, - "localAffordanceBasis": turn.local_affordance_basis, - "startedAt": turn.started_at, - "startedSceneClock": turn.started_scene_clock, - "baseRecovery": turn.base_recovery, - "initiativeFrozen": turn.initiative_frozen, - "initiativeFreezeReason": turn.initiative_freeze_reason.as_ref(), - "recovery": turn.recovery, - "cooldownPolicy": turn.cooldown_policy, - "completedAt": turn.completed_at, - "completedSceneClock": turn.completed_scene_clock, - "nextReadyAt": turn.next_ready_at, - }) -} - -pub(super) fn history_event_json(event: HeartbeatHistoryEvent) -> Value { - serde_json::json!({ - "ts": event.ts, - "scheduleId": event.schedule_id, - "selectedRole": event.selected_role, - "selectedAgentId": event.selected_agent_id, - "actionId": event.action_id, - "actionType": event.action_type, - "arena": event.arena, - "participantKind": event.participant_kind, - "actionScale": event.action_scale, - "coordinatorAction": event.coordinator_action, - "targetRole": event.target_role, - "workRole": event.work_role, - "sceneClock": event.scene_clock, - "nextReadyAt": event.next_ready_at, - "turnStatus": event.turn_status, - "cooldownStartedAfterCompletion": event.cooldown_started_after_completion, - }) -} diff --git a/epiphany-core/src/heartbeat_state/heartbeat_retention.rs b/epiphany-core/src/heartbeat_state/heartbeat_retention.rs deleted file mode 100644 index f5d8f392..00000000 --- a/epiphany-core/src/heartbeat_state/heartbeat_retention.rs +++ /dev/null @@ -1,472 +0,0 @@ -use super::{ - EpiphanyHeartbeatArtifactRetentionPlan, EpiphanyHeartbeatArtifactRetentionReceipt, - HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY, HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION, - HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY, - HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_SCHEMA_VERSION, HeartbeatArtifactRetentionMember, - heartbeat_state_cache, -}; -use anyhow::{Context, Result, anyhow, bail}; -use cultcache_rs::{DatabaseEntry, SingleFileMessagePackBackingStore}; -use sha2::{Digest, Sha256}; -use std::fs; -use std::path::{Path, PathBuf}; - -pub fn retain_heartbeat_pulse_artifacts( - store_path: impl AsRef, - artifact_root: impl AsRef, - retain_pulse_count: usize, - batch_size: usize, - completed_at_utc: &str, -) -> Result> { - if retain_pulse_count == 0 || batch_size == 0 { - bail!("heartbeat artifact retention requires non-zero retain and batch counts"); - } - if completed_at_utc.trim().is_empty() { - bail!("heartbeat artifact retention requires completion time"); - } - let artifact_root = artifact_root - .as_ref() - .canonicalize() - .with_context(|| format!("failed to resolve {}", artifact_root.as_ref().display()))?; - if !artifact_root.is_dir() { - bail!("heartbeat artifact root is not a directory"); - } - - if let Some(pending) = pending_retention_plan(store_path.as_ref())? { - if PathBuf::from(&pending.artifact_root) != artifact_root { - bail!("pending heartbeat retention plan belongs to another artifact root"); - } - return reconcile_retention_plan( - store_path.as_ref(), - &artifact_root, - &pending, - completed_at_utc, - ) - .map(Some); - } - - let pulses = pulse_directories(&artifact_root)?; - let threshold = retain_pulse_count - .checked_add(batch_size) - .ok_or_else(|| anyhow!("heartbeat artifact retention threshold overflow"))?; - if pulses.len() <= threshold { - return Ok(None); - } - let retire_count = batch_size.min(pulses.len() - retain_pulse_count); - let members = pulses - .iter() - .take(retire_count) - .map(|(_, path)| artifact_member(&artifact_root, path)) - .collect::>>()?; - let plan_id = retention_plan_id(&artifact_root, retain_pulse_count, batch_size, &members); - let plan = EpiphanyHeartbeatArtifactRetentionPlan { - schema_version: HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION.to_string(), - plan_id, - artifact_root: artifact_root.display().to_string(), - retain_pulse_count: retain_pulse_count as u64, - batch_size: batch_size as u64, - members, - planned_at_utc: completed_at_utc.to_string(), - private_state_exposed: false, - }; - validate_retention_plan(&plan)?; - write_retention_plan(store_path.as_ref(), &plan)?; - reconcile_retention_plan(store_path.as_ref(), &artifact_root, &plan, completed_at_utc).map(Some) -} - -fn pending_retention_plan( - store_path: &Path, -) -> Result> { - let cache = heartbeat_state_cache(store_path)?; - let plan = cache.get::( - HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY, - )?; - let receipt = cache.get::( - HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY, - )?; - Ok(plan.filter(|plan| { - receipt - .as_ref() - .is_none_or(|receipt| receipt.plan_id != plan.plan_id) - })) -} - -fn reconcile_retention_plan( - store_path: &Path, - artifact_root: &Path, - plan: &EpiphanyHeartbeatArtifactRetentionPlan, - completed_at_utc: &str, -) -> Result { - validate_retention_plan(plan)?; - for member in &plan.members { - let path = artifact_root.join(&member.directory_name); - if !path.exists() { - continue; - } - let observed = artifact_member(artifact_root, &path)?; - if observed != *member { - bail!( - "heartbeat artifact {:?} changed after retention planning", - member.directory_name - ); - } - fs::remove_dir_all(&path) - .with_context(|| format!("failed to retire heartbeat artifact {}", path.display()))?; - } - for member in &plan.members { - if artifact_root.join(&member.directory_name).exists() { - bail!("heartbeat artifact retirement did not remove planned directory"); - } - } - let receipt = EpiphanyHeartbeatArtifactRetentionReceipt { - schema_version: HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_SCHEMA_VERSION.to_string(), - receipt_id: format!("artifact-retention-receipt-{}", plan.plan_id), - plan_id: plan.plan_id.clone(), - status: "completed".to_string(), - deleted_directories: plan - .members - .iter() - .map(|member| member.directory_name.clone()) - .collect(), - deleted_file_count: plan.members.iter().map(|member| member.file_count).sum(), - deleted_byte_count: plan.members.iter().map(|member| member.byte_count).sum(), - completed_at_utc: completed_at_utc.to_string(), - private_state_exposed: false, - }; - validate_retention_receipt(&receipt, plan)?; - write_retention_receipt(store_path, &receipt)?; - Ok(receipt) -} - -fn pulse_directories(root: &Path) -> Result> { - let mut pulses = Vec::new(); - for entry in fs::read_dir(root)? { - let entry = entry?; - let file_type = entry.file_type()?; - if !file_type.is_dir() { - continue; - } - if file_type.is_symlink() { - bail!("heartbeat artifact retention refuses symlink directories"); - } - let name = entry - .file_name() - .into_string() - .map_err(|_| anyhow!("heartbeat artifact directory name is not UTF-8"))?; - let sequence = pulse_sequence(&name) - .ok_or_else(|| anyhow!("heartbeat artifact retention refuses directory {name:?}"))?; - pulses.push((sequence, entry.path())); - } - pulses.sort_by_key(|(sequence, _)| *sequence); - if pulses.windows(2).any(|pair| pair[0].0 == pair[1].0) { - bail!("heartbeat artifact root contains duplicate pulse sequences"); - } - Ok(pulses) -} - -fn pulse_sequence(name: &str) -> Option { - let digits = name.strip_prefix("pulse-")?; - (digits.len() == 6 && digits.bytes().all(|byte| byte.is_ascii_digit())) - .then(|| digits.parse().ok()) - .flatten() -} - -fn artifact_member(root: &Path, directory: &Path) -> Result { - let name = directory - .file_name() - .and_then(|value| value.to_str()) - .ok_or_else(|| anyhow!("heartbeat artifact directory has no UTF-8 name"))?; - pulse_sequence(name).ok_or_else(|| anyhow!("invalid heartbeat pulse directory {name:?}"))?; - let canonical = directory.canonicalize()?; - if canonical.parent() != Some(root) { - bail!("heartbeat artifact directory escapes its canonical root"); - } - let mut rows = Vec::new(); - collect_manifest_rows(&canonical, &canonical, &mut rows)?; - rows.sort(); - let mut digest = Sha256::new(); - let mut file_count = 0_u64; - let mut byte_count = 0_u64; - for row in rows { - if row.kind == b'F' { - file_count += 1; - byte_count = byte_count - .checked_add(row.byte_count) - .ok_or_else(|| anyhow!("heartbeat artifact byte count overflow"))?; - } - digest.update([row.kind]); - digest.update((row.relative_path.len() as u64).to_be_bytes()); - digest.update(row.relative_path.as_bytes()); - digest.update(row.byte_count.to_be_bytes()); - digest.update(row.sha256.as_bytes()); - } - Ok(HeartbeatArtifactRetentionMember { - directory_name: name.to_string(), - manifest_sha256: format!("sha256-{:x}", digest.finalize()), - file_count, - byte_count, - }) -} - -#[derive(Eq, Ord, PartialEq, PartialOrd)] -struct ManifestRow { - kind: u8, - relative_path: String, - byte_count: u64, - sha256: String, -} - -fn collect_manifest_rows(root: &Path, directory: &Path, rows: &mut Vec) -> Result<()> { - for entry in fs::read_dir(directory)? { - let entry = entry?; - let file_type = entry.file_type()?; - if file_type.is_symlink() { - bail!("heartbeat artifact retention refuses symlinks"); - } - let path = entry.path(); - let relative = path - .strip_prefix(root)? - .to_string_lossy() - .replace('\\', "/"); - if file_type.is_dir() { - rows.push(ManifestRow { - kind: b'D', - relative_path: relative, - byte_count: 0, - sha256: String::new(), - }); - collect_manifest_rows(root, &path, rows)?; - } else if file_type.is_file() { - let bytes = fs::read(&path)?; - rows.push(ManifestRow { - kind: b'F', - relative_path: relative, - byte_count: bytes.len() as u64, - sha256: format!("sha256-{:x}", Sha256::digest(bytes)), - }); - } else { - bail!("heartbeat artifact retention refuses non-file artifacts"); - } - } - Ok(()) -} - -fn retention_plan_id( - root: &Path, - retain_pulse_count: usize, - batch_size: usize, - members: &[HeartbeatArtifactRetentionMember], -) -> String { - let mut digest = Sha256::new(); - digest.update(b"epiphany.heartbeat.artifact-retention-plan.v0\0"); - digest.update(root.to_string_lossy().as_bytes()); - digest.update((retain_pulse_count as u64).to_be_bytes()); - digest.update((batch_size as u64).to_be_bytes()); - for member in members { - digest.update(member.directory_name.as_bytes()); - digest.update(member.manifest_sha256.as_bytes()); - digest.update(member.file_count.to_be_bytes()); - digest.update(member.byte_count.to_be_bytes()); - } - format!("sha256-{:x}", digest.finalize()) -} - -fn validate_retention_plan(plan: &EpiphanyHeartbeatArtifactRetentionPlan) -> Result<()> { - if plan.schema_version != HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION - || plan.private_state_exposed - || plan.plan_id.trim().is_empty() - || plan.artifact_root.trim().is_empty() - || plan.retain_pulse_count == 0 - || plan.batch_size == 0 - || plan.members.is_empty() - || plan.planned_at_utc.trim().is_empty() - { - bail!("invalid heartbeat artifact retention plan"); - } - if plan.members.len() as u64 > plan.batch_size { - bail!("heartbeat artifact retention plan exceeds its batch size"); - } - let mut names = std::collections::BTreeSet::new(); - for member in &plan.members { - if pulse_sequence(&member.directory_name).is_none() - || !member.manifest_sha256.starts_with("sha256-") - || !names.insert(member.directory_name.as_str()) - { - bail!("invalid heartbeat artifact retention member"); - } - } - Ok(()) -} - -fn validate_retention_receipt( - receipt: &EpiphanyHeartbeatArtifactRetentionReceipt, - plan: &EpiphanyHeartbeatArtifactRetentionPlan, -) -> Result<()> { - if receipt.schema_version != HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_SCHEMA_VERSION - || receipt.private_state_exposed - || receipt.status != "completed" - || receipt.plan_id != plan.plan_id - || receipt.deleted_directories - != plan - .members - .iter() - .map(|member| member.directory_name.clone()) - .collect::>() - || receipt.completed_at_utc.trim().is_empty() - { - bail!("invalid heartbeat artifact retention receipt"); - } - Ok(()) -} - -fn write_retention_plan( - store_path: &Path, - plan: &EpiphanyHeartbeatArtifactRetentionPlan, -) -> Result<()> { - let cache = heartbeat_state_cache(store_path)?; - let (plan_entry, _) = cache.prepare_entry(&plan.plan_id, plan)?; - let (latest_entry, _) = - cache.prepare_entry(HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY, plan)?; - let expected = cache - .snapshot_envelopes() - .into_iter() - .filter(|entry| { - entry.r#type == ::TYPE - && (entry.key == plan.plan_id - || entry.key == HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY) - }) - .collect::>(); - if !SingleFileMessagePackBackingStore::new(store_path) - .compare_and_swap_batch(&expected, vec![plan_entry, latest_entry])? - { - bail!("heartbeat artifact retention plan lost exact compare-and-swap"); - } - Ok(()) -} - -fn write_retention_receipt( - store_path: &Path, - receipt: &EpiphanyHeartbeatArtifactRetentionReceipt, -) -> Result<()> { - let cache = heartbeat_state_cache(store_path)?; - if let Some(existing) = - cache.get::(&receipt.receipt_id)? - { - if existing == *receipt { - return Ok(()); - } - bail!("heartbeat artifact retention receipt identity collision"); - } - let (receipt_entry, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; - let (latest_entry, _) = - cache.prepare_entry(HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY, receipt)?; - let expected = cache - .snapshot_envelopes() - .into_iter() - .filter(|entry| { - entry.r#type == ::TYPE - && (entry.key == receipt.receipt_id - || entry.key == HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY) - }) - .collect::>(); - if !SingleFileMessagePackBackingStore::new(store_path) - .compare_and_swap_batch(&expected, vec![receipt_entry, latest_entry])? - { - bail!("heartbeat artifact retention receipt lost exact compare-and-swap"); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::initialize_heartbeat_store; - - fn pulse(root: &Path, sequence: u64, content: &str) -> Result { - let path = root.join(format!("pulse-{sequence:06}")); - fs::create_dir_all(path.join("nested"))?; - fs::write(path.join("nested").join("artifact.json"), content)?; - Ok(path) - } - - #[test] - fn retention_is_receipted_bounded_and_idempotent() -> Result<()> { - let temp = tempfile::tempdir()?; - let store = temp.path().join("heartbeat.cc"); - let artifacts = temp.path().join("artifacts"); - fs::create_dir(&artifacts)?; - initialize_heartbeat_store(&store, 1.0)?; - for sequence in 1..=5 { - pulse(&artifacts, sequence, &format!("artifact-{sequence}"))?; - } - - let receipt = - retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:00:00Z")? - .expect("retention should run above hysteresis threshold"); - assert_eq!( - receipt.deleted_directories, - vec!["pulse-000001", "pulse-000002"] - ); - assert_eq!(receipt.deleted_file_count, 2); - assert_eq!(pulse_directories(&artifacts)?.len(), 3); - assert!( - retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:00:01Z",)? - .is_none() - ); - Ok(()) - } - - #[test] - fn pending_plan_refuses_changed_artifact_and_preserves_it() -> Result<()> { - let temp = tempfile::tempdir()?; - let store = temp.path().join("heartbeat.cc"); - let artifacts = temp.path().join("artifacts"); - fs::create_dir(&artifacts)?; - initialize_heartbeat_store(&store, 1.0)?; - let first = pulse(&artifacts, 1, "before")?; - for sequence in 2..=5 { - pulse(&artifacts, sequence, "stable")?; - } - let members = vec![artifact_member(&artifacts.canonicalize()?, &first)?]; - let root = artifacts.canonicalize()?; - let plan = EpiphanyHeartbeatArtifactRetentionPlan { - schema_version: HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION.to_string(), - plan_id: retention_plan_id(&root, 2, 2, &members), - artifact_root: root.display().to_string(), - retain_pulse_count: 2, - batch_size: 2, - members, - planned_at_utc: "2026-08-08T21:01:00Z".to_string(), - private_state_exposed: false, - }; - write_retention_plan(&store, &plan)?; - fs::write(first.join("nested").join("artifact.json"), "after")?; - - assert!( - retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:01:01Z",) - .is_err() - ); - assert!(first.exists()); - Ok(()) - } - - #[test] - fn unknown_directory_fails_closed_before_deletion() -> Result<()> { - let temp = tempfile::tempdir()?; - let store = temp.path().join("heartbeat.cc"); - let artifacts = temp.path().join("artifacts"); - fs::create_dir(&artifacts)?; - initialize_heartbeat_store(&store, 1.0)?; - for sequence in 1..=5 { - pulse(&artifacts, sequence, "stable")?; - } - fs::create_dir(artifacts.join("operator-evidence"))?; - - assert!( - retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:02:00Z",) - .is_err() - ); - assert!(artifacts.join("pulse-000001").exists()); - Ok(()) - } -} diff --git a/epiphany-core/src/heartbeat_state/heartbeat_roles.rs b/epiphany-core/src/heartbeat_state/heartbeat_roles.rs deleted file mode 100644 index cfc97183..00000000 --- a/epiphany-core/src/heartbeat_state/heartbeat_roles.rs +++ /dev/null @@ -1,119 +0,0 @@ -use super::EpiphanyHeartbeatStateEntry; -use super::HEARTBEAT_ARENA_MAINTENANCE; -use super::HEARTBEAT_STATE_SCHEMA_VERSION; -use super::HeartbeatPacingPolicy; -use super::HeartbeatParticipant; -use super::HeartbeatSelectionPolicy; -use super::PARTICIPANT_KIND_AGENT; -use super::write_heartbeat_state_entry; -use anyhow::Result; -use std::path::Path; - -pub(super) const ROLE_ORDER: &[&str] = &["coordinator"]; - -pub fn default_heartbeat_state(target_heartbeat_rate: f64) -> EpiphanyHeartbeatStateEntry { - EpiphanyHeartbeatStateEntry { - schema_version: HEARTBEAT_STATE_SCHEMA_VERSION.to_string(), - target_heartbeat_rate, - scene_clock: 0.0, - selection_policy: HeartbeatSelectionPolicy { - mode: "readiness_queue".to_string(), - reaction_precedence: true, - minimum_speed: 0.2, - tie_breakers: vec![ - "reaction_readiness_desc".to_string(), - "next_ready_at_asc".to_string(), - "initiative_speed_desc".to_string(), - "stable_actor_id_asc".to_string(), - ], - }, - pacing_policy: HeartbeatPacingPolicy { - cooldown_starts_after_turn_completion: true, - work_base_recovery: 6.0, - idle_base_recovery: 2.0, - sleep_heartbeat_rate_multiplier: 0.05, - minimum_effective_rate: 0.001, - }, - participants: ROLE_ORDER - .iter() - .map(|role_id| default_participant(role_id)) - .collect(), - history: Vec::new(), - } -} - -pub fn initialize_heartbeat_store( - store_path: impl AsRef, - target_heartbeat_rate: f64, -) -> Result { - write_heartbeat_state_entry(store_path, &default_heartbeat_state(target_heartbeat_rate)) -} - -pub(super) fn default_participant(role_id: &str) -> HeartbeatParticipant { - HeartbeatParticipant { - agent_id: agent_id_for_role(role_id).to_string(), - role_id: role_id.to_string(), - display_name: display_name_for_role(role_id).to_string(), - arena: HEARTBEAT_ARENA_MAINTENANCE.to_string(), - participant_kind: PARTICIPANT_KIND_AGENT.to_string(), - initiative_speed: initiative_speed_for_role(role_id), - next_ready_at: 0.0, - reaction_bias: reaction_bias_for_role(role_id), - interrupt_threshold: interrupt_threshold_for_role(role_id), - current_load: 0.0, - status: "active".to_string(), - constraints: participant_constraints(role_id) - .into_iter() - .map(str::to_string) - .collect(), - last_action_id: None, - last_woke_at: None, - last_finished_at: None, - pending_turn: None, - } -} - -pub(super) fn agent_id_for_role(role_id: &str) -> &'static str { - match role_id { - "coordinator" => "epiphany.self", - _ => "epiphany.unknown", - } -} - -pub(super) fn display_name_for_role(role_id: &str) -> &'static str { - match role_id { - "coordinator" => "Self", - _ => "Unknown", - } -} - -fn initiative_speed_for_role(role_id: &str) -> f64 { - match role_id { - "coordinator" => 1.28, - _ => 1.0, - } -} - -fn reaction_bias_for_role(role_id: &str) -> f64 { - match role_id { - "coordinator" => 0.88, - _ => 0.5, - } -} - -fn interrupt_threshold_for_role(role_id: &str) -> f64 { - match role_id { - "coordinator" => 0.42, - _ => 0.5, - } -} - -fn participant_constraints(role_id: &str) -> Vec<&'static str> { - let role_specific = match role_id { - "coordinator" => { - "Routes and reviews; must not implement, verify, or accept its own comfort." - } - _ => "Unknown role.", - }; - vec![role_specific] -} diff --git a/epiphany-core/src/heartbeat_state/heartbeat_store.rs b/epiphany-core/src/heartbeat_state/heartbeat_store.rs deleted file mode 100644 index f25aaa51..00000000 --- a/epiphany-core/src/heartbeat_state/heartbeat_store.rs +++ /dev/null @@ -1,330 +0,0 @@ -use super::EpiphanyHeartbeatArtifactRetentionPlan; -use super::EpiphanyHeartbeatArtifactRetentionReceipt; -use super::EpiphanyHeartbeatStaleTurnRepairReceipt; -use super::EpiphanyHeartbeatStateEntry; -use super::HEARTBEAT_ARENA_MAINTENANCE; -use super::HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY; -use super::HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION; -use super::HEARTBEAT_STATE_KEY; -use super::HEARTBEAT_STATE_SCHEMA_VERSION; -use super::PARTICIPANT_KIND_AGENT; -use super::participant_arena; -use super::participant_kind; -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultcache_rs::{CacheBackingStore, CultCache, CultCacheEnvelope, DatabaseEntry}; -use std::collections::HashSet; -use std::path::Path; - -pub fn heartbeat_state_cache(store_path: impl AsRef) -> Result { - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref())?; - Ok(cache) -} - -fn import_owned_heartbeat_envelopes(cache: &mut CultCache, store_path: &Path) -> Result<()> { - let state_type = ::TYPE; - let mut identities = HashSet::new(); - for envelope in SingleFileMessagePackBackingStore::new(store_path).pull_all()? { - let owned = (envelope.r#type == state_type && envelope.key == HEARTBEAT_STATE_KEY) - || envelope.r#type == ::TYPE; - let owned = owned - || envelope.r#type == ::TYPE - || envelope.r#type - == ::TYPE; - if !owned { - continue; - } - if !identities.insert((envelope.r#type.clone(), envelope.key.clone())) { - return Err(anyhow!( - "heartbeat store contains duplicate owner entry type {:?} key {:?}", - envelope.r#type, - envelope.key - )); - } - if envelope.r#type == state_type { - cache.load_envelope::(envelope)?; - } else if envelope.r#type == ::TYPE - { - cache.load_envelope::(envelope)?; - } else if envelope.r#type - == ::TYPE - { - cache.load_envelope::(envelope)?; - } else { - cache.load_envelope::(envelope)?; - } - } - Ok(()) -} - -fn commit_owned_entry( - store_path: &Path, - cache: &CultCache, - key: &str, - value: &T, -) -> Result { - let expected = cache - .snapshot_envelopes() - .into_iter() - .find(|entry| entry.r#type == T::TYPE && entry.key == key); - let (replacement, written) = cache.prepare_entry(key, value)?; - let backing = SingleFileMessagePackBackingStore::new(store_path); - let committed = match expected { - Some(expected) => backing.compare_and_swap_entry(&expected, replacement)?, - None => backing.insert_entry_if_absent(replacement)?, - }; - if !committed { - return Err(anyhow!( - "heartbeat owner entry lost exact atomic compare-and-swap" - )); - } - Ok(written) -} - -pub fn load_heartbeat_state_entry( - store_path: impl AsRef, -) -> Result> { - let cache = heartbeat_state_cache(store_path)?; - cache.get::(HEARTBEAT_STATE_KEY) -} - -pub fn write_heartbeat_state_entry( - store_path: impl AsRef, - state: &EpiphanyHeartbeatStateEntry, -) -> Result { - validate_heartbeat_state(state)?; - let store_path = store_path.as_ref(); - let cache = heartbeat_state_cache(store_path)?; - commit_owned_entry(store_path, &cache, HEARTBEAT_STATE_KEY, state) -} - -pub fn load_heartbeat_state_transaction( - store_path: impl AsRef, -) -> Result<( - Option, - Option, -)> { - let cache = heartbeat_state_cache(store_path)?; - let envelope = cache.snapshot_envelopes().into_iter().find(|entry| { - entry.r#type == ::TYPE - && entry.key == HEARTBEAT_STATE_KEY - }); - Ok(( - cache.get::(HEARTBEAT_STATE_KEY)?, - envelope, - )) -} - -pub fn commit_heartbeat_state_transaction( - store_path: impl AsRef, - expected: Option, - state: &EpiphanyHeartbeatStateEntry, -) -> Result<()> { - validate_heartbeat_state(state)?; - let cache = heartbeat_state_cache(store_path.as_ref())?; - let (replacement, _) = cache.prepare_entry(HEARTBEAT_STATE_KEY, state)?; - let backing = SingleFileMessagePackBackingStore::new(store_path.as_ref()); - let committed = match expected { - Some(expected) => backing.compare_and_swap_entry(&expected, replacement)?, - None => backing.insert_entry_if_absent(replacement)?, - }; - if !committed { - return Err(anyhow!( - "heartbeat state lost exact atomic compare-and-swap" - )); - } - Ok(()) -} - -pub fn write_heartbeat_stale_turn_repair_receipt( - store_path: impl AsRef, - receipt: &EpiphanyHeartbeatStaleTurnRepairReceipt, -) -> Result { - validate_heartbeat_stale_turn_repair_receipt(receipt)?; - let store_path = store_path.as_ref(); - let cache = heartbeat_state_cache(store_path)?; - let written = receipt.clone(); - let (receipt_entry, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; - let (latest_entry, _) = cache.prepare_entry(HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY, receipt)?; - let expected = cache - .snapshot_envelopes() - .into_iter() - .filter(|entry| { - entry.r#type == ::TYPE - && (entry.key == receipt.receipt_id - || entry.key == HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY) - }) - .collect::>(); - if !SingleFileMessagePackBackingStore::new(store_path) - .compare_and_swap_batch(&expected, vec![receipt_entry, latest_entry])? - { - return Err(anyhow!( - "heartbeat stale-turn receipt lost exact atomic compare-and-swap" - )); - } - Ok(written) -} - -pub fn load_latest_heartbeat_stale_turn_repair_receipt( - store_path: impl AsRef, -) -> Result> { - let cache = heartbeat_state_cache(store_path)?; - cache.get::(HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY) -} - -pub fn validate_heartbeat_stale_turn_repair_receipt( - receipt: &EpiphanyHeartbeatStaleTurnRepairReceipt, -) -> Result<()> { - if receipt.schema_version != HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION { - return Err(anyhow!( - "heartbeat stale-turn repair schema_version is {:?}, expected {:?}", - receipt.schema_version, - HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION - )); - } - if receipt.private_state_exposed { - return Err(anyhow!( - "heartbeat stale-turn repair receipt must not expose private state" - )); - } - if receipt.receipt_id.trim().is_empty() - || receipt.role_id.trim().is_empty() - || receipt.agent_id.trim().is_empty() - || receipt.action_id.trim().is_empty() - { - return Err(anyhow!( - "heartbeat stale-turn repair receipt requires receipt, role, agent, and action ids" - )); - } - if receipt.stale_age_seconds < 0 { - return Err(anyhow!( - "heartbeat stale-turn repair receipt stale_age_seconds must be non-negative" - )); - } - if receipt.reason.trim().is_empty() { - return Err(anyhow!( - "heartbeat stale-turn repair receipt requires a reason" - )); - } - if receipt.resulting_status != "repaired" { - return Err(anyhow!( - "heartbeat stale-turn repair receipt resulting_status must be repaired" - )); - } - Ok(()) -} - -pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<()> { - if state.schema_version != HEARTBEAT_STATE_SCHEMA_VERSION { - return Err(anyhow!( - "heartbeat state schema_version is {:?}, expected {:?}", - state.schema_version, - HEARTBEAT_STATE_SCHEMA_VERSION - )); - } - if state.participants.is_empty() { - return Err(anyhow!("heartbeat state has no participants")); - } - if state.target_heartbeat_rate < 0.0 { - return Err(anyhow!( - "heartbeat target_heartbeat_rate must be non-negative" - )); - } - for participant in &state.participants { - if participant.agent_id.trim().is_empty() { - return Err(anyhow!("heartbeat participant has empty agent_id")); - } - if participant.role_id.trim().is_empty() { - return Err(anyhow!( - "heartbeat participant {} has empty role_id", - participant.agent_id - )); - } - if participant.initiative_speed <= 0.0 { - return Err(anyhow!( - "heartbeat participant {} initiative_speed must be positive", - participant.agent_id - )); - } - let arena = participant_arena(participant); - if arena != HEARTBEAT_ARENA_MAINTENANCE { - return Err(anyhow!( - "heartbeat participant {} arena {:?} is unsupported", - participant.agent_id, - arena - )); - } - let participant_kind = participant_kind(participant); - if participant_kind != PARTICIPANT_KIND_AGENT { - return Err(anyhow!( - "heartbeat participant {} participant_kind {:?} is unsupported", - participant.agent_id, - participant_kind - )); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::heartbeat_state::default_heartbeat_state; - use pretty_assertions::assert_eq; - - #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] - #[cultcache(type = "test.foreign.readiness", schema = "ForeignReadiness")] - struct ForeignReadiness { - #[cultcache(key = 0)] - status: String, - } - - #[test] - fn round_trips_scheduler_state() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("heartbeats.msgpack"); - let state = default_heartbeat_state(1.0); - - write_heartbeat_state_entry(&store_path, &state)?; - let loaded = load_heartbeat_state_entry(&store_path)? - .expect("heartbeat state should round-trip through CultCache"); - - assert_eq!(loaded.schema_version, HEARTBEAT_STATE_SCHEMA_VERSION); - assert_eq!(loaded.participants.len(), state.participants.len()); - - Ok(()) - } - - #[test] - fn heartbeat_owner_reads_and_writes_preserve_foreign_shared_store_rows() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("heartbeats.msgpack"); - let mut foreign_cache = CultCache::new(); - foreign_cache.register_entry_type::()?; - let (foreign, _) = foreign_cache.prepare_entry( - "provider-readiness", - &ForeignReadiness { - status: "ready".into(), - }, - )?; - let mut backing = SingleFileMessagePackBackingStore::new(&store_path); - backing.push(&foreign)?; - - assert!(load_heartbeat_state_entry(&store_path)?.is_none()); - write_heartbeat_state_entry(&store_path, &default_heartbeat_state(1.0))?; - - let rows = backing.pull_all()?; - assert!(rows.contains(&foreign)); - assert!(rows.iter().any(|row| { - row.r#type == ::TYPE - && row.key == HEARTBEAT_STATE_KEY - })); - Ok(()) - } -} diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index ad5fe1b0..50ca5965 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -10,7 +10,6 @@ mod distillation; mod eyes_gateway; mod frontier_plan_chain; mod hands_gateway; -mod heartbeat_state; mod host_identity; mod idunn_provider_health; mod idunn_runtime_health; @@ -488,43 +487,6 @@ pub use eyes_gateway::default_eyes_cultnet_contracts; pub use eyes_gateway::eyes_evidence_packet_from_research_finding; pub use frontier_plan_chain::validate_frontier_plan_decision_chain; pub use hands_gateway::*; -pub use heartbeat_state::EpiphanyHeartbeatArtifactRetentionPlan; -pub use heartbeat_state::EpiphanyHeartbeatArtifactRetentionReceipt; -pub use heartbeat_state::EpiphanyHeartbeatStaleTurnRepairReceipt; -pub use heartbeat_state::EpiphanyHeartbeatStateEntry; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_PLAN_LATEST_KEY; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_PLAN_SCHEMA_VERSION; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_PLAN_TYPE; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_LATEST_KEY; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_SCHEMA_VERSION; -pub use heartbeat_state::HEARTBEAT_ARTIFACT_RETENTION_RECEIPT_TYPE; -pub use heartbeat_state::HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY; -pub use heartbeat_state::HEARTBEAT_STALE_TURN_REPAIR_SCHEMA_VERSION; -pub use heartbeat_state::HEARTBEAT_STALE_TURN_REPAIR_TYPE; -pub use heartbeat_state::HEARTBEAT_STATE_KEY; -pub use heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION; -pub use heartbeat_state::HEARTBEAT_STATE_TYPE; -pub use heartbeat_state::HeartbeatArtifactRetentionMember; -pub use heartbeat_state::HeartbeatHistoryEvent; -pub use heartbeat_state::HeartbeatPacingPolicy; -pub use heartbeat_state::HeartbeatParticipant; -pub use heartbeat_state::HeartbeatPendingTurn; -pub use heartbeat_state::HeartbeatSelectionPolicy; -pub use heartbeat_state::HeartbeatStaleTurnRepairOptions; -pub use heartbeat_state::ResidentSelfHeartbeatPulse; -pub use heartbeat_state::default_heartbeat_state; -pub use heartbeat_state::heartbeat_state_cache; -pub use heartbeat_state::heartbeat_status_projection; -pub use heartbeat_state::initialize_heartbeat_store; -pub use heartbeat_state::load_heartbeat_state_entry; -pub use heartbeat_state::load_latest_heartbeat_stale_turn_repair_receipt; -pub use heartbeat_state::pulse_resident_self_heartbeat; -pub use heartbeat_state::reconcile_resident_self_heartbeat_ack; -pub use heartbeat_state::recover_stale_heartbeat_store; -pub use heartbeat_state::retain_heartbeat_pulse_artifacts; -pub use heartbeat_state::validate_heartbeat_state; -pub use heartbeat_state::write_heartbeat_stale_turn_repair_receipt; -pub use heartbeat_state::write_heartbeat_state_entry; pub use host_identity::{ HOST_IDENTITY_KEY, HOST_IDENTITY_SCHEMA_VERSION, HOST_IDENTITY_TRUST_ANCHOR_KEY, HOST_IDENTITY_TRUST_ANCHOR_TYPE, HOST_IDENTITY_TYPE, HostIdentitySignature, HostIdentitySigner, diff --git a/epiphany-core/src/reorientation_work.rs b/epiphany-core/src/reorientation_work.rs index 056be9c4..58632e1a 100644 --- a/epiphany-core/src/reorientation_work.rs +++ b/epiphany-core/src/reorientation_work.rs @@ -857,26 +857,6 @@ mod tests { use super::*; use epiphany_model_adapter::EpiphanyModelRequest; - #[test] - fn reorientation_has_no_aggregate_or_freshness_authority_path() { - let coordinator = include_str!("bin/epiphany-mvp-coordinator.rs"); - let status = include_str!("bin/epiphany-mvp-status.rs"); - let agent_launch = include_str!("agent_launch.rs"); - let worker_launch = include_str!("surfaces/worker_launch.rs"); - for (source, forbidden) in [ - (coordinator, "build_epiphany_reorient_launch_request"), - (status, "recommend_reorientation"), - (status, "crrc_regather_current_after_relinquishment"), - (agent_launch, "build_epiphany_reorient_launch_request"), - (worker_launch, "EpiphanyReorientLaunchRequestInput"), - ] { - assert!( - !source.contains(forbidden), - "obsolete reorientation authority {forbidden:?} survived" - ); - } - } - #[test] fn reorientation_is_decision_auditable_thread_free_and_keyed() -> Result<()> { let temp = tempfile::tempdir()?; diff --git a/epiphany-core/src/resident_readiness.rs b/epiphany-core/src/resident_readiness.rs index b3f23398..9dfd0232 100644 --- a/epiphany-core/src/resident_readiness.rs +++ b/epiphany-core/src/resident_readiness.rs @@ -1,8 +1,8 @@ use crate::{ ProcessInstanceIdentity, ProcessInstanceObservation, ResidentSelfPolicy, authenticate_epiphany_packaged_release, epiphany_packaged_release_binary_path, - load_epiphany_cultmesh_swarm_brake, load_heartbeat_state_entry, load_resident_self_state, - observe_process_instance, validate_resident_self_store_separation, + load_epiphany_cultmesh_swarm_brake, load_resident_self_state, observe_process_instance, + validate_resident_self_store_separation, }; use anyhow::{Result, anyhow, bail}; use cultcache_rs::{ @@ -45,12 +45,12 @@ pub struct ResidentProcessSingletonGuard { } #[cfg(unix)] -fn resident_process_singleton_lock_path(role: &str, store: &Path) -> Result { +fn resident_process_singleton_lock_path(store: &Path) -> Result { let file_name = store .file_name() .ok_or_else(|| anyhow!("resident singleton store has no file name"))? .to_string_lossy(); - Ok(store.with_file_name(format!("{file_name}.{role}.process.lock"))) + Ok(store.with_file_name(format!("{file_name}.resident-self.process.lock"))) } impl Drop for ResidentProcessSingletonGuard { @@ -68,34 +68,30 @@ impl Drop for ResidentProcessSingletonGuard { } pub fn acquire_resident_process_singleton( - role: &str, store: impl AsRef, ) -> Result { - if !matches!(role, "heartbeat" | "resident-self") { - bail!("unsupported resident singleton role: {role}"); - } let store = canonical_or_absolute(store.as_ref()); #[cfg(windows)] { let identity = format!( "{:x}", - Sha256::digest(format!("{role}|{}", store.to_string_lossy().to_lowercase()).as_bytes()) + Sha256::digest(store.to_string_lossy().to_lowercase().as_bytes()) ); - let name = OsStr::new(&format!("Global\\EpiphanyResident-{role}-{identity}")) + let name = OsStr::new(&format!("Global\\EpiphanyResidentSelf-{identity}")) .encode_wide() .chain(std::iter::once(0)) .collect::>(); let handle = unsafe { CreateMutexW(std::ptr::null_mut(), 1, name.as_ptr()) }; if handle.is_null() { bail!( - "resident {role} singleton mutex creation failed: {}", + "resident Self singleton mutex creation failed: {}", unsafe { GetLastError() } ); } if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { unsafe { CloseHandle(handle) }; bail!( - "resident {role} owner already exists for canonical store {}", + "resident Self owner already exists for canonical store {}", store.display() ); } @@ -103,7 +99,7 @@ pub fn acquire_resident_process_singleton( } #[cfg(unix)] { - let path = resident_process_singleton_lock_path(role, &store)?; + let path = resident_process_singleton_lock_path(&store)?; let file = File::options() .create(true) .read(true) @@ -111,7 +107,7 @@ pub fn acquire_resident_process_singleton( .open(path)?; if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 { bail!( - "resident {role} owner already exists for canonical store {}", + "resident Self owner already exists for canonical store {}", store.display() ); } @@ -123,24 +119,21 @@ pub fn acquire_resident_process_singleton( )) } -fn resident_process_singleton_is_owned(role: &str, store: &Path) -> Result { - if !matches!(role, "heartbeat" | "resident-self") { - bail!("unsupported resident singleton role: {role}"); - } +fn resident_process_singleton_is_owned(store: &Path) -> Result { let store = canonical_or_absolute(store); #[cfg(windows)] { let identity = format!( "{:x}", - Sha256::digest(format!("{role}|{}", store.to_string_lossy().to_lowercase()).as_bytes()) + Sha256::digest(store.to_string_lossy().to_lowercase().as_bytes()) ); - let name = OsStr::new(&format!("Global\\EpiphanyResident-{role}-{identity}")) + let name = OsStr::new(&format!("Global\\EpiphanyResidentSelf-{identity}")) .encode_wide() .chain(std::iter::once(0)) .collect::>(); let handle = unsafe { CreateMutexW(std::ptr::null_mut(), 0, name.as_ptr()) }; if handle.is_null() { - bail!("resident {role} singleton mutex probe failed: {}", unsafe { + bail!("resident Self singleton mutex probe failed: {}", unsafe { GetLastError() }); } @@ -155,7 +148,7 @@ fn resident_process_singleton_is_owned(role: &str, store: &Path) -> Result } other => { unsafe { CloseHandle(handle) }; - bail!("resident {role} singleton mutex probe returned {other}"); + bail!("resident Self singleton mutex probe returned {other}"); } }; unsafe { CloseHandle(handle) }; @@ -163,7 +156,7 @@ fn resident_process_singleton_is_owned(role: &str, store: &Path) -> Result } #[cfg(unix)] { - let path = resident_process_singleton_lock_path(role, &store)?; + let path = resident_process_singleton_lock_path(&store)?; if !path.exists() { return Ok(false); } @@ -225,7 +218,7 @@ impl ResidentProviderReadiness { if self.schema_version != RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION { bail!("resident provider readiness schema is unsupported"); } - if !matches!(self.provider.as_str(), "heartbeat" | "resident-self") { + if self.provider != "resident-self" { bail!("resident provider readiness owner is invalid"); } if self.runtime_id.trim().is_empty() @@ -306,22 +299,6 @@ pub fn load_resident_provider_readiness(store: &Path) -> Result &'static str { - if distinct_physical_paths(heartbeat_store, resident_store) - && load_heartbeat_state_entry(heartbeat_store) - .ok() - .flatten() - .is_some() - { - "ready" - } else { - "warming" - } -} - pub fn resident_self_local_provider_status( resident_store: &Path, policy: &ResidentSelfPolicy, @@ -354,7 +331,6 @@ pub fn resident_self_local_provider_status( #[derive(Clone, Debug)] pub struct ResidentReadinessRequest<'a> { pub release_store: &'a Path, - pub heartbeat_store: &'a Path, pub resident_store: &'a Path, pub policy: &'a ResidentSelfPolicy, pub release_runtime_id: &'a str, @@ -375,8 +351,6 @@ pub struct ResidentCognitionReadinessProjection { pub source_commit: Option, pub release_authenticated: bool, pub physical_stores_separate: bool, - pub heartbeat_initialized: bool, - pub heartbeat_provider_fresh: bool, pub resident_provider_fresh: bool, pub resident_state_coherent: bool, pub active_lease_observation: String, @@ -402,10 +376,6 @@ pub fn derive_resident_cognition_readiness( .as_ref() .ok() .map(|value| value.source_commit_sha.as_str()); - let heartbeat_executable = release - .as_ref() - .ok() - .and_then(|value| epiphany_packaged_release_binary_path(value, "heartbeat").ok()); let resident_executable = release .as_ref() .ok() @@ -424,26 +394,10 @@ pub fn derive_resident_cognition_readiness( } }; let physical_stores_separate = - validate_resident_self_store_separation(request.resident_store, request.policy).is_ok() - && distinct_physical_paths(request.heartbeat_store, request.resident_store); + validate_resident_self_store_separation(request.resident_store, request.policy).is_ok(); if !physical_stores_separate { reasons.push("resident cognition stores are not physically separated".into()); } - let heartbeat_initialized = load_heartbeat_state_entry(request.heartbeat_store) - .ok() - .flatten() - .is_some(); - if !heartbeat_initialized { - reasons.push("heartbeat state is absent or unreadable".into()); - } - let heartbeat_provider_fresh = provider_is_fresh( - request.heartbeat_store, - "heartbeat", - &request, - expected_source_commit, - heartbeat_executable.as_deref(), - &mut reasons, - ); let resident_provider_fresh = provider_is_fresh( request.resident_store, "resident-self", @@ -491,15 +445,13 @@ pub fn derive_resident_cognition_readiness( } let ready = release_authenticated && physical_stores_separate - && heartbeat_initialized - && heartbeat_provider_fresh && resident_provider_fresh && resident_state_coherent && cognitive_runtime_id.is_some() && workspace_ready && credential_ready; ResidentCognitionReadinessProjection { - schema_version: "epiphany.resident_cognition.readiness.v0".into(), + schema_version: "epiphany.resident_cognition.readiness.v1".into(), status: if ready { "active" } else if release_authenticated { @@ -521,8 +473,6 @@ pub fn derive_resident_cognition_readiness( .map(|value| value.source_commit_sha.clone()), release_authenticated, physical_stores_separate, - heartbeat_initialized, - heartbeat_provider_fresh, resident_provider_fresh, resident_state_coherent, active_lease_observation, @@ -571,34 +521,24 @@ fn classify_active_lease_observation( } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResidentProviderPairHealth { +pub struct ResidentProviderHealth { pub terminal_current: usize, pub warming: usize, pub contradictions: Vec, } -pub fn authenticate_resident_provider_pair( +pub fn authenticate_resident_provider( release: &crate::EpiphanyPackagedReleaseEntry, witness: &str, - heartbeat_store: &Path, resident_store: &Path, freshness_millis: u64, -) -> ResidentProviderPairHealth { - let mut result = ResidentProviderPairHealth { +) -> ResidentProviderHealth { + let mut result = ResidentProviderHealth { terminal_current: 0, warming: 0, contradictions: Vec::new(), }; - if !distinct_physical_paths(heartbeat_store, resident_store) { - result - .contradictions - .push("heartbeat and resident Self provider stores alias physically".into()); - return result; - } - for (owner, store, role) in [ - ("heartbeat", heartbeat_store, "heartbeat"), - ("resident-self", resident_store, "swarm"), - ] { + for (owner, store, role) in [("resident-self", resident_store, "swarm")] { let executable = epiphany_packaged_release_binary_path(release, role).ok(); let value = match load_resident_provider_readiness(store) { Ok(Some(value)) => value, @@ -613,7 +553,7 @@ pub fn authenticate_resident_provider_pair( continue; } }; - let role_owner_present = match resident_process_singleton_is_owned(owner, store) { + let role_owner_present = match resident_process_singleton_is_owned(store) { Ok(value) => value, Err(error) => { result.contradictions.push(format!( @@ -665,7 +605,7 @@ fn provider_is_fresh( return false; } }; - let role_owner_present = match resident_process_singleton_is_owned(owner, store) { + let role_owner_present = match resident_process_singleton_is_owned(store) { Ok(value) => value, Err(error) => { reasons.push(format!( @@ -772,11 +712,6 @@ fn provider_matches_authority( .is_empty() } -fn distinct_physical_paths(left: &Path, right: &Path) -> bool { - canonical_or_absolute(left) != canonical_or_absolute(right) - && matches!(crate::same_existing_file(left, right), Ok(false)) -} - fn canonical_or_absolute(path: &Path) -> PathBuf { fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } @@ -838,17 +773,15 @@ mod tests { use super::*; #[test] - fn resident_process_singleton_excludes_only_the_same_role_and_store() -> Result<()> { + fn resident_process_singleton_excludes_only_the_same_store() -> Result<()> { let temp = tempfile::tempdir()?; let store = temp.path().join("resident.cc"); fs::write(&store, [])?; - let owner = acquire_resident_process_singleton("resident-self", &store)?; - assert!(acquire_resident_process_singleton("resident-self", &store).is_err()); - let other_role = acquire_resident_process_singleton("heartbeat", &store)?; - let other_store = - acquire_resident_process_singleton("resident-self", temp.path().join("other.cc"))?; - drop((owner, other_role, other_store)); - assert!(acquire_resident_process_singleton("resident-self", &store).is_ok()); + let owner = acquire_resident_process_singleton(&store)?; + assert!(acquire_resident_process_singleton(&store).is_err()); + let other_store = acquire_resident_process_singleton(temp.path().join("other.cc"))?; + drop((owner, other_store)); + assert!(acquire_resident_process_singleton(&store).is_ok()); Ok(()) } @@ -857,13 +790,9 @@ mod tests { fn unix_resident_singleton_lock_is_shared_beside_the_mounted_store() -> Result<()> { let store = Path::new("/state/resident.cc"); assert_eq!( - resident_process_singleton_lock_path("resident-self", store)?, + resident_process_singleton_lock_path(store)?, PathBuf::from("/state/resident.cc.resident-self.process.lock") ); - assert_eq!( - resident_process_singleton_lock_path("heartbeat", store)?, - PathBuf::from("/state/resident.cc.heartbeat.process.lock") - ); Ok(()) } @@ -894,7 +823,7 @@ mod tests { fn provider() -> ResidentProviderReadiness { ResidentProviderReadiness { schema_version: RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION.into(), - provider: "heartbeat".into(), + provider: "resident-self".into(), runtime_id: "ygg".into(), release_id: "release-a".into(), release_witness_sha256: format!("sha256-{}", "a".repeat(64)), @@ -915,7 +844,7 @@ mod tests { let executable = std::env::current_exe().unwrap(); assert!(provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -927,7 +856,7 @@ mod tests { )); assert!(!provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-b", &value.release_witness_sha256, @@ -939,7 +868,7 @@ mod tests { )); assert!(!provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -951,7 +880,7 @@ mod tests { )); assert!(!provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -966,22 +895,22 @@ mod tests { #[test] fn role_singleton_probe_observes_only_a_live_owner() -> Result<()> { let temp = tempfile::tempdir()?; - let store = temp.path().join("heartbeat.cc"); - assert!(!resident_process_singleton_is_owned("heartbeat", &store)?); + let store = temp.path().join("resident.cc"); + assert!(!resident_process_singleton_is_owned(&store)?); let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(0); let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0); let owned_store = store.clone(); let owner = std::thread::spawn(move || -> Result<()> { - let _guard = acquire_resident_process_singleton("heartbeat", &owned_store)?; + let _guard = acquire_resident_process_singleton(&owned_store)?; ready_tx.send(())?; release_rx.recv()?; Ok(()) }); ready_rx.recv()?; - assert!(resident_process_singleton_is_owned("heartbeat", &store)?); + assert!(resident_process_singleton_is_owned(&store)?); release_tx.send(())?; owner.join().expect("singleton owner thread")?; - assert!(!resident_process_singleton_is_owned("heartbeat", &store)?); + assert!(!resident_process_singleton_is_owned(&store)?); Ok(()) } @@ -1041,17 +970,6 @@ mod tests { Ok(()) } - #[test] - fn hardlink_store_aliases_are_not_physically_distinct() -> Result<()> { - let temp = tempfile::tempdir()?; - let first = temp.path().join("first.cc"); - let alias = temp.path().join("alias.cc"); - fs::write(&first, b"cultcache")?; - fs::hard_link(&first, &alias)?; - assert!(!distinct_physical_paths(&first, &alias)); - Ok(()) - } - #[cfg(unix)] #[test] fn unix_credentials_reject_group_or_other_access() -> Result<()> { @@ -1083,35 +1001,6 @@ mod tests { Ok(()) } - #[test] - fn projection_schema_has_no_systemd_or_secret_substitution_surface() { - let projection = ResidentCognitionReadinessProjection { - schema_version: "epiphany.resident_cognition.readiness.v0".into(), - status: "warming".into(), - runtime_id: Some("epiphany-yggdrasil".into()), - release_runtime_id: Some("epiphany-release-yggdrasil".into()), - release_id: Some("release-1".into()), - release_witness_sha256: Some("sha256:witness".into()), - source_commit: Some("commit-1".into()), - release_authenticated: true, - physical_stores_separate: true, - heartbeat_initialized: true, - heartbeat_provider_fresh: false, - resident_provider_fresh: false, - resident_state_coherent: true, - active_lease_observation: "none".into(), - brake_engaged: true, - workspace_ready: true, - credential_ready: true, - reasons: vec!["provider absent".into()], - private_state_exposed: false, - }; - let json = serde_json::to_string(&projection).unwrap(); - assert!(!json.contains("systemd")); - assert!(!json.contains("credentialPath")); - assert!(!json.contains("secret")); - } - #[test] fn readiness_uses_mounted_cognitive_identity_for_projection_and_brake() -> Result<()> { let temp = tempfile::tempdir()?; @@ -1159,11 +1048,9 @@ mod tests { release_id: "sha256:release".into(), release_witness_sha256: "sha256:witness".into(), }; - let heartbeat_store = shared.join("heartbeat.cc"); let resident_store = shared.join("resident.cc"); let projection = derive_resident_cognition_readiness(ResidentReadinessRequest { release_store: &policy.release_store, - heartbeat_store: &heartbeat_store, resident_store: &resident_store, policy: &policy, release_runtime_id: &policy.release_runtime_id, @@ -1209,7 +1096,7 @@ mod tests { assert_eq!( provider_authority_failures( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -1236,7 +1123,7 @@ mod tests { let executable = PathBuf::from(&value.process_executable_path); assert!(provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -1249,7 +1136,7 @@ mod tests { value.observed_at_millis = 2_001; assert!(!provider_matches_authority( &value, - "heartbeat", + "resident-self", "ygg", "release-a", &value.release_witness_sha256, @@ -1262,9 +1149,8 @@ mod tests { } #[test] - fn aggregate_pair_warms_on_missing_and_rejects_wrong_release() -> Result<()> { + fn aggregate_health_warms_on_missing_and_rejects_wrong_release() -> Result<()> { let temp = tempfile::tempdir()?; - let heartbeat = temp.path().join("heartbeat.cc"); let resident = temp.path().join("resident.cc"); let executable = std::env::current_exe()?; let binary = |role: &str| crate::EpiphanyPackagedReleaseBinary { @@ -1284,13 +1170,12 @@ mod tests { toolchain_fingerprint: "toolchain".into(), created_at_utc: "now".into(), package_root: temp.path().display().to_string(), - binaries: vec![binary("heartbeat"), binary("swarm")], + binaries: vec![binary("swarm")], private_state_exposed: false, }; let witness = format!("sha256-{}", "a".repeat(64)); - let missing = - authenticate_resident_provider_pair(&release, &witness, &heartbeat, &resident, 10); - assert_eq!(missing.warming, 2); + let missing = authenticate_resident_provider(&release, &witness, &resident, 10); + assert_eq!(missing.warming, 1); let process = crate::capture_process_instance(std::process::id())?; let mut wrong = provider(); wrong.runtime_id = "ygg".into(); @@ -1301,9 +1186,8 @@ mod tests { wrong.process_id = process.process_id; wrong.process_creation_token = process.creation_token; wrong.process_executable_path = process.executable_path.display().to_string(); - publish_resident_provider_readiness(&heartbeat, wrong)?; - let rejected = - authenticate_resident_provider_pair(&release, &witness, &heartbeat, &resident, 10); + publish_resident_provider_readiness(&resident, wrong)?; + let rejected = authenticate_resident_provider(&release, &witness, &resident, 10); assert!( rejected .contradictions diff --git a/epiphany-core/src/resident_self.rs b/epiphany-core/src/resident_self.rs index 3747f5a7..5070c88d 100644 --- a/epiphany-core/src/resident_self.rs +++ b/epiphany-core/src/resident_self.rs @@ -8,7 +8,7 @@ use std::collections::{BTreeSet, HashSet}; use std::path::{Path, PathBuf}; pub const RESIDENT_SELF_STATE_KEY: &str = "resident-self"; -pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v0"; +pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v1"; pub const RESIDENT_SELF_RUNTIME_RECEIPT_SCHEMA_VERSION: &str = "epiphany.resident_self.runtime_receipt.v0"; pub const RESIDENT_SELF_PRESSURE_SCHEMA_VERSION: &str = "epiphany.resident_self.pressure.v0"; @@ -21,8 +21,9 @@ pub const RESIDENT_SELF_ATLAS_IMPACT_PROVENANCE_PREFIX: &str = "cultcache://atla pub const RESIDENT_SELF_ATLAS_NO_HANDS_AUTHORITY_CLAUSE: &str = "This wake grants no Hands authority."; pub const RESIDENT_SELF_CURRENT_WORK_PROVENANCE_PREFIX: &str = "cultcache://current-work/"; -pub const RESIDENT_SELF_GRANT_SCHEMA_VERSION: &str = "epiphany.resident_self.heartbeat_grant.v1"; -pub const RESIDENT_SELF_ACK_SCHEMA_VERSION: &str = "epiphany.resident_self.terminal_ack.v0"; +pub const RESIDENT_SELF_GRANT_SCHEMA_VERSION: &str = "epiphany.resident_self.grant.v1"; +pub const RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION: &str = + "epiphany.resident_self.terminal_receipt.v1"; pub const RESIDENT_SELF_CHILD_CLAIM_SCHEMA_VERSION: &str = "epiphany.resident_self.child_claim.v0"; pub const RESIDENT_SELF_RETENTION_HEAD_SCHEMA_VERSION: &str = "epiphany.resident_self.retention_head.v0"; @@ -139,11 +140,8 @@ impl ResidentSelfPressure { } #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "epiphany.resident_self.heartbeat_grant.v0", - schema = "ResidentSelfHeartbeatGrant" -)] -pub struct ResidentSelfHeartbeatGrant { +#[cultcache(type = "epiphany.resident_self.grant.v1", schema = "ResidentSelfGrant")] +pub struct ResidentSelfGrant { #[cultcache(key = 0)] pub schema_version: String, #[cultcache(key = 1)] @@ -157,18 +155,14 @@ pub struct ResidentSelfHeartbeatGrant { #[cultcache(key = 5)] pub objective: String, #[cultcache(key = 6)] - pub heartbeat_schedule_id: String, - #[cultcache(key = 7)] - pub heartbeat_action_id: String, - #[cultcache(key = 8)] pub issued_at_millis: u64, - #[cultcache(key = 9, default)] + #[cultcache(key = 7, default)] pub consumed_at_millis: Option, - #[cultcache(key = 10, default)] + #[cultcache(key = 8, default)] pub private_state_exposed: bool, - #[cultcache(key = 11, default)] + #[cultcache(key = 9, default)] pub terminal_at_millis: Option, - #[cultcache(key = 12, default)] + #[cultcache(key = 10, default)] pub terminal_status: Option, } @@ -178,8 +172,6 @@ pub struct ResidentSelfGrantLifecycleProjection { pub grant_id: String, pub pressure_id: String, pub pressure_kind: String, - pub heartbeat_schedule_id: String, - pub heartbeat_action_id: String, pub issued_at_millis: u64, pub consumed_at_millis: Option, pub terminal_at_millis: Option, @@ -189,43 +181,31 @@ pub struct ResidentSelfGrantLifecycleProjection { pub launchable: bool, } -fn resident_self_grant_is_pending( - grant: &ResidentSelfHeartbeatGrant, - legacy_terminal_grant_ids: &BTreeSet, -) -> bool { - grant.consumed_at_millis.is_none() - && grant.terminal_at_millis.is_none() - && !(grant.schema_version == "epiphany.resident_self.heartbeat_grant.v0" - && legacy_terminal_grant_ids.contains(&grant.grant_id)) +fn resident_self_grant_is_pending(grant: &ResidentSelfGrant) -> bool { + grant.consumed_at_millis.is_none() && grant.terminal_at_millis.is_none() } #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache( - type = "epiphany.resident_self.terminal_ack.v0", - schema = "ResidentSelfTerminalAck" + type = "epiphany.resident_self.terminal_receipt.v1", + schema = "ResidentSelfTerminalReceipt" )] -pub struct ResidentSelfTerminalAck { +pub struct ResidentSelfTerminalReceipt { #[cultcache(key = 0)] pub schema_version: String, #[cultcache(key = 1)] - pub ack_id: String, + pub receipt_id: String, #[cultcache(key = 2)] pub grant_id: String, #[cultcache(key = 3)] - pub heartbeat_schedule_id: String, - #[cultcache(key = 4)] - pub heartbeat_action_id: String, - #[cultcache(key = 5)] pub launch_digest: String, - #[cultcache(key = 6)] + #[cultcache(key = 4)] pub coordinator_receipt_id: String, - #[cultcache(key = 7)] + #[cultcache(key = 5)] pub terminal_status: String, - #[cultcache(key = 8)] + #[cultcache(key = 6)] pub completed_at_millis: u64, - #[cultcache(key = 9, default)] - pub consumed_by_heartbeat_at_millis: Option, - #[cultcache(key = 10, default)] + #[cultcache(key = 7, default)] pub private_state_exposed: bool, } @@ -506,7 +486,7 @@ pub struct ResidentSelfTurnLease { pub struct ResidentSelfPreparedLaunch { pub preparation_id: String, pub prepared_at_millis: u64, - pub grant: ResidentSelfHeartbeatGrant, + pub grant: ResidentSelfGrant, pub argv: Vec, pub launch_digest: String, pub policy_digest: String, @@ -728,7 +708,7 @@ fn resident_self_atlas_required_action(pressure_kind: &str) -> Option<&'static s fn resident_coordinator_binding_for_grant( policy: &ResidentSelfPolicy, - grant: &ResidentSelfHeartbeatGrant, + grant: &ResidentSelfGrant, ) -> Result> { let mut cache = crate::runtime_spine_cache(&policy.runtime_store)?; cache.pull_all_backing_stores()?; @@ -807,17 +787,25 @@ fn state_cache(path: &Path) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; let mut identities = HashSet::new(); for envelope in SingleFileMessagePackBackingStore::new(path).pull_all()? { + if matches!( + envelope.r#type.as_str(), + "epiphany.resident_self.heartbeat_grant.v0" | "epiphany.resident_self.terminal_ack.v0" + ) { + return Err(anyhow!( + "resident Self store contains a pre-cut heartbeat lifecycle document" + )); + } let owned = envelope.r#type == ResidentSelfState::TYPE || envelope.r#type == ResidentSelfRuntimeReceipt::TYPE || envelope.r#type == ResidentSelfPressure::TYPE - || envelope.r#type == ResidentSelfHeartbeatGrant::TYPE - || envelope.r#type == ResidentSelfTerminalAck::TYPE + || envelope.r#type == ResidentSelfGrant::TYPE + || envelope.r#type == ResidentSelfTerminalReceipt::TYPE || envelope.r#type == ResidentSelfChildClaim::TYPE || envelope.r#type == ResidentSelfRetentionHead::TYPE; if !owned { @@ -840,11 +828,11 @@ fn state_cache(path: &Path) -> Result { ResidentSelfPressure::TYPE => { cache.load_envelope::(envelope)?; } - ResidentSelfHeartbeatGrant::TYPE => { - cache.load_envelope::(envelope)?; + ResidentSelfGrant::TYPE => { + cache.load_envelope::(envelope)?; } - ResidentSelfTerminalAck::TYPE => { - cache.load_envelope::(envelope)?; + ResidentSelfTerminalReceipt::TYPE => { + cache.load_envelope::(envelope)?; } ResidentSelfChildClaim::TYPE => { cache.load_envelope::(envelope)?; @@ -855,6 +843,26 @@ fn state_cache(path: &Path) -> Result { _ => unreachable!("owned resident Self type was matched above"), }; } + if let Some(state) = cache.get::(RESIDENT_SELF_STATE_KEY)? + && state.schema_version != RESIDENT_SELF_STATE_SCHEMA_VERSION + { + return Err(anyhow!( + "resident Self store has an obsolete writable state epoch" + )); + } + if cache + .get_all::()? + .iter() + .any(|grant| grant.schema_version != RESIDENT_SELF_GRANT_SCHEMA_VERSION) + || cache + .get_all::()? + .iter() + .any(|receipt| receipt.schema_version != RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION) + { + return Err(anyhow!( + "resident Self store has an obsolete writable lifecycle epoch" + )); + } Ok(cache) } @@ -1057,12 +1065,10 @@ pub fn ingest_resident_self_current_work_pressure( ) } -pub fn heartbeat_issue_resident_self_grant( +pub fn issue_resident_self_grant( path: &Path, - schedule_id: &str, - action_id: &str, now_millis: u64, -) -> Result> { +) -> Result> { let mut cache = state_cache(path)?; if cache .get::(RESIDENT_SELF_STATE_KEY)? @@ -1079,16 +1085,8 @@ pub fn heartbeat_issue_resident_self_grant( if state.active_turn.is_some() || state.prepared_launch.is_some() { return Ok(None); } - let legacy_terminal_grant_ids = cache - .get_all::()? - .into_iter() - .map(|ack| ack.grant_id) - .collect::>(); - let grants = cache.get_all::()?; - if grants.iter().any(|grant| { - (grant.heartbeat_schedule_id == schedule_id && grant.heartbeat_action_id == action_id) - || resident_self_grant_is_pending(grant, &legacy_terminal_grant_ids) - }) { + let grants = cache.get_all::()?; + if grants.iter().any(resident_self_grant_is_pending) { return Ok(None); } let mut pending = cache @@ -1110,18 +1108,16 @@ pub fn heartbeat_issue_resident_self_grant( .count() + 1; let grant_id = format!( - "resident-self-grant-{schedule_id}-{action_id}-attempt-{attempt_ordinal}-{}", + "resident-self-grant-attempt-{attempt_ordinal}-{}", pressure.pressure_id ); - let grant = ResidentSelfHeartbeatGrant { + let grant = ResidentSelfGrant { schema_version: RESIDENT_SELF_GRANT_SCHEMA_VERSION.into(), grant_id: grant_id.clone(), pressure_id: pressure.pressure_id.clone(), pressure_kind: pressure.kind.clone(), provenance_ref: pressure.provenance_ref.clone(), objective: pressure.objective.clone(), - heartbeat_schedule_id: schedule_id.into(), - heartbeat_action_id: action_id.into(), issued_at_millis: now_millis, consumed_at_millis: None, private_state_exposed: false, @@ -1155,9 +1151,7 @@ pub fn heartbeat_issue_resident_self_grant( &[expected_state, expected_pressure], vec![state_entry, pressure_entry, grant_entry], )? { - return Err(anyhow!( - "heartbeat lost resident Self pressure-to-grant CAS" - )); + return Err(anyhow!("resident Self lost pressure-to-grant CAS")); } Ok(Some(grant)) } @@ -1184,13 +1178,8 @@ pub fn resident_self_grant_lifecycle_projection( let state = cache .get::(RESIDENT_SELF_STATE_KEY)? .unwrap_or_default(); - let legacy_terminal_grant_ids = cache - .get_all::()? - .into_iter() - .map(|ack| ack.grant_id) - .collect::>(); let mut grants = cache - .get_all::()? + .get_all::()? .into_iter() .filter(|grant| grant_id.is_none_or(|id| grant.grant_id == id)) .collect::>(); @@ -1214,15 +1203,11 @@ pub fn resident_self_grant_lifecycle_projection( .as_ref() .map(|prepared| prepared.grant.grant_id.as_str()) == Some(grant.grant_id.as_str()); - let launchable = resident_self_grant_is_pending(&grant, &legacy_terminal_grant_ids) - && !active - && !prepared; + let launchable = resident_self_grant_is_pending(&grant) && !active && !prepared; ResidentSelfGrantLifecycleProjection { grant_id: grant.grant_id, pressure_id: grant.pressure_id, pressure_kind: grant.pressure_kind, - heartbeat_schedule_id: grant.heartbeat_schedule_id, - heartbeat_action_id: grant.heartbeat_action_id, issued_at_millis: grant.issued_at_millis, consumed_at_millis: grant.consumed_at_millis, terminal_at_millis: grant.terminal_at_millis, @@ -1240,7 +1225,7 @@ pub fn resident_self_grant_lifecycle_projection( pub fn live_resident_self_typed_request_ids(path: &Path) -> Result> { let cache = state_cache(path)?; let mut request_ids = BTreeSet::new(); - for grant in cache.get_all::()? { + for grant in cache.get_all::()? { if let Some(request) = resident_self_typed_request_ref(&grant)? { request_ids.insert(request.request_id().to_string()); } @@ -1254,7 +1239,7 @@ pub fn verify_resident_self_grant_fulfillment( grant_id: &str, ) -> Result { let grant = state_cache(resident_store)? - .get::(grant_id)? + .get::(grant_id)? .ok_or_else(|| anyhow!("resident Self fulfillment check lost its grant"))?; let request = resident_self_typed_request_ref(&grant)?; let Some(request) = request else { @@ -1275,7 +1260,7 @@ fn resident_self_grant_typed_request_is_superseded( grant_id: &str, ) -> Result { let grant = state_cache(resident_store)? - .get::(grant_id)? + .get::(grant_id)? .ok_or_else(|| anyhow!("resident Self supersession check lost its grant"))?; let Some(crate::RuntimeTypedRequestRef::AdmittedModelDirection(request_id)) = resident_self_typed_request_ref(&grant)? @@ -1291,7 +1276,7 @@ fn resident_self_grant_typed_request_is_superseded( } fn resident_self_typed_request_ref<'a>( - grant: &'a ResidentSelfHeartbeatGrant, + grant: &'a ResidentSelfGrant, ) -> Result>> { Ok(match grant.pressure_kind.as_str() { "repo-frontier-proposal-modeling" => { @@ -1346,7 +1331,7 @@ pub fn resident_self_typed_attempt_exists( grant_id: &str, ) -> Result { let grant = state_cache(resident_store)? - .get::(grant_id)? + .get::(grant_id)? .ok_or_else(|| anyhow!("resident Self attempt check lost its grant"))?; let Some(request) = resident_self_typed_request_ref(&grant)? else { return Ok(false); @@ -1361,7 +1346,7 @@ pub fn recover_dead_resident_typed_worker( recovered_at_millis: u64, ) -> Result { let grant = state_cache(resident_store)? - .get::(grant_id)? + .get::(grant_id)? .ok_or_else(|| anyhow!("typed worker death recovery lost its grant"))?; let Some(request) = resident_self_typed_request_ref(&grant)? else { return Ok(false); @@ -1496,7 +1481,7 @@ pub fn resident_self_grant_has_typed_request( grant_id: &str, ) -> Result { let grant = state_cache(resident_store)? - .get::(grant_id)? + .get::(grant_id)? .ok_or_else(|| anyhow!("resident Self typed-request check lost its grant"))?; Ok(resident_self_typed_request_ref(&grant)?.is_some()) } @@ -1799,7 +1784,7 @@ pub fn recover_receipt_free_dead_coordinator_session( )); } let grant = cache - .get::(&lease.grant_id)? + .get::(&lease.grant_id)? .ok_or_else(|| anyhow!("coordinator death recovery lost its grant"))?; if grant.consumed_at_millis.is_none() || grant.terminal_at_millis.is_some() @@ -1882,17 +1867,12 @@ pub fn recover_receipt_free_dead_coordinator_session( Ok(Some(recovery)) } -pub fn pending_resident_self_grant(path: &Path) -> Result> { +pub fn pending_resident_self_grant(path: &Path) -> Result> { let cache = state_cache(path)?; - let legacy_terminal_grant_ids = cache - .get_all::()? - .into_iter() - .map(|ack| ack.grant_id) - .collect::>(); let mut grants = cache - .get_all::()? + .get_all::()? .into_iter() - .filter(|grant| resident_self_grant_is_pending(grant, &legacy_terminal_grant_ids)) + .filter(resident_self_grant_is_pending) .collect::>(); grants.sort_by(|a, b| { a.issued_at_millis @@ -1930,9 +1910,9 @@ pub fn resident_self_policy_digest(policy: &ResidentSelfPolicy) -> String { fn supersede_unlaunched_resident_self_derived_grant( path: &Path, - grant: &ResidentSelfHeartbeatGrant, + grant: &ResidentSelfGrant, now_millis: u64, -) -> Result { +) -> Result { if matches!( grant.pressure_kind.as_str(), "operator-objective" | "persona-feedback" @@ -1953,7 +1933,7 @@ fn supersede_unlaunched_resident_self_derived_grant( )); } let mut current_grant = cache - .get::(&grant.grant_id)? + .get::(&grant.grant_id)? .ok_or_else(|| anyhow!("superseded continuation grant is missing"))?; if current_grant != *grant { return Err(anyhow!("superseded continuation grant authority changed")); @@ -1973,17 +1953,14 @@ fn supersede_unlaunched_resident_self_derived_grant( grant.grant_id.as_str(), grant.provenance_ref.as_str(), ]); - let ack = ResidentSelfTerminalAck { - schema_version: RESIDENT_SELF_ACK_SCHEMA_VERSION.into(), - ack_id: format!("resident-self-superseded-{}", grant.grant_id), + let terminal = ResidentSelfTerminalReceipt { + schema_version: RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: format!("resident-self-superseded-{}", grant.grant_id), grant_id: grant.grant_id.clone(), - heartbeat_schedule_id: grant.heartbeat_schedule_id.clone(), - heartbeat_action_id: grant.heartbeat_action_id.clone(), launch_digest, coordinator_receipt_id: format!("resident-self-superseded-input-{}", grant.grant_id), terminal_status: "superseded".into(), completed_at_millis: now_millis, - consumed_by_heartbeat_at_millis: None, private_state_exposed: false, }; let envelopes = cache.snapshot_envelopes(); @@ -2006,7 +1983,7 @@ fn supersede_unlaunched_resident_self_derived_grant( let expected_grant = envelopes .iter() .find(|entry| { - entry.r#type == ::TYPE + entry.r#type == ::TYPE && entry.key == grant.grant_id }) .cloned() @@ -2018,14 +1995,14 @@ fn supersede_unlaunched_resident_self_derived_grant( let (state_entry, _) = cache.prepare_entry(RESIDENT_SELF_STATE_KEY, &state)?; let (pressure_entry, _) = cache.prepare_entry(&pressure.pressure_id, &pressure)?; let (grant_entry, _) = cache.prepare_entry(¤t_grant.grant_id, ¤t_grant)?; - let (ack_entry, _) = cache.prepare_entry(&ack.ack_id, &ack)?; + let (terminal_entry, _) = cache.prepare_entry(&terminal.receipt_id, &terminal)?; if !SingleFileMessagePackBackingStore::new(path).compare_and_swap_batch( &[expected_state, expected_pressure, expected_grant], - vec![state_entry, pressure_entry, grant_entry, ack_entry], + vec![state_entry, pressure_entry, grant_entry, terminal_entry], )? { return Err(anyhow!("resident Self lost derived-grant supersession CAS")); } - Ok(ack) + Ok(terminal) } pub fn prepare_resident_self_launch( @@ -2154,7 +2131,7 @@ pub fn prepare_resident_self_launch( let grant_expected = snapshot .iter() .find(|entry| { - entry.r#type == ::TYPE + entry.r#type == ::TYPE && entry.key == grant.grant_id }) .cloned() @@ -2405,7 +2382,7 @@ pub fn complete_resident_self_turn_after_death( recovery: &crate::EpiphanyCoordinatorDeathRecovery, now_millis: u64, cooldown_seconds: u64, -) -> Result { +) -> Result { if recovery.session_id != crate::coordinator_run_session_id(&lease.turn_id, Some(&lease.launch_digest))? || recovery.thread_id != lease.turn_id @@ -2439,30 +2416,27 @@ fn complete_resident_self_turn_with_terminal( now_millis: u64, cooldown_seconds: u64, count_failure: bool, -) -> Result { +) -> Result { let cache = state_cache(path)?; let mut state = cache .get::(RESIDENT_SELF_STATE_KEY)? - .ok_or_else(|| anyhow!("resident Self state missing at terminal ack"))?; + .ok_or_else(|| anyhow!("resident Self state missing at terminal receipt"))?; if state.active_turn.as_ref() != Some(lease) { return Err(anyhow!( - "resident Self active lease changed before terminal ack" + "resident Self active lease changed before terminal receipt" )); } let mut grant = cache - .get::(&lease.grant_id)? - .ok_or_else(|| anyhow!("resident Self grant missing at terminal ack"))?; - let ack = ResidentSelfTerminalAck { - schema_version: RESIDENT_SELF_ACK_SCHEMA_VERSION.into(), - ack_id: format!("resident-self-ack-{}", lease.grant_id), + .get::(&lease.grant_id)? + .ok_or_else(|| anyhow!("resident Self grant missing at terminal receipt"))?; + let terminal = ResidentSelfTerminalReceipt { + schema_version: RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: format!("resident-self-terminal-{}", lease.grant_id), grant_id: lease.grant_id.clone(), - heartbeat_schedule_id: grant.heartbeat_schedule_id.clone(), - heartbeat_action_id: grant.heartbeat_action_id.clone(), launch_digest: lease.launch_digest.clone(), coordinator_receipt_id: terminal_id.to_string(), terminal_status: terminal_status.to_string(), completed_at_millis: now_millis, - consumed_by_heartbeat_at_millis: None, private_state_exposed: false, }; let envelopes = cache.snapshot_envelopes(); @@ -2477,11 +2451,11 @@ fn complete_resident_self_turn_with_terminal( let expected_grant = envelopes .iter() .find(|entry| { - entry.r#type == ::TYPE + entry.r#type == ::TYPE && entry.key == grant.grant_id }) .cloned() - .ok_or_else(|| anyhow!("resident Self grant lost envelope at terminal ack"))?; + .ok_or_else(|| anyhow!("resident Self grant lost envelope at terminal receipt"))?; state.active_turn = None; state.last_coordinator_receipt_id = Some(terminal_id.to_string()); state.next_eligible_at_millis = @@ -2493,14 +2467,14 @@ fn complete_resident_self_turn_with_terminal( grant.terminal_status = Some(terminal_status.to_string()); let (state_entry, _) = cache.prepare_entry(RESIDENT_SELF_STATE_KEY, &state)?; let (grant_entry, _) = cache.prepare_entry(&grant.grant_id, &grant)?; - let (ack_entry, _) = cache.prepare_entry(&ack.ack_id, &ack)?; + let (terminal_entry, _) = cache.prepare_entry(&terminal.receipt_id, &terminal)?; if !SingleFileMessagePackBackingStore::new(path).compare_and_swap_batch( &[expected_state, expected_grant], - vec![state_entry, grant_entry, ack_entry], + vec![state_entry, grant_entry, terminal_entry], )? { - return Err(anyhow!("resident Self lost terminal-ack CAS")); + return Err(anyhow!("resident Self lost terminal-receipt CAS")); } - Ok(ack) + Ok(terminal) } pub fn cancel_resident_self_turn( @@ -2509,7 +2483,7 @@ pub fn cancel_resident_self_turn( status: &str, reason: &str, now_millis: u64, -) -> Result { +) -> Result { if !matches!( status, "brake-cancelled" | "shutdown-cancelled" | "timed-out" | "process-failed" | "unfulfilled" @@ -2527,7 +2501,7 @@ pub fn cancel_resident_self_turn( return Err(anyhow!("resident Self cancellation lease changed")); } let mut grant = cache - .get::(&lease.grant_id)? + .get::(&lease.grant_id)? .ok_or_else(|| anyhow!("resident Self cancellation grant missing"))?; let mut pressure = cache .get::(&grant.pressure_id)? @@ -2539,17 +2513,14 @@ pub fn cancel_resident_self_turn( "resident Self cancellation pressure authority changed" )); } - let ack = ResidentSelfTerminalAck { - schema_version: RESIDENT_SELF_ACK_SCHEMA_VERSION.into(), - ack_id: format!("resident-self-cancel-{}-{status}", lease.grant_id), + let terminal = ResidentSelfTerminalReceipt { + schema_version: RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: format!("resident-self-cancel-{}-{status}", lease.grant_id), grant_id: lease.grant_id.clone(), - heartbeat_schedule_id: grant.heartbeat_schedule_id.clone(), - heartbeat_action_id: grant.heartbeat_action_id.clone(), launch_digest: lease.launch_digest.clone(), coordinator_receipt_id: format!("resident-self-runtime-{status}-{}", lease.grant_id), terminal_status: status.into(), completed_at_millis: now_millis, - consumed_by_heartbeat_at_millis: None, private_state_exposed: false, }; let envelopes = cache.snapshot_envelopes(); @@ -2572,7 +2543,7 @@ pub fn cancel_resident_self_turn( let expected_grant = envelopes .iter() .find(|entry| { - entry.r#type == ::TYPE + entry.r#type == ::TYPE && entry.key == grant.grant_id }) .cloned() @@ -2589,68 +2560,23 @@ pub fn cancel_resident_self_turn( let (state_entry, _) = cache.prepare_entry(RESIDENT_SELF_STATE_KEY, &state)?; let (pressure_entry, _) = cache.prepare_entry(&pressure.pressure_id, &pressure)?; let (grant_entry, _) = cache.prepare_entry(&grant.grant_id, &grant)?; - let (ack_entry, _) = cache.prepare_entry(&ack.ack_id, &ack)?; + let (terminal_entry, _) = cache.prepare_entry(&terminal.receipt_id, &terminal)?; if !SingleFileMessagePackBackingStore::new(path).compare_and_swap_batch( &[expected_state, expected_pressure, expected_grant], - vec![state_entry, pressure_entry, grant_entry, ack_entry], + vec![state_entry, pressure_entry, grant_entry, terminal_entry], )? { return Err(anyhow!("resident Self lost cancellation CAS")); } - Ok(ack) + Ok(terminal) } -pub fn pending_resident_self_ack_for( - path: &Path, - schedule_id: &str, - action_id: &str, -) -> Result> { +pub fn resident_self_terminal_receipts(path: &Path) -> Result> { Ok(state_cache(path)? - .get_all::()? + .get_all::()? .into_iter() - .find(|ack| { - ack.heartbeat_schedule_id == schedule_id - && ack.heartbeat_action_id == action_id - && ack.consumed_by_heartbeat_at_millis.is_none() - })) -} - -pub fn pending_resident_self_acks(path: &Path) -> Result> { - Ok(state_cache(path)? - .get_all::()? - .into_iter() - .filter(|ack| ack.consumed_by_heartbeat_at_millis.is_none()) .collect()) } -pub fn heartbeat_consume_resident_self_ack( - path: &Path, - ack_id: &str, - now_millis: u64, -) -> Result<()> { - let cache = state_cache(path)?; - let mut ack = cache - .get::(ack_id)? - .ok_or_else(|| anyhow!("resident Self terminal ack is missing"))?; - if ack.consumed_by_heartbeat_at_millis.is_some() { - return Err(anyhow!("resident Self terminal ack was already consumed")); - } - let expected = cache - .snapshot_envelopes() - .into_iter() - .find(|entry| { - entry.r#type == ::TYPE && entry.key == ack_id - }) - .ok_or_else(|| anyhow!("terminal ack lost envelope"))?; - ack.consumed_by_heartbeat_at_millis = Some(now_millis); - let (replacement, _) = cache.prepare_entry(ack_id, &ack)?; - if !SingleFileMessagePackBackingStore::new(path) - .compare_and_swap_entry(&expected, replacement)? - { - return Err(anyhow!("heartbeat lost terminal ack CAS")); - } - Ok(()) -} - pub fn retain_resident_self_lifecycles( path: &Path, retain_closed: usize, @@ -2666,23 +2592,20 @@ pub fn retain_resident_self_lifecycles( .map(|pressure| (pressure.pressure_id.clone(), pressure)) .collect::>(); let grants = cache - .get_all::()? + .get_all::()? .into_iter() .map(|grant| (grant.grant_id.clone(), grant)) .collect::>(); let claims = cache.get_all::()?; let mut closed = cache - .get_all::()? + .get_all::()? .into_iter() - .filter_map(|ack| { - let grant = grants.get(&ack.grant_id)?; + .filter_map(|terminal| { + let grant = grants.get(&terminal.grant_id)?; let pressure = pressures.get(&grant.pressure_id)?; - let terminal_grant = if grant.schema_version == RESIDENT_SELF_GRANT_SCHEMA_VERSION { - grant.terminal_at_millis == Some(ack.completed_at_millis) - && grant.terminal_status.as_deref() == Some(ack.terminal_status.as_str()) - } else { - grant.schema_version == "epiphany.resident_self.heartbeat_grant.v0" - }; + let terminal_grant = grant.schema_version == RESIDENT_SELF_GRANT_SCHEMA_VERSION + && grant.terminal_at_millis == Some(terminal.completed_at_millis) + && grant.terminal_status.as_deref() == Some(terminal.terminal_status.as_str()); let inactive = state.active_turn.as_ref().map(|lease| &lease.grant_id) != Some(&grant.grant_id) && state @@ -2690,15 +2613,14 @@ pub fn retain_resident_self_lifecycles( .as_ref() .map(|prepared| &prepared.grant.grant_id) != Some(&grant.grant_id); - (ack.consumed_by_heartbeat_at_millis.is_some() - && grant.consumed_at_millis.is_some() + (grant.consumed_at_millis.is_some() && terminal_grant && pressure.status == "consumed" && pressure.consumed_by_grant_id.as_deref() == Some(grant.grant_id.as_str()) && inactive) .then_some(( - ack.completed_at_millis, - ack.ack_id.clone(), + terminal.completed_at_millis, + terminal.receipt_id.clone(), grant.grant_id.clone(), )) }) @@ -2733,8 +2655,8 @@ pub fn retain_resident_self_lifecycles( .ok_or_else(|| anyhow!("closed resident lifecycle lost its pressure"))?; for (entry_type, key) in [ (ResidentSelfPressure::TYPE, pressure.pressure_id.as_str()), - (ResidentSelfHeartbeatGrant::TYPE, grant_id.as_str()), - (ResidentSelfTerminalAck::TYPE, ack_id.as_str()), + (ResidentSelfGrant::TYPE, grant_id.as_str()), + (ResidentSelfTerminalReceipt::TYPE, ack_id.as_str()), ] { expected.push( snapshot @@ -2956,15 +2878,13 @@ mod coordinator_launch_contract_tests { }, )?; let policy = test_policy(temp.path(), runtime_store); - let grant = ResidentSelfHeartbeatGrant { + let grant = ResidentSelfGrant { schema_version: RESIDENT_SELF_GRANT_SCHEMA_VERSION.into(), grant_id: "legacy-receipt-grant".into(), pressure_id: "legacy-receipt-pressure".into(), pressure_kind: RESIDENT_SELF_COORDINATOR_CONTINUATION_PRESSURE_KIND.into(), provenance_ref: "cultcache://coordinator-receipt/old-receipt".into(), objective: "Replay the old receipt action".into(), - heartbeat_schedule_id: "schedule".into(), - heartbeat_action_id: "action".into(), issued_at_millis: 1, consumed_at_millis: None, private_state_exposed: false, @@ -2974,18 +2894,6 @@ mod coordinator_launch_contract_tests { assert!(resident_coordinator_binding_for_grant(&policy, &grant)?.is_none()); Ok(()) } - - #[test] - fn domain_materialization_has_no_scheduler_pressure_writer() { - let source = include_str!("resident_self.rs"); - let body = source - .split("pub fn materialize_resident_self_domain_obligations") - .nth(1) - .and_then(|tail| tail.split("fn resident_self_current_work_objective").next()) - .expect("domain materialization function must remain inspectable"); - assert!(!body.contains("ResidentSelfPressure")); - assert!(!body.contains("enqueue_resident_self_pressure")); - } } #[cfg(test)] @@ -3020,25 +2928,20 @@ mod atlas_pressure_tests { Ok(()) } - fn atlas_grant( - lane: crate::AtlasImpactLane, - proposal_id: uuid::Uuid, - ) -> ResidentSelfHeartbeatGrant { + fn atlas_grant(lane: crate::AtlasImpactLane, proposal_id: uuid::Uuid) -> ResidentSelfGrant { let (kind, lane_name) = match lane { crate::AtlasImpactLane::Modeling => { (RESIDENT_SELF_ATLAS_MODELING_PRESSURE_KIND, "Modeling") } crate::AtlasImpactLane::Soul => (RESIDENT_SELF_ATLAS_SOUL_PRESSURE_KIND, "Soul"), }; - ResidentSelfHeartbeatGrant { + ResidentSelfGrant { schema_version: RESIDENT_SELF_GRANT_SCHEMA_VERSION.into(), grant_id: format!("atlas-test-grant-{proposal_id}"), pressure_id: format!("{kind}-{proposal_id}"), pressure_kind: kind.into(), provenance_ref: format!("{RESIDENT_SELF_ATLAS_IMPACT_PROVENANCE_PREFIX}{proposal_id}"), objective: resident_self_atlas_objective(lane_name, proposal_id), - heartbeat_schedule_id: "atlas-test-schedule".into(), - heartbeat_action_id: "atlas-test-action".into(), issued_at_millis: 100, consumed_at_millis: Some(200), private_state_exposed: false, @@ -3047,7 +2950,7 @@ mod atlas_pressure_tests { } } - fn atlas_lease(grant: &ResidentSelfHeartbeatGrant) -> ResidentSelfTurnLease { + fn atlas_lease(grant: &ResidentSelfGrant) -> ResidentSelfTurnLease { ResidentSelfTurnLease { turn_id: "atlas-test-turn".into(), wake: ResidentSelfWake::Explicit { @@ -3117,7 +3020,7 @@ mod atlas_pressure_tests { Some("launchVerification") ); - let grant = heartbeat_issue_resident_self_grant(&store, "atlas", "wake", 102)? + let grant = issue_resident_self_grant(&store, 102)? .expect("oldest Atlas pressure should receive one resident grant"); assert_eq!( grant.pressure_kind, @@ -3155,9 +3058,7 @@ mod atlas_pressure_tests { &atlas_decision(crate::AtlasImpactLane::Modeling, active_id), 100, )?; - assert!( - heartbeat_issue_resident_self_grant(&active_store, "atlas", "active", 101)?.is_none() - ); + assert!(issue_resident_self_grant(&active_store, 101)?.is_none()); let prepared_store = test_store("prepared"); let prepared_id = uuid::Uuid::new_v4(); @@ -3187,10 +3088,7 @@ mod atlas_pressure_tests { &atlas_decision(crate::AtlasImpactLane::Soul, prepared_id), 100, )?; - assert!( - heartbeat_issue_resident_self_grant(&prepared_store, "atlas", "prepared", 101)? - .is_none() - ); + assert!(issue_resident_self_grant(&prepared_store, 101)?.is_none()); assert!(pending_resident_self_pressure(&active_store)?); assert!(pending_resident_self_pressure(&prepared_store)?); std::fs::remove_file(active_store)?; @@ -3228,7 +3126,7 @@ mod atlas_pressure_tests { assert!(state.active_turn.is_none()); assert_eq!(state.next_eligible_at_millis, 12_000); let grant = state_cache(&store)? - .get::(&grant.grant_id)? + .get::(&grant.grant_id)? .expect("completed Atlas grant"); assert_eq!(grant.terminal_at_millis, Some(5_000)); std::fs::remove_file(store)?; diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 9893c39f..777383ac 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -26,8 +26,6 @@ use crate::eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE; use crate::eyes_gateway::EyesEvidencePacket; use crate::eyes_gateway::EyesSourceLookupReceipt; use crate::hands_gateway::*; -use crate::heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION; -use crate::heartbeat_state::HEARTBEAT_STATE_TYPE; use crate::organ_dependencies::EpiphanyLaunchOrganContract; use crate::repo_model_gateway::{ REPO_FRONTIER_AUTONOMOUS_PROPOSAL_BINDING_CONTRACT, @@ -13945,20 +13943,6 @@ fn epiphany_mutation_contracts() -> Vec { "OpenAI terminal receipts are provider-specific evidence behind the model receipt.", ], ), - mutation_contract( - HEARTBEAT_STATE_TYPE, - HEARTBEAT_STATE_SCHEMA_VERSION, - vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::ReceiptWatch, - ], - CultNetMutationAuthority::ReadOnly, - vec![], - vec![], - vec![ - "Heartbeat is internal scheduling physiology for Resident Self grants and Persona turns; external callers may inspect it but cannot steer it.", - ], - ), mutation_contract( STATE_LEDGER_STORE_TYPE, STATE_LEDGER_SCHEMA_VERSION, diff --git a/epiphany-core/src/semantic_projector_service.rs b/epiphany-core/src/semantic_projector_service.rs index 5382340e..db6ca06e 100644 --- a/epiphany-core/src/semantic_projector_service.rs +++ b/epiphany-core/src/semantic_projector_service.rs @@ -290,17 +290,4 @@ mod tests { assert!(acquire_semantic_projector_singleton(&modeling).is_ok()); Ok(()) } - - #[test] - fn authority_body_has_no_public_raw_session_or_session_local_windows_lock() { - let source = include_str!("semantic_projector_service.rs"); - let public_session = ["pub struct Local", "IdunnSemanticProjectorSession"].concat(); - let public_singleton = ["pub fn acquire_semantic", "_projector_singleton"].concat(); - let local_namespace = ["Local", "\\\\EpiphanySemanticProjector"].concat(); - assert!(!source.contains(&public_session)); - assert!(!source.contains(&public_singleton)); - assert!(!source.contains(&local_namespace)); - assert!(source.contains("Global\\\\EpiphanySemanticProjector")); - assert!(source.contains("pub struct SemanticProjectorServiceBody")); - } } diff --git a/epiphany-core/src/surfaces/coordinator_decision.rs b/epiphany-core/src/surfaces/coordinator_decision.rs index 3f8fdc9c..f10c769b 100644 --- a/epiphany-core/src/surfaces/coordinator_decision.rs +++ b/epiphany-core/src/surfaces/coordinator_decision.rs @@ -290,22 +290,3 @@ fn planning_decision( }; Some(value) } - -#[cfg(test)] -mod authority_tests { - #[test] - fn source_has_no_aggregate_or_latest_lane_routing() { - let source = include_str!("coordinator_decision.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for forbidden in [ - "EpiphanyThreadState", - "accepted_after", - "latest_result", - "finding_signals", - "state_revision", - "coordinator_run_receipt", - ] { - assert!(!production.contains(forbidden)); - } - } -} diff --git a/epiphany-core/src/surfaces/jobs.rs b/epiphany-core/src/surfaces/jobs.rs index 92ae1355..f657de16 100644 --- a/epiphany-core/src/surfaces/jobs.rs +++ b/epiphany-core/src/surfaces/jobs.rs @@ -220,19 +220,4 @@ mod tests { assert_eq!(jobs[2].status, EpiphanyJobStatus::Blocked); assert!(jobs.iter().all(|job| job.runtime_job_id.is_none())); } - - #[test] - fn source_contains_no_aggregate_job_or_runtime_link_owner() { - let source = include_str!("jobs.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for forbidden in [ - "EpiphanyThreadState", - "EpiphanyJobBinding", - "EpiphanyRuntimeLink", - "latest_runtime_link", - "state_revision", - ] { - assert!(!production.contains(forbidden)); - } - } } diff --git a/epiphany-core/src/surfaces/research_decision.rs b/epiphany-core/src/surfaces/research_decision.rs index 54e51921..b4030934 100644 --- a/epiphany-core/src/surfaces/research_decision.rs +++ b/epiphany-core/src/surfaces/research_decision.rs @@ -120,20 +120,4 @@ mod tests { duplicate.evidence.push(evidence("evidence-a")); assert!(duplicate.validate().is_err()); } - - #[test] - fn role_decision_sources_have_no_generic_patch_authority() { - let retired_type = ["Epiphany", "Role", "State", "Patch", "Document"].concat(); - let retired_field = ["state", "Patch"].concat(); - for source in [ - include_str!("../agent_launch.rs"), - include_str!("../current_work.rs"), - include_str!("../runtime_spine.rs"), - include_str!("role_result.rs"), - ] { - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - assert!(!production.contains(&retired_type)); - assert!(!production.contains(&retired_field)); - } - } } diff --git a/epiphany-core/src/surfaces/role_board.rs b/epiphany-core/src/surfaces/role_board.rs index 3cf24ed2..21823e4f 100644 --- a/epiphany-core/src/surfaces/role_board.rs +++ b/epiphany-core/src/surfaces/role_board.rs @@ -314,21 +314,3 @@ pub fn render_role_board_note( "Mind {state_status}; {active} role lane(s) carry current work; continuity recommendation {recommendation:?}." ) } - -#[cfg(test)] -mod tests { - #[test] - fn source_has_no_generic_lane_or_aggregate_authority() { - let source = include_str!("role_board.rs"); - let production = source.split("#[cfg(test)]").next().unwrap_or(source); - for forbidden in [ - "EpiphanyThreadState", - "binding_id", - "acceptance_receipt", - "state_revision", - "epiphany.coordinator.role.launch", - ] { - assert!(!production.contains(forbidden)); - } - } -} diff --git a/epiphany-release-construction/src/lib.rs b/epiphany-release-construction/src/lib.rs index 735b27e4..77550988 100644 --- a/epiphany-release-construction/src/lib.rs +++ b/epiphany-release-construction/src/lib.rs @@ -106,7 +106,6 @@ pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static ("repository-body", file_name("epiphany-repository-body")), ("host-identity", file_name("epiphany-host-identity")), ("swarm", file_name("epiphany-swarm")), - ("heartbeat", file_name("epiphany-heartbeat-store")), ("runtime-spine", file_name("epiphany-runtime-spine")), ( "persona-feedback-ingress", @@ -632,7 +631,6 @@ fn required_release_build_target(role: &str) -> Result<(&'static str, &'static s "repository-body" => Ok(("epiphany-core", "epiphany-repository-body")), "host-identity" => Ok(("epiphany-core", "epiphany-host-identity")), "swarm" => Ok(("epiphany-core", "epiphany-swarm")), - "heartbeat" => Ok(("epiphany-core", "epiphany-heartbeat-store")), "runtime-spine" => Ok(("epiphany-core", "epiphany-runtime-spine")), "persona-feedback-ingress" => Ok(("epiphany-core", "epiphany-persona-feedback-ingress")), "persona-service" => Ok(("epiphany-openai-runtime", "epiphany-persona-service")), @@ -1259,7 +1257,6 @@ mod tests { .collect::>(); for role in [ "swarm", - "heartbeat", "runtime-spine", "state-steward", "coordinator", @@ -1277,10 +1274,6 @@ mod tests { required_release_build_target("swarm").unwrap(), ("epiphany-core", "epiphany-swarm") ); - assert_eq!( - required_release_build_target("heartbeat").unwrap(), - ("epiphany-core", "epiphany-heartbeat-store") - ); assert_eq!( required_release_build_target("runtime-spine").unwrap(), ("epiphany-core", "epiphany-runtime-spine") diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 9e5e4dd9..b09e0e2e 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -266,9 +266,9 @@ authority. These are distinct authorities and must not be collapsed into one state enum: -- Heartbeat owns scheduling pressure and terminal acknowledgement consumption. -- Resident Self owns grant, coordinator lease, settlement, cooldown, and - requeue. +- Resident Self owns typed pressure, exact grant creation, prepared and active + launch exclusion, coordinator lease, settlement, cooldown, retry, terminal + receipts, and bounded lifecycle retention. - Coordinator process owns its incarnation receipt or exact-death closure. - Runtime worker attempt owns immutable launch, process claim, activation, typed result, job result, and terminal attempt classification. @@ -279,19 +279,13 @@ The attempt aggregate centralizes exact request association and typed terminal classification. Archived fulfilled attempts must retain recoverable decision authority, not merely an ID/digest tombstone. -Heartbeat is scheduling physiology only. Exact `d1685df8` deletes its -personality, appraisal, rumination, dreaming, void-routine cognition, and -memory-graph paths. Exact `39dd9fdb` deletes the parallel agent-memory -identity/generation store, utterance vector, and generic self-patch admission. -When no explicit typed pressure exists, heartbeat sleeps and authors no Mind -state. - -Exact `2164bd0a` removes the unowned scheduler generality. Heartbeat has one -live participant: Resident Self. Ghostlight scene scheduling, generic -tick/pump/heat/complete controls, adaptive pacing, initiative heat, and their -public intent schemas are gone. Modeling, Eyes, Hands, Soul, and Imagination -route from typed current-work obligations and never acquire idle heartbeat -lanes. +The heartbeat scheduler does not exist. Resident Self directly selects the +oldest pending pressure when no grant, prepared launch, or active lease exists. +Grant creation consumes that exact pressure through one batch CAS. Terminal +settlement writes the final receipt and terminal grant state atomically; there +is no acknowledgement consumer, scheduler history, pacing head, stale repair, +or cross-store reconciliation loop. A loop iteration is physiology only and +authors no state when there is no unresolved obligation. Persona owns a separate keyed social corpus. Each mention, immutable turn request, terminal receipt, quarantine record, retention head, and retention @@ -299,7 +293,8 @@ plan has its own CultCache identity. The Persona daemon derives an exact request from pending mentions, reserves them through batch CAS, and cites that request envelope as the observed source of its admitted pass input. Failed turns make the mention pending with the terminal receipt bound into its next deterministic -request identity. Heartbeat cannot launch, block, or terminalize Persona work. +request identity. Resident Self cannot launch, block, or terminalize Persona +work. ## RepoModel and semantic projection diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 036c3774..1a3ed51a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -3,7 +3,7 @@ Updated: 2026-08-23 Branch: `codex/epiphany-shakedown-live` Latest committed implementation cut: `39dd9fdb` -Current worktree: heartbeat concurrency-domain audit; Ox17 remains paused +Current worktree: final heartbeat deletion and behavioral-test audit; Ox17 remains paused ## Orientation @@ -28,8 +28,8 @@ authorize the next capstone or Gate 1. `de0fc6b360ce03493b13208d917dc8349801f03364617d966152c85846c47482`. - Model provider: OpenRouter `stealth/ox-alpha`, selected explicitly and injected through the root-owned systemd credential boundary. -- `epiphany.service`, `epiphany-heartbeat.service`, and - `epiphany-swarm.service` are inactive with zero restarts after the hard epoch +- `epiphany.service` and `epiphany-swarm.service` are inactive with zero + restarts after the hard epoch refusal rollback. The d2ca stores and swarm brake remain historical production state; no newer package is admitted there. - Idunn source `8b972715c47731f2418d0c423cb0dd2076940bd7` is provenance-exact and @@ -44,6 +44,30 @@ authorize the next capstone or Gate 1. ## What just landed +The current uncommitted subtraction removes the final heartbeat scheduler. + +- Resident Self directly owns typed pressure selection, pressure-to-grant + batch CAS, prepared and active launch exclusion, leases, terminal receipts, + retry, and bounded retention. Terminal receipts are final records; no second + daemon consumes acknowledgements. +- Persona owns its separate keyed social store. The swarm receives that store + directly and cannot use a scheduler singleton to launch, block, retain, or + terminalize Persona work. +- The heartbeat Rust module, six submodules, CLI binary, release role, schema, + store contract, systemd unit, deployment lifecycle, readiness checks, + artifact directory, pacing/history state, stale repair, and old cross-store + reconciliation are deleted. Old heartbeat grants, acknowledgements, and + Resident Self v0 stores fail the writable epoch check. +- Native core passes 426/426 after the first fake-test pruning slice. Twenty- + one tests across library and runtime targets that only inspected source + spelling, copied removed fields, or exercised one-line helper truth tables + are gone. Daemon supervisor passes 29/29, coordinator 8/8, status 1/1, and + swarm 4/4. Exact CAS, hostile substitution, concurrency, lifecycle, + readiness, and re-entry proofs remain. +- The gamecult-ops contract removes 52 net lines, and Git Bash syntax plus the + deployment outage-order check pass. Bootstrap retains one deliberate cleanup + line to remove an obsolete installed heartbeat unit. + The global thread-state authority is gone. - `1662a012` makes coordinator policy a pure projection over keyed Mind, diff --git a/schemas/README.md b/schemas/README.md index 4799d373..83f5a760 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -3,7 +3,7 @@ This folder is the canonical paperwork shrine for Epiphany's shared state contracts. -If a Persona field, Mind document, or heartbeat scheduling surface matters +If a Persona field or Mind document matters enough to steer the machine, it should have a receipt here instead of living only in one Rust struct or one developer's damp recollection. @@ -19,8 +19,6 @@ only in one Rust struct or one developer's damp recollection. companion custom-label fields. - [cultnet/epiphany.work_organ_state.v0.schema.json](./cultnet/epiphany.work_organ_state.v0.schema.json): light function-shaped state for Epiphany internal work organs. -- [heartbeat-state-schema.md](./heartbeat-state-schema.md): - typed initiative and routine-state contract for the swarm heartbeat organ. - [cultnet/README.md](./cultnet/README.md): published CultNet-facing state, surface, intent, and receipt schemas that Aquarium and other runtimes can discover through Epiphany's schema-catalog @@ -30,7 +28,6 @@ only in one Rust struct or one developer's damp recollection. The living implementation is in code: -- [heartbeat_state.rs](/E:/Projects/EpiphanyAgent/epiphany-core/src/heartbeat_state.rs) - [mind_documents.rs](/E:/Projects/EpiphanyAgent/epiphany-core/src/mind_documents.rs) The rule is simple: @@ -45,7 +42,6 @@ When changing any of the following, update this folder in the same pass: - canonical organ-state or Persona family names - standing role trait names -- heartbeat store shape or pacing semantics - schema version identifiers If a change lands in code without a matching receipt here, assume the machine diff --git a/schemas/cultnet/epiphany.agent-heartbeat.schema.json b/schemas/cultnet/epiphany.agent-heartbeat.schema.json deleted file mode 100644 index c7806efa..00000000 --- a/schemas/cultnet/epiphany.agent-heartbeat.schema.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-heartbeat.schema.json", - "title": "Epiphany Heartbeat State", - "description": "Typed scheduling physiology for Resident Self grants.", - "type": "object", - "additionalProperties": false, - "properties": { - "schemaVersion": { - "const": "epiphany.agent_heartbeat.v1" - }, - "targetHeartbeatRate": { - "type": "number" - }, - "sceneClock": { - "type": "number" - }, - "selectionPolicy": { - "type": "object", - "additionalProperties": true - }, - "pacingPolicy": { - "type": "object", - "additionalProperties": true - }, - "participants": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } - }, - "history": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } - } - }, - "required": [ - "schemaVersion", - "targetHeartbeatRate", - "sceneClock", - "selectionPolicy", - "pacingPolicy", - "participants", - "history" - ] -} diff --git a/schemas/cultnet/index.json b/schemas/cultnet/index.json index 5de8c3e0..9cb3c14b 100644 --- a/schemas/cultnet/index.json +++ b/schemas/cultnet/index.json @@ -257,17 +257,6 @@ "title": "Epiphany Work Organ State v0", "path": "epiphany.work_organ_state.v0.schema.json" }, - { - "schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-heartbeat.schema.json", - "kind": "document_payload", - "wireContracts": [ - "cultnet.schema.v0" - ], - "schemaVersion": "epiphany.agent_heartbeat.v1", - "documentType": "epiphany.agent_heartbeat", - "title": "Epiphany Heartbeat State", - "path": "epiphany.agent-heartbeat.schema.json" - }, { "schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.state-ledger.schema.json", "kind": "document_payload", diff --git a/schemas/heartbeat-state-schema.md b/schemas/heartbeat-state-schema.md deleted file mode 100644 index 8f84390c..00000000 --- a/schemas/heartbeat-state-schema.md +++ /dev/null @@ -1,41 +0,0 @@ -# Heartbeat State Schema - -Epiphany's heartbeat store is scheduling physiology. It decides when an -already-existing typed obligation may receive a turn; it does not decide what -the organism believes or invent cognition to fill idle time. - -The executable contract lives in -`epiphany-core/src/heartbeat_state/heartbeat_documents.rs` and the scheduling -law lives in `heartbeat_state.rs` and `heartbeat_store.rs`. - -## Store identity - -- document type: `epiphany.agent_heartbeat` -- schema version: `epiphany.agent_heartbeat.v1` -- key: `default` - -The singleton carries Resident Self scheduler readiness, one pending turn, and -bounded scheduling history. Persona social state is not part of this document. -Pending mentions, immutable turn requests, terminal receipts, quarantine -records, and retention state are keyed CultCache documents owned by Persona. - -## Scheduling invariants - -- An existing pending turn cannot be scheduled again. -- Cooldown begins after terminal completion, not at launch. -- Resident Self pressure may wake the coordinator; heartbeat does not create - that pressure. -- The swarm brake prevents new Resident Self cognition while allowing terminal - acknowledgement and recovery physiology to settle. -- When no typed obligation exists, the scheduler sleeps. - -Heartbeat owns no personality, mood, appraisal, rumination, dreaming, memory -graph, utterance vector, or generic self-modification surface. Durable Persona -memory and social interpretation are keyed Mind documents admitted through the -Persona decision context. Persona derives its own current turn from pending -keyed social documents; heartbeat cannot launch, block, or terminalize it. Work -for Modeling, Eyes, Hands, Soul, and Imagination comes from the pure -current-work projection over typed state obligations. - -JSON emitted by the heartbeat CLI is an operator artifact only. CultCache -documents remain authority. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 4c9b2c10d8302c056d1507a5c56409c4b7203083..23934e16754ae8fbd54dac4857c00d191bab22e4 100644 GIT binary patch delta 526 zcmXAlJ#W-N5Qg#bnSzFb4rwF`C}QuObBZqK5-BwbX`tJ92YbDA~Zn5*c@6P3)M_=AQ{&qjRyIhy^_444w<+Z)P=ieV@ue0pe z#cuXtH_Mdiysq3(ZYsBw+sd6EyV>62_q|U)%XxX2&sTYQa5|q{pFh6K9&EoJ`)C8S zXp@|w72vu>Hv~*z2WcsJ8Np#dLAQnzWbZHvJj3u3oKILw7=bpCp-;#>Bs5L3QA|L~ zXarL?1(>9QD4t>kPcbn?3rw}&YBq}%goK^-s&SrQk&jZq$-n{~O=ZlPO44Yia-9Vm z9XxLumn{1LL-b-C_JPK3>egFn8EismC}IImNUzKC&?pr+iycKMk4$+~!L}w!PUS)g zR%TUQwHX)|W})|uO&u)FvI6QEc#LA-UZv9@w!vIybx}1%Nw%JnnF~1GXp1q|Y={(V xJMi>L8d8lp_ZWhY&HgmRl8T|$