Remove heartbeat JSON migration hatches

This commit is contained in:
Meta Vens 2026-05-06 13:58:22 +01:00
commit 2bc2e0890f
4 changed files with 1 additions and 236 deletions

View file

@ -8,10 +8,8 @@ use epiphany_core::complete_heartbeat_store;
use epiphany_core::heartbeat_status_projection;
use epiphany_core::initialize_heartbeat_store;
use epiphany_core::load_heartbeat_state_entry;
use epiphany_core::migrate_heartbeat_json_to_cultcache;
use epiphany_core::tick_heartbeat_store;
use epiphany_core::validate_agent_memory_store;
use epiphany_core::write_heartbeat_json_projection;
use std::env;
use std::fs;
use std::path::Path;
@ -23,9 +21,7 @@ fn main() -> Result<()> {
let Some(command) = args.next() else {
return usage();
};
let mut json_path: Option<PathBuf> = None;
let mut store_path: Option<PathBuf> = None;
let mut projection_path: Option<PathBuf> = None;
let mut artifact_dir: Option<PathBuf> = None;
let mut target_heartbeat_rate = 1.0_f64;
let mut coordinator_action: Option<String> = None;
@ -42,9 +38,7 @@ fn main() -> Result<()> {
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => json_path = Some(next_path(&mut args, "--json")?),
"--store" => store_path = Some(next_path(&mut args, "--store")?),
"--projection" => projection_path = Some(next_path(&mut args, "--projection")?),
"--artifact-dir" => artifact_dir = Some(next_path(&mut args, "--artifact-dir")?),
"--target-heartbeat-rate" => {
target_heartbeat_rate = next_value(&mut args, "--target-heartbeat-rate")?.parse()?
@ -129,44 +123,6 @@ fn main() -> Result<()> {
)?;
println!("{}", result);
}
"migrate-json" => {
let json_path = json_path.ok_or_else(|| anyhow!("migrate-json requires --json"))?;
let store_path = store_path.ok_or_else(|| anyhow!("migrate-json requires --store"))?;
let state = migrate_heartbeat_json_to_cultcache(&json_path, &store_path)?;
if let Some(projection_path) = projection_path {
write_heartbeat_json_projection(&store_path, projection_path)?;
}
println!(
"{}",
serde_json::json!({
"ok": true,
"command": "migrate-json",
"json": json_path,
"store": store_path,
"schemaVersion": state.schema_version,
"participants": state.participants.len(),
"history": state.history.len(),
})
);
}
"project" => {
let store_path = store_path.ok_or_else(|| anyhow!("project requires --store"))?;
let projection_path =
projection_path.ok_or_else(|| anyhow!("project requires --projection"))?;
let state = write_heartbeat_json_projection(&store_path, &projection_path)?;
println!(
"{}",
serde_json::json!({
"ok": true,
"command": "project",
"store": store_path,
"projection": projection_path,
"schemaVersion": state.schema_version,
"participants": state.participants.len(),
"history": state.history.len(),
})
);
}
"status" => {
let store_path = store_path.ok_or_else(|| anyhow!("status requires --store"))?;
if let Some(artifact_dir) = artifact_dir {
@ -221,7 +177,7 @@ fn next_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<Str
fn usage() -> Result<()> {
Err(anyhow!(
"usage: epiphany-heartbeat-store init --store <path>\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store migrate-json --json <path> --store <path> [--projection <path>]\n epiphany-heartbeat-store project --store <path> --projection <path>\n epiphany-heartbeat-store smoke [--agent-store <path>]"
"usage: epiphany-heartbeat-store init --store <path>\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store smoke [--agent-store <path>]"
))
}

View file

@ -176,61 +176,6 @@ pub struct HeartbeatCompleteOptions {
pub action_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct EpiphanyHeartbeatStateProjection {
pub schema_version: String,
pub target_heartbeat_rate: f64,
pub scene_clock: f64,
pub selection_policy: HeartbeatSelectionPolicy,
pub pacing_policy: HeartbeatPacingPolicy,
pub participants: Vec<HeartbeatParticipant>,
pub history: Vec<HeartbeatHistoryEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sleep_cycle: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_resonance: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub incubation: Option<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl From<EpiphanyHeartbeatStateProjection> for EpiphanyHeartbeatStateEntry {
fn from(value: EpiphanyHeartbeatStateProjection) -> Self {
Self {
schema_version: value.schema_version,
target_heartbeat_rate: value.target_heartbeat_rate,
scene_clock: value.scene_clock,
selection_policy: value.selection_policy,
pacing_policy: value.pacing_policy,
participants: value.participants,
history: value.history,
sleep_cycle: value.sleep_cycle,
memory_resonance: value.memory_resonance,
incubation: value.incubation,
extra: value.extra,
}
}
}
impl From<EpiphanyHeartbeatStateEntry> for EpiphanyHeartbeatStateProjection {
fn from(value: EpiphanyHeartbeatStateEntry) -> Self {
Self {
schema_version: value.schema_version,
target_heartbeat_rate: value.target_heartbeat_rate,
scene_clock: value.scene_clock,
selection_policy: value.selection_policy,
pacing_policy: value.pacing_policy,
participants: value.participants,
history: value.history,
sleep_cycle: value.sleep_cycle,
memory_resonance: value.memory_resonance,
incubation: value.incubation,
extra: value.extra,
}
}
}
pub fn heartbeat_state_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<EpiphanyHeartbeatStateEntry>()?;
@ -255,59 +200,6 @@ pub fn write_heartbeat_state_entry(
cache.put(HEARTBEAT_STATE_KEY, state)
}
pub fn migrate_heartbeat_json_to_cultcache(
json_path: impl AsRef<Path>,
store_path: impl AsRef<Path>,
) -> Result<EpiphanyHeartbeatStateEntry> {
let state = read_heartbeat_json_projection(json_path)?;
write_heartbeat_state_entry(store_path, &state)
}
pub fn write_heartbeat_json_projection(
store_path: impl AsRef<Path>,
json_path: impl AsRef<Path>,
) -> Result<EpiphanyHeartbeatStateEntry> {
let state = load_heartbeat_state_entry(store_path.as_ref())?.ok_or_else(|| {
anyhow!(
"CultCache store {} has no heartbeat state entry",
store_path.as_ref().display()
)
})?;
write_projection_json(json_path, &state)?;
Ok(state)
}
pub fn read_heartbeat_json_projection(
json_path: impl AsRef<Path>,
) -> Result<EpiphanyHeartbeatStateEntry> {
let path = json_path.as_ref();
let raw = fs::read_to_string(path)
.with_context(|| format!("failed to read heartbeat JSON {}", path.display()))?;
let projection: EpiphanyHeartbeatStateProjection = serde_json::from_str(&raw)
.with_context(|| format!("failed to decode heartbeat JSON {}", path.display()))?;
let state = EpiphanyHeartbeatStateEntry::from(projection);
validate_heartbeat_state(&state)?;
Ok(state)
}
pub fn write_projection_json(
json_path: impl AsRef<Path>,
state: &EpiphanyHeartbeatStateEntry,
) -> Result<()> {
validate_heartbeat_state(state)?;
let path = json_path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let projection = EpiphanyHeartbeatStateProjection::from(state.clone());
let raw = serde_json::to_string_pretty(&projection)
.context("failed to encode heartbeat JSON projection")?;
fs::write(path, format!("{raw}\n"))
.with_context(|| format!("failed to write heartbeat JSON {}", path.display()))?;
Ok(())
}
pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<()> {
if state.schema_version != HEARTBEAT_STATE_SCHEMA_VERSION {
return Err(anyhow!(
@ -1273,45 +1165,6 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn heartbeat_json_migrates_to_typed_cultcache_and_projects_back() -> Result<()> {
let temp = tempfile::tempdir()?;
let json_path = temp.path().join("heartbeats.json");
let store_path = temp.path().join("heartbeats.msgpack");
let projected_path = temp.path().join("heartbeats.projected.json");
fs::write(&json_path, sample_json())?;
let migrated = migrate_heartbeat_json_to_cultcache(&json_path, &store_path)?;
assert_eq!(migrated.schema_version, HEARTBEAT_STATE_SCHEMA_VERSION);
assert_eq!(migrated.participants[0].role_id, "coordinator");
let loaded = load_heartbeat_state_entry(&store_path)?.expect("persisted heartbeat state");
assert_eq!(loaded, migrated);
write_heartbeat_json_projection(&store_path, &projected_path)?;
let projected = read_heartbeat_json_projection(&projected_path)?;
assert_eq!(projected, migrated);
Ok(())
}
#[test]
fn live_heartbeat_json_shape_is_compatible_when_present() -> Result<()> {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../state/agent-heartbeats.json");
if !path.exists() {
return Ok(());
}
let state = read_heartbeat_json_projection(&path)?;
validate_heartbeat_state(&state)?;
assert!(state.participants.iter().any(|item| item.role_id == "face"));
assert!(
state
.participants
.iter()
.any(|item| item.role_id == "coordinator")
);
Ok(())
}
#[test]
fn native_heartbeat_store_ticks_and_completes_without_json_state() -> Result<()> {
let temp = tempfile::tempdir()?;
@ -1368,44 +1221,4 @@ mod tests {
assert!(artifact_dir.join("native-work.completion.json").exists());
Ok(())
}
fn sample_json() -> &'static str {
r#"{
"schema_version": "epiphany.agent_heartbeat.v0",
"target_heartbeat_rate": 1.0,
"scene_clock": 0.0,
"selection_policy": {
"mode": "readiness_queue",
"reaction_precedence": true,
"minimum_speed": 0.2,
"tie_breakers": ["next_ready_at_asc"]
},
"pacing_policy": {
"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": [
{
"agent_id": "epiphany.self",
"role_id": "coordinator",
"display_name": "Self",
"initiative_speed": 1.28,
"next_ready_at": 0.0,
"reaction_bias": 0.88,
"interrupt_threshold": 0.42,
"current_load": 0.0,
"status": "active",
"constraints": ["Routes and reviews."],
"last_action_id": null,
"last_woke_at": null,
"last_finished_at": null,
"pending_turn": null
}
],
"history": []
}"#
}
}

View file

@ -44,13 +44,9 @@ 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::migrate_heartbeat_json_to_cultcache;
pub use heartbeat_state::read_heartbeat_json_projection;
pub use heartbeat_state::tick_heartbeat_store;
pub use heartbeat_state::validate_heartbeat_state;
pub use heartbeat_state::write_heartbeat_json_projection;
pub use heartbeat_state::write_heartbeat_state_entry;
pub use heartbeat_state::write_projection_json;
pub use promotion::EpiphanyPromotionDecision;
pub use promotion::EpiphanyPromotionInput;
pub use promotion::EpiphanyStateReplacementValidationInput;

Binary file not shown.