Remove heartbeat JSON migration hatches
This commit is contained in:
parent
2b28703d16
commit
2bc2e0890f
4 changed files with 1 additions and 236 deletions
|
|
@ -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>]"
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}"#
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
Reference in a new issue