From d1685df830d1a9df0aee7b697d336d2d6afe66ef Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 23 Aug 2026 03:18:21 +0200 Subject: [PATCH] Delete heartbeat cognition simulation --- epiphany-core/src/agent_utterance_state.rs | 104 +- .../src/bin/epiphany-heartbeat-store.rs | 643 +---- epiphany-core/src/heartbeat_state.rs | 2313 +---------------- .../heartbeat_state/heartbeat_documents.rs | 552 ---- .../src/heartbeat_state/heartbeat_pacing.rs | 75 +- .../heartbeat_state/heartbeat_projection.rs | 51 +- .../heartbeat_state/heartbeat_retention.rs | 80 +- .../src/heartbeat_state/heartbeat_roles.rs | 10 - .../src/heartbeat_state/heartbeat_store.rs | 165 +- epiphany-core/src/lib.rs | 20 - epiphany-core/src/memory_graph/mod.rs | 1 - .../src/memory_graph/profiles/heartbeat.rs | 479 ---- .../src/memory_graph/profiles/mod.rs | 2 - 13 files changed, 53 insertions(+), 4442 deletions(-) delete mode 100644 epiphany-core/src/memory_graph/profiles/heartbeat.rs diff --git a/epiphany-core/src/agent_utterance_state.rs b/epiphany-core/src/agent_utterance_state.rs index e3d9b31c..6f4cfb16 100644 --- a/epiphany-core/src/agent_utterance_state.rs +++ b/epiphany-core/src/agent_utterance_state.rs @@ -91,8 +91,6 @@ pub struct AgentUtteranceActivation { pub initiative_speed: f64, pub reaction_bias: f64, pub interrupt_threshold: f64, - pub personality_cooldown_multiplier: f64, - pub mood_cooldown_multiplier: f64, pub initiative_heat_multiplier: f64, pub effective_cooldown_multiplier: f64, pub pending_turn_active: bool, @@ -168,7 +166,7 @@ pub fn derive_agent_utterance_state( unforgivable_if_betrayed: value.unforgivable_if_betrayed, }) .collect::>(); - let mood = derive_utterance_mood(participant, mood_label, canonical); + let mood = derive_utterance_mood(mood_label, canonical); let activation = derive_utterance_activation(participant); let vector_source = source.into(); let identity = AgentUtteranceIdentity { @@ -211,65 +209,37 @@ pub fn derive_agent_utterance_state( } fn derive_utterance_mood( - participant: Option<&HeartbeatParticipant>, mood_label: Option<&str>, canonical: &GhostlightCanonicalState, ) -> AgentUtteranceMood { - let timing = participant.and_then(|participant| participant.mood_timing.as_ref()); let situational = &canonical.situational_state; - let anxiety = timing - .map(|mood| mood.anxiety) - .unwrap_or_else(|| trait_activation(situational, "anxiety")); - let urgency = timing - .map(|mood| mood.urgency) - .unwrap_or_else(|| trait_activation(situational, "urgency")); - let arousal = timing - .map(|mood| mood.arousal) - .unwrap_or_else(|| trait_activation(situational, "arousal")); - let thought_pressure = timing - .map(|mood| mood.thought_pressure) - .unwrap_or_else(|| trait_activation(situational, "thought_pressure")); - let guardedness = timing - .map(|mood| mood.guardedness) - .unwrap_or_else(|| trait_activation(situational, "guardedness")); - let reaction_intensity = timing - .map(|mood| mood.reaction_intensity) - .unwrap_or_else(|| trait_activation(situational, "reaction_intensity")); + let anxiety = trait_activation(situational, "anxiety"); + let urgency = trait_activation(situational, "urgency"); + let arousal = trait_activation(situational, "arousal"); + let thought_pressure = trait_activation(situational, "thought_pressure"); + let guardedness = trait_activation(situational, "guardedness"); + let reaction_intensity = trait_activation(situational, "reaction_intensity"); AgentUtteranceMood { label: mood_label .map(str::to_string) - .unwrap_or_else(|| inferred_mood_label(timing.map(|mood| mood.arousal), situational)), - source: timing.and_then(|mood| mood.source.clone()), - emotional_dimensions: timing - .filter(|mood| !mood.emotional_dimensions.is_empty()) - .map(|mood| { - mood.emotional_dimensions - .iter() - .map(|dimension| AgentUtteranceMoodDimension { - name: dimension.name.clone(), - value: dimension.value, - source_path: dimension.source_path.clone(), - }) - .collect() - }) - .unwrap_or_else(|| { - emotional_dimensions( - situational, - anxiety, - urgency, - arousal, - thought_pressure, - guardedness, - reaction_intensity, - ) - }), + .unwrap_or_else(|| inferred_mood_label(arousal, situational)), + source: None, + emotional_dimensions: emotional_dimensions( + situational, + anxiety, + urgency, + arousal, + thought_pressure, + guardedness, + reaction_intensity, + ), anxiety, urgency, arousal, thought_pressure, guardedness, reaction_intensity, - cooldown_multiplier: timing.map(|mood| mood.cooldown_multiplier).unwrap_or(1.0), + cooldown_multiplier: 1.0, } } @@ -362,24 +332,18 @@ fn derive_utterance_activation( initiative_speed: 1.0, reaction_bias: 0.0, interrupt_threshold: 0.0, - personality_cooldown_multiplier: 1.0, - mood_cooldown_multiplier: 1.0, initiative_heat_multiplier: 1.0, effective_cooldown_multiplier: 1.0, ..AgentUtteranceActivation::default() }; }; - let effective_cooldown_multiplier = participant.personality_cooldown_multiplier - * participant.mood_cooldown_multiplier - / participant.initiative_heat_multiplier.max(0.001); + let effective_cooldown_multiplier = 1.0 / participant.initiative_heat_multiplier.max(0.001); AgentUtteranceActivation { status: participant.status.clone(), current_load: participant.current_load, initiative_speed: participant.initiative_speed, reaction_bias: participant.reaction_bias, interrupt_threshold: participant.interrupt_threshold, - personality_cooldown_multiplier: participant.personality_cooldown_multiplier, - mood_cooldown_multiplier: participant.mood_cooldown_multiplier, initiative_heat_multiplier: participant.initiative_heat_multiplier, effective_cooldown_multiplier, pending_turn_active: participant.pending_turn.is_some(), @@ -390,10 +354,9 @@ fn derive_utterance_activation( } fn inferred_mood_label( - arousal: Option, - situational: &BTreeMap, + arousal: f64, + _situational: &BTreeMap, ) -> String { - let arousal = arousal.unwrap_or_else(|| trait_activation(situational, "arousal")); if arousal >= 0.75 { "activated".to_string() } else if arousal <= 0.25 { @@ -1029,10 +992,6 @@ fn activation_value(activation: &AgentUtteranceActivation, name: &str) -> f64 { "cooldown_pressure" => multiplier_pressure(activation.effective_cooldown_multiplier), "reaction_bias" => activation.reaction_bias, "interrupt_threshold" => activation.interrupt_threshold, - "personality_cooldown_multiplier" => { - multiplier_pressure(activation.personality_cooldown_multiplier) - } - "mood_cooldown_multiplier" => multiplier_pressure(activation.mood_cooldown_multiplier), "initiative_speed" => multiplier_pressure(activation.initiative_speed), _ => 0.0, } @@ -1194,7 +1153,6 @@ mod tests { use crate::agent_memory::GhostlightMemories; use crate::agent_memory::GhostlightValue; use crate::agent_memory::GhostlightWorld; - use crate::heartbeat_state::HeartbeatMoodTiming; #[test] fn utterance_state_projects_voice_without_memories() -> anyhow::Result<()> { @@ -1215,6 +1173,11 @@ mod tests { ("dismissal", 0.58), ("flippancy", 0.67), ("emotional_containment", 0.49), + ("urgency", 0.74), + ("arousal", 0.82), + ("thought_pressure", 0.69), + ("guardedness", 0.21), + ("reaction_intensity", 0.77), ] { situational_state.insert( name.to_string(), @@ -1280,20 +1243,7 @@ mod tests { last_woke_at: Some("2026-05-20T12:00:00+00:00".to_string()), last_finished_at: None, pending_turn: None, - personality_cooldown_multiplier: 0.8, - mood_cooldown_multiplier: 0.7, initiative_heat_multiplier: 1.5, - mood_timing: Some(HeartbeatMoodTiming { - source: Some("heartbeat appraisal".to_string()), - anxiety: 0.33, - urgency: 0.74, - arousal: 0.82, - thought_pressure: 0.69, - guardedness: 0.21, - reaction_intensity: 0.77, - cooldown_multiplier: 0.7, - ..HeartbeatMoodTiming::default() - }), ..HeartbeatParticipant::default() }; diff --git a/epiphany-core/src/bin/epiphany-heartbeat-store.rs b/epiphany-core/src/bin/epiphany-heartbeat-store.rs index ff7d8f68..e1ed27da 100644 --- a/epiphany-core/src/bin/epiphany-heartbeat-store.rs +++ b/epiphany-core/src/bin/epiphany-heartbeat-store.rs @@ -10,8 +10,6 @@ use epiphany_core::HeartbeatPumpOptions; use epiphany_core::HeartbeatQueueMentionOptions; use epiphany_core::HeartbeatStaleTurnRepairOptions; use epiphany_core::HeartbeatTickOptions; -use epiphany_core::VoidRoutineOptions; -use epiphany_core::apply_agent_self_patch_document; use epiphany_core::complete_heartbeat_store; use epiphany_core::heartbeat_local_provider_status; use epiphany_core::heartbeat_status_projection; @@ -26,10 +24,8 @@ use epiphany_core::queue_heartbeat_pending_mention_store; use epiphany_core::recover_stale_heartbeat_store; use epiphany_core::resident_cognitive_runtime_id; use epiphany_core::retain_heartbeat_pulse_artifacts; -use epiphany_core::run_void_routine_store; use epiphany_core::tick_heartbeat_store; use epiphany_core::update_heartbeat_heat_store; -use epiphany_core::validate_agent_memory_store; use epiphany_core::{ ResidentProviderReadiness, acquire_resident_process_singleton, authenticate_epiphany_packaged_release, capture_process_instance, @@ -45,7 +41,6 @@ use std::sync::{ }; use std::thread; use std::time::Duration; -use uuid::Uuid; fn main() -> Result<()> { let mut args = env::args().skip(1); @@ -71,15 +66,11 @@ fn main() -> Result<()> { let mut role: Option = None; let mut action_id: Option = None; let mut limit = 8_usize; - let mut agent_store: Option = None; let mut resident_self_store: Option = None; let mut resident_runtime_store: Option = None; - let mut apply_rumination = false; let mut profile = "epiphany".to_string(); let mut scene_id = "ghostlight.scene".to_string(); let mut scene_participants = Vec::::new(); - let mut source = "epiphany/void-routine".to_string(); - let mut no_dream = false; let mut heat_scope = "global".to_string(); let mut heat_selector = String::new(); let mut heat_multiplier = 1.0_f64; @@ -148,21 +139,17 @@ fn main() -> Result<()> { "--role" => role = Some(next_value(&mut args, "--role")?), "--action-id" => action_id = Some(next_value(&mut args, "--action-id")?), "--limit" => limit = next_value(&mut args, "--limit")?.parse()?, - "--agent-store" => agent_store = Some(next_path(&mut args, "--agent-store")?), "--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")?) } - "--apply-rumination" => apply_rumination = true, "--profile" => profile = next_value(&mut args, "--profile")?, "--scene-id" => scene_id = next_value(&mut args, "--scene-id")?, "--scene-participant" => scene_participants.push(parse_scene_participant( &next_value(&mut args, "--scene-participant")?, )?), - "--source" => source = next_value(&mut args, "--source")?, - "--no-dream" => no_dream = true, "--scope" => heat_scope = next_value(&mut args, "--scope")?, "--selector" => heat_selector = next_value(&mut args, "--selector")?, "--multiplier" => heat_multiplier = next_value(&mut args, "--multiplier")?.parse()?, @@ -257,15 +244,6 @@ fn main() -> Result<()> { let store_path = store_path.ok_or_else(|| anyhow!("tick requires --store"))?; let artifact_dir = artifact_dir.ok_or_else(|| anyhow!("tick requires --artifact-dir"))?; - if let Some(agent_store) = &agent_store { - let errors = validate_agent_memory_store(agent_store)?; - if !errors.is_empty() { - return Err(anyhow!( - "agent memory validation failed: {}", - errors.join("; ") - )); - } - } let tick_schedule_id = schedule_id.clone(); let mut result = tick_heartbeat_store( &store_path, @@ -278,16 +256,9 @@ fn main() -> Result<()> { schedule_id: tick_schedule_id, source_scene_ref, defer_completion, - agent_store: agent_store.clone(), resident_self_store: resident_self_store.clone(), }, )?; - if apply_rumination { - let agent_store = agent_store - .as_ref() - .ok_or_else(|| anyhow!("--apply-rumination requires --agent-store"))?; - apply_rumination_patch(&mut result, agent_store, &artifact_dir, &schedule_id)?; - } println!("{}", result); } "complete" => { @@ -350,15 +321,6 @@ fn main() -> Result<()> { let store_path = store_path.ok_or_else(|| anyhow!("pump requires --store"))?; let artifact_dir = artifact_dir.ok_or_else(|| anyhow!("pump requires --artifact-dir"))?; - if let Some(agent_store) = &agent_store { - let errors = validate_agent_memory_store(agent_store)?; - if !errors.is_empty() { - return Err(anyhow!( - "agent memory validation failed: {}", - errors.join("; ") - )); - } - } let result = pump_heartbeat_store( &store_path, &artifact_dir, @@ -374,7 +336,6 @@ fn main() -> Result<()> { target_role, schedule_id, source_scene_ref, - agent_store, resident_self_store, }, )?; @@ -464,35 +425,6 @@ fn main() -> Result<()> { ); } } - "routine" => { - assert_swarm_brake_allows_heartbeat( - &local_verse_store, - release_runtime_id.as_deref(), - "routine", - )?; - let store_path = store_path.ok_or_else(|| anyhow!("routine requires --store"))?; - let artifact_dir = - artifact_dir.ok_or_else(|| anyhow!("routine requires --artifact-dir"))?; - if let Some(agent_store) = &agent_store { - let errors = validate_agent_memory_store(agent_store)?; - if !errors.is_empty() { - return Err(anyhow!( - "agent memory validation failed: {}", - errors.join("; ") - )); - } - } - let result = run_void_routine_store( - &store_path, - &artifact_dir, - VoidRoutineOptions { - agent_store, - source, - allow_dream: !no_dream, - }, - )?; - println!("{}", result); - } "serve" => { if interval_seconds == 0 { return Err(anyhow!( @@ -535,15 +467,6 @@ fn main() -> Result<()> { &resident_runtime_store, release_runtime_id.as_deref(), )?; - if let Some(agent_store) = &agent_store { - let errors = validate_agent_memory_store(agent_store)?; - if !errors.is_empty() { - return Err(anyhow!( - "agent memory validation failed: {}", - errors.join("; ") - )); - } - } fs::create_dir_all(&artifact_dir).with_context(|| { format!( "failed to create heartbeat serve artifact directory {}", @@ -551,7 +474,6 @@ fn main() -> Result<()> { ) })?; let mut completed_iterations = highest_pulse_artifact_sequence(&artifact_dir)?; - let mut completed_routines = 0_u64; let mut refused_pulses = 0_u64; let shutdown_requested = install_shutdown_signal_owner()?; loop { @@ -600,7 +522,6 @@ fn main() -> Result<()> { brake.is_some(), &format!("{schedule_id}.serve-{iteration:06}"), &source_scene_ref, - agent_store.clone(), )?; if pulse.status != "idle" { completed_iterations = iteration; @@ -670,7 +591,6 @@ fn main() -> Result<()> { &iteration_dir, &format!("{schedule_id}.persona-{iteration:06}"), &source_scene_ref, - agent_store.clone(), false, )?; if persona_pulse["status"] != "idle" { @@ -696,17 +616,7 @@ fn main() -> Result<()> { wait_for_shutdown(&shutdown_requested, Duration::from_secs(interval_seconds)); continue; } - let result = run_void_routine_store( - &store_path, - &iteration_dir, - VoidRoutineOptions { - agent_store: agent_store.clone(), - source: source.clone(), - allow_dream: !no_dream, - }, - )?; completed_iterations = iteration; - completed_routines += 1; let retention = retain_heartbeat_pulse_artifacts( &store_path, &artifact_dir, @@ -718,13 +628,11 @@ fn main() -> Result<()> { "{}", serde_json::json!({ "schemaVersion": "epiphany.heartbeat.serve_pulse.v0", - "status": "completed", + "status": "idle", "owner": "heartbeat", "lifecycleOwner": "Idunn", "iteration": iteration, "artifactDir": iteration_dir, - "routineOk": result["ok"], - "routineArtifact": result["artifactPath"], "retention": retention.as_ref().map(retention_receipt_projection), "privateStateExposed": false }) @@ -745,7 +653,6 @@ fn main() -> Result<()> { "artifactDir": artifact_dir, "intervalSeconds": interval_seconds, "completedIterations": completed_iterations, - "completedRoutines": completed_routines, "refusedPulses": refused_pulses, "bounded": max_iterations > 0, "shutdownRequested": shutdown_requested.load(Ordering::SeqCst), @@ -753,15 +660,6 @@ fn main() -> Result<()> { }) ); } - "smoke" => { - let agent_store = agent_store.unwrap_or_else(|| PathBuf::from("state/agents.msgpack")); - let result = run_smoke(&agent_store)?; - let ok = result["ok"].as_bool().unwrap_or(false); - println!("{}", serde_json::to_string_pretty(&result)?); - if !ok { - std::process::exit(1); - } - } _ => return usage(), } @@ -863,9 +761,7 @@ fn usage() -> Result<()> { " epiphany-heartbeat-store retain-artifacts --store --artifact-dir [--retain-pulse-artifacts ] [--retention-batch-size ]\n", " epiphany-heartbeat-store queue-mention --store [options]\n", " epiphany-heartbeat-store status --store [--artifact-dir ]\n", - " epiphany-heartbeat-store routine --store --artifact-dir [options]\n", - " epiphany-heartbeat-store serve --store --artifact-dir [--local-verse-store ] [--resident-self-store --resident-runtime-store ] [--agent-store ] [--source ] [--no-dream] [--interval-seconds ] [--max-iterations ]\n", - " epiphany-heartbeat-store smoke [--agent-store ]" + " epiphany-heartbeat-store serve --store --artifact-dir [--local-verse-store ] [--resident-self-store --resident-runtime-store ] [--interval-seconds ] [--max-iterations ]" ))) } @@ -1034,538 +930,3 @@ fn parse_scene_participant(raw: &str) -> Result .collect(), }) } - -fn run_smoke(agent_store: &Path) -> Result { - let temp_dir = scoped_temp_dir("epiphany-heartbeat-smoke")?; - let temp_agent_store = temp_dir.join("agents.msgpack"); - let store_path = temp_dir.join("heartbeats.msgpack"); - let artifact_dir = temp_dir.join("artifacts"); - fs::copy(agent_store, &temp_agent_store).with_context(|| { - format!( - "failed to copy {} to {}", - agent_store.display(), - temp_agent_store.display() - ) - })?; - - let initial_errors = validate_agent_memory_store(&temp_agent_store)?; - if !initial_errors.is_empty() { - let _ = fs::remove_dir_all(&temp_dir); - return Ok(serde_json::json!({ - "ok": false, - "phase": "agent-memory-validate", - "validationErrors": initial_errors, - })); - } - - initialize_heartbeat_store(&store_path, 1.0)?; - let heat = update_heartbeat_heat_store( - &store_path, - HeartbeatHeatUpdateOptions { - scope: "role".to_string(), - selector: "implementation".to_string(), - multiplier: 4.0, - id: Some("smoke-implementation-heat".to_string()), - label: Some("Smoke implementation heat".to_string()), - reason: Some( - "Prove heartbeat initiative heat changes the live turn surface.".to_string(), - ), - expires_after_scene_clock: Some(20.0), - clear: false, - }, - )?; - let work = tick_heartbeat_store( - &store_path, - &artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - coordinator_action: Some("continueImplementation".to_string()), - target_role: None, - urgency: 0.95, - schedule_id: "smoke-work".to_string(), - source_scene_ref: "smoke/coordinator".to_string(), - defer_completion: true, - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - let blocked_repeat = tick_heartbeat_store( - &store_path, - &artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - coordinator_action: Some("continueImplementation".to_string()), - target_role: None, - urgency: 0.95, - schedule_id: "smoke-work".to_string(), - source_scene_ref: "smoke/coordinator".to_string(), - defer_completion: true, - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - ) - .err() - .map(|error| error.to_string()); - - let action_id = work["event"]["actionId"] - .as_str() - .ok_or_else(|| anyhow!("smoke work event has no actionId"))? - .to_string(); - let completed = complete_heartbeat_store( - &store_path, - &artifact_dir, - HeartbeatCompleteOptions { - role: "implementation".to_string(), - action_id: Some(action_id), - }, - )?; - let mut idle = tick_heartbeat_store( - &store_path, - &artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - coordinator_action: None, - target_role: None, - urgency: 0.0, - schedule_id: "smoke-idle".to_string(), - source_scene_ref: "smoke/idle".to_string(), - defer_completion: false, - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - - if let Some(patch) = idle["rumination"]["selfPatch"].as_object() { - let role_id = idle["rumination"]["roleId"] - .as_str() - .ok_or_else(|| anyhow!("idle rumination has no roleId"))? - .to_string(); - let patch = serde_json::from_value(serde_json::Value::Object(patch.clone())) - .context("idle rumination selfPatch is not a typed AgentSelfPatch")?; - let applied = apply_agent_self_patch_document(&role_id, patch, &temp_agent_store)?; - idle["rumination"]["result"] = serde_json::to_value(applied)?; - idle["rumination"]["applied"] = serde_json::Value::Bool(true); - write_rumination_artifact(&artifact_dir, "smoke-idle", &idle["rumination"])?; - } - - let validation_errors = validate_agent_memory_store(&temp_agent_store)?; - let status = heartbeat_status_projection(&store_path, &artifact_dir, 1.0, 8)?; - if status["status"] != "loaded" || status["schedulerStatus"] != "active" { - anyhow::bail!( - "heartbeat status must distinguish loaded state from active scheduler physiology" - ); - } - let routine = run_void_routine_store( - &store_path, - &artifact_dir, - VoidRoutineOptions { - agent_store: Some(temp_agent_store.clone()), - source: "smoke/void-routine".to_string(), - allow_dream: true, - }, - )?; - let routine_status = heartbeat_status_projection(&store_path, &artifact_dir, 1.0, 8)?; - let relaxed_store_path = temp_dir.join("relaxed-heartbeats.msgpack"); - initialize_heartbeat_store(&relaxed_store_path, 1.0)?; - let relaxed_pump = pump_heartbeat_store( - &relaxed_store_path, - &artifact_dir, - HeartbeatPumpOptions { - base_heartbeat_rate: 1.0, - min_heartbeat_rate: 0.05, - max_heartbeat_rate: 4.0, - min_concurrency: 1, - max_concurrency: 8, - max_ticks: 8, - external_urgency: 0.0, - coordinator_action: None, - target_role: None, - schedule_id: "smoke-relaxed-pump".to_string(), - source_scene_ref: "smoke/relaxed".to_string(), - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - let alarmed_store_path = temp_dir.join("alarmed-heartbeats.msgpack"); - initialize_heartbeat_store(&alarmed_store_path, 1.0)?; - let alarmed_pump = pump_heartbeat_store( - &alarmed_store_path, - &artifact_dir, - HeartbeatPumpOptions { - base_heartbeat_rate: 1.0, - min_heartbeat_rate: 0.05, - max_heartbeat_rate: 4.0, - min_concurrency: 1, - max_concurrency: 8, - max_ticks: 8, - external_urgency: 1.0, - coordinator_action: None, - target_role: None, - schedule_id: "smoke-alarmed-pump".to_string(), - source_scene_ref: "smoke/alarmed".to_string(), - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - let stale_store_path = temp_dir.join("stale-heartbeats.msgpack"); - initialize_heartbeat_store(&stale_store_path, 1.0)?; - let stale_work = tick_heartbeat_store( - &stale_store_path, - &artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - coordinator_action: Some("continueImplementation".to_string()), - target_role: None, - urgency: 0.95, - schedule_id: "smoke-stale-work".to_string(), - source_scene_ref: "smoke/stale".to_string(), - defer_completion: true, - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - let stale_repair = recover_stale_heartbeat_store( - &stale_store_path, - &artifact_dir, - HeartbeatStaleTurnRepairOptions { - max_age_seconds: 0, - now_utc: None, - reason: "Smoke-test stale heartbeat recovery receipt.".to_string(), - }, - )?; - let after_stale_repair = tick_heartbeat_store( - &stale_store_path, - &artifact_dir, - HeartbeatTickOptions { - target_heartbeat_rate: 1.0, - coordinator_action: Some("continueImplementation".to_string()), - target_role: None, - urgency: 0.95, - schedule_id: "smoke-after-stale-repair".to_string(), - source_scene_ref: "smoke/stale".to_string(), - defer_completion: true, - agent_store: Some(temp_agent_store.clone()), - resident_self_store: None, - }, - )?; - let initiative_errors = validate_schedule_shape(&work["schedule"]) - .into_iter() - .chain(validate_schedule_shape(&idle["schedule"])) - .collect::>(); - let ok = work["event"]["selectedRole"] == "implementation" - && work["event"]["turnStatus"] == "running" - && blocked_repeat - .as_deref() - .is_some_and(|message| message.contains("already has running heartbeat turn")) - && completed["event"]["turnStatus"] == "completed" - && idle["event"]["actionType"] == "ruminate_memory" - && idle["event"]["turnStatus"] == "completed" - && idle["event"]["nextReadyAt"].as_f64().unwrap_or_default() - > completed["event"]["nextReadyAt"] - .as_f64() - .unwrap_or_default() - && idle["rumination"]["result"]["status"] == "accepted" - && validation_errors.is_empty() - && initiative_errors.is_empty() - && heat["heat"]["multipliers"] - .as_array() - .is_some_and(|multipliers| { - multipliers.iter().any(|multiplier| { - multiplier["id"] == "smoke-implementation-heat" - && multiplier["scope"] == "role" - && multiplier["selector"] == "implementation" - && multiplier["multiplier"].as_f64() == Some(4.0) - }) - }) - && work["schedule"]["action_catalog"] - .as_array() - .and_then(|actions| actions.first()) - .and_then(|action| action["initiative_heat_multiplier"].as_f64()) - == Some(4.0) - && artifact_dir.join("smoke-work.initiative.json").exists() - && artifact_dir.join("smoke-work.completion.json").exists() - && artifact_dir.join("smoke-idle.rumination.json").exists() - && status["participants"].as_array().map(Vec::len) == Some(7) - && routine["routine"]["schema_version"] == "epiphany.void_routine.v0" - && routine["routine"]["memoryResonance"]["schema_version"] - == "epiphany.memory_resonance.v0" - && routine["routine"]["incubation"]["schema_version"] == "epiphany.incubation.v0" - && routine["routine"]["thoughtLanes"]["schema_version"] == "epiphany.cognition_lanes.v0" - && routine["routine"]["bridge"]["schema_version"] == "epiphany.cognition_bridge.v0" - && routine["routine"]["candidateInterventions"]["schema_version"] - == "epiphany.candidate_interventions.v0" - && routine["routine"]["appraisals"]["schema_version"] - == "epiphany.agent_thought_appraisals.v0" - && routine["routine"]["reactions"]["schema_version"] == "epiphany.agent_reactions.v0" - && routine["routine"]["incubation"]["sourceCoverage"]["schema_version"] - == "epiphany.source_coverage.v0" - && routine["routine"]["incubation"]["themes"] - .as_array() - .and_then(|themes| themes.first()) - .is_some_and(|theme| { - theme - .get("noveltyToSelf") - .and_then(serde_json::Value::as_f64) - .is_some() - && theme - .get("noveltyToRoom") - .and_then(serde_json::Value::as_f64) - .is_some() - && theme - .get("saturationScore") - .and_then(serde_json::Value::as_f64) - .is_some() - && theme - .get("status") - .and_then(serde_json::Value::as_str) - .is_some() - }) - && routine["routine"]["bridge"]["sourceCoverage"]["schema_version"] - == "epiphany.source_coverage.v0" - && routine["routine"]["bridge"]["refractoryTopics"].is_array() - && routine["routine"]["bridge"]["decision"]["reason"] - .as_str() - .is_some_and(|reason| !reason.is_empty()) - && routine_status["cognitionQuarantine"]["schema_version"] - == "epiphany.agent_heartbeat_cognition.v0" - && routine_status["cognitionQuarantine"]["latestRunId"].as_str() - == routine["routine"]["runId"].as_str() - && routine_status["participants"] - .as_array() - .and_then(|participants| { - let hands = participants - .iter() - .find(|item| item["roleId"] == "implementation")?; - let self_lane = participants - .iter() - .find(|item| item["roleId"] == "coordinator")?; - let hands_personality = hands["personalityCooldownMultiplier"] - .as_f64() - .unwrap_or(1.0); - let self_personality = self_lane["personalityCooldownMultiplier"] - .as_f64() - .unwrap_or(1.0); - let hands_effective = hands["effectiveCooldownMultiplier"].as_f64().unwrap_or(1.0); - let self_effective = self_lane["effectiveCooldownMultiplier"] - .as_f64() - .unwrap_or(1.0); - Some(hands_personality < self_personality && hands_effective < self_effective) - }) - == Some(true) - && routine_status["sleepCycle"]["schema_version"] == "epiphany.sleep_cycle.v0"; - let ok = ok - && relaxed_pump["pump"]["launched"].as_u64() == Some(0) - && relaxed_pump["pump"]["pacing"]["targetConcurrency"].as_u64() == Some(0) - && alarmed_pump["pump"]["launched"].as_u64() == Some(7) - && alarmed_pump["pump"]["pacing"]["targetConcurrency"].as_u64() == Some(7) - && stale_work["event"]["turnStatus"] == "running" - && stale_repair["repaired"].as_u64() == Some(1) - && stale_repair["receipts"] - .as_array() - .and_then(|receipts| receipts.first()) - .is_some_and(|receipt| receipt[10] == "repaired" && receipt[12] == false) - && after_stale_repair["event"]["selectedRole"] == stale_work["event"]["selectedRole"] - && after_stale_repair["event"]["turnStatus"] == "running"; - - let result = serde_json::json!({ - "ok": ok, - "workEvent": work["event"], - "heat": heat["heat"], - "blockedRepeat": blocked_repeat, - "completionEvent": completed["event"], - "idleEvent": idle["event"], - "idleRumination": idle["rumination"], - "voidRoutine": routine["routine"], - "personalityTiming": routine_status["participants"], - "relaxedPump": relaxed_pump["pump"], - "alarmedPump": alarmed_pump["pump"], - "staleRepair": stale_repair, - "afterStaleRepairEvent": after_stale_repair["event"], - "validationErrors": validation_errors, - "initiativeErrors": initiative_errors, - }); - let _ = fs::remove_dir_all(&temp_dir); - Ok(result) -} - -fn apply_rumination_patch( - result: &mut serde_json::Value, - agent_store: &Path, - artifact_dir: &Path, - schedule_id: &str, -) -> Result<()> { - if result["rumination"].is_null() { - return Ok(()); - } - let Some(patch) = result["rumination"]["selfPatch"].as_object() else { - return Ok(()); - }; - let role_id = result["rumination"]["roleId"] - .as_str() - .ok_or_else(|| anyhow!("rumination has no roleId"))? - .to_string(); - let patch = serde_json::from_value(serde_json::Value::Object(patch.clone())) - .context("rumination selfPatch is not a typed AgentSelfPatch")?; - let applied = apply_agent_self_patch_document(&role_id, patch, agent_store)?; - result["rumination"]["result"] = serde_json::to_value(applied)?; - result["rumination"]["applied"] = serde_json::Value::Bool(true); - write_rumination_artifact(artifact_dir, schedule_id, &result["rumination"])?; - Ok(()) -} - -fn write_rumination_artifact( - artifact_dir: &Path, - schedule_id: &str, - value: &serde_json::Value, -) -> Result<()> { - fs::create_dir_all(artifact_dir) - .with_context(|| format!("failed to create {}", artifact_dir.display()))?; - let path = artifact_dir.join(format!("{schedule_id}.rumination.json")); - fs::write(&path, format!("{}\n", serde_json::to_string_pretty(value)?)) - .with_context(|| format!("failed to write {}", path.display())) -} - -fn validate_schedule_shape(schedule: &serde_json::Value) -> Vec { - let mut errors = Vec::new(); - let required = [ - "schema_version", - "schedule_id", - "source_scene_ref", - "scene_clock", - "participants", - "action_catalog", - "reaction_windows", - "selection_policy", - "next_actor_selection", - "review_notes", - ]; - for key in required { - if schedule.get(key).is_none() { - errors.push(format!("initiative schedule missing {key}")); - } - } - if schedule["schema_version"] != "ghostlight.initiative_schedule.v0" { - errors.push("initiative schedule has wrong schema_version".to_string()); - } - if !schedule["scene_clock"].is_number() - || schedule["scene_clock"] - .as_f64() - .is_some_and(|clock| clock < 0.0) - { - errors.push("initiative schedule scene_clock must be non-negative number".to_string()); - } - for participant in schedule["participants"].as_array().into_iter().flatten() { - if ![ - "active", - "blocked", - "withdrawn", - "incapacitated", - "offscreen", - ] - .contains(&participant["status"].as_str().unwrap_or_default()) - { - errors.push(format!( - "participant {:?} has invalid status", - participant["agent_id"] - )); - } - for key in [ - "initiative_speed", - "next_ready_at", - "reaction_bias", - "interrupt_threshold", - "current_load", - ] { - if !participant[key].is_number() { - errors.push(format!( - "participant {:?} {key} must be numeric", - participant["agent_id"] - )); - } - } - } - for action in schedule["action_catalog"].as_array().into_iter().flatten() { - if ![ - "speak", - "silence", - "move", - "gesture", - "touch_object", - "block_object", - "use_object", - "show_object", - "withhold_object", - "transfer_object", - "spend_resource", - "attack", - "wait", - "mixed", - "role_work", - "ruminate_memory", - "scene_turn", - "persona_turn", - ] - .contains(&action["action_type"].as_str().unwrap_or_default()) - { - errors.push(format!( - "action {:?} has invalid action_type", - action["action_id"] - )); - } - if !["micro", "short", "standard", "major", "committed"] - .contains(&action["action_scale"].as_str().unwrap_or_default()) - { - errors.push(format!( - "action {:?} has invalid action_scale", - action["action_id"] - )); - } - } - let selection = &schedule["next_actor_selection"]; - if ![ - "scheduled_turn", - "reaction_interrupt", - "coordinator_override", - ] - .contains(&selection["selection_kind"].as_str().unwrap_or_default()) - { - errors.push("next_actor_selection has invalid selection_kind".to_string()); - } - for snapshot in selection["readiness_snapshot"] - .as_array() - .into_iter() - .flatten() - { - if let Some(object) = snapshot.as_object() { - let extra_keys = object - .keys() - .filter(|key| { - ![ - "agent_id", - "arena", - "participant_kind", - "next_ready_at", - "initiative_frozen", - "freeze_reason", - "reaction_readiness", - "eligible_for_reaction", - ] - .contains(&key.as_str()) - }) - .cloned() - .collect::>(); - if !extra_keys.is_empty() { - errors.push(format!( - "readiness snapshot has Ghostlight-incompatible keys: {extra_keys:?}" - )); - } - } - } - errors -} - -fn scoped_temp_dir(prefix: &str) -> Result { - let path = env::temp_dir().join(format!("{prefix}-{}", Uuid::new_v4())); - fs::create_dir(&path).with_context(|| format!("failed to claim {}", path.display()))?; - Ok(path) -} diff --git a/epiphany-core/src/heartbeat_state.rs b/epiphany-core/src/heartbeat_state.rs index 7ecee2bb..30b40dab 100644 --- a/epiphany-core/src/heartbeat_state.rs +++ b/epiphany-core/src/heartbeat_state.rs @@ -2,11 +2,9 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use chrono::DateTime; -use chrono::Duration; use chrono::Utc; use serde_json::Value; use sha2::Digest; -use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fs; use std::path::Path; @@ -22,8 +20,6 @@ use heartbeat_pacing::adaptive_swarm_pacing; use heartbeat_pacing::apply_initiative_heat_policy; use heartbeat_pacing::effective_cooldown_multiplier; use heartbeat_pacing::initiative_heat_multiplier; -use heartbeat_pacing::mood_cooldown_multiplier; -use heartbeat_pacing::personality_cooldown_multiplier; use heartbeat_pacing::running_turn_count; pub use heartbeat_projection::heartbeat_status_projection; use heartbeat_projection::history_event_json; @@ -35,19 +31,15 @@ use heartbeat_roles::ROLE_ORDER; use heartbeat_roles::agent_id_for_role; pub use heartbeat_roles::default_heartbeat_state; use heartbeat_roles::default_participant; -use heartbeat_roles::display_name_for_role; pub use heartbeat_roles::ghostlight_scene_heartbeat_state; pub use heartbeat_roles::initialize_ghostlight_scene_heartbeat_store; 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_cognition_entry; 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_cognition; pub use heartbeat_store::validate_heartbeat_state; -pub use heartbeat_store::write_heartbeat_cognition_entry; pub use heartbeat_store::write_heartbeat_stale_turn_repair_receipt; pub use heartbeat_store::write_heartbeat_state_entry; @@ -68,114 +60,6 @@ struct HeartbeatAction { local_affordance_basis: Vec, } -pub fn run_void_routine_store( - store_path: impl AsRef, - artifact_dir: impl AsRef, - options: VoidRoutineOptions, -) -> Result { - let store_path = store_path.as_ref(); - let mut state = - load_heartbeat_state_entry(store_path)?.unwrap_or_else(|| default_heartbeat_state(1.0)); - patch_missing_participants(&mut state); - let previous_cognition = load_heartbeat_cognition_entry(store_path)?; - - let memory_records = collect_role_memory_records(options.agent_store.as_deref())?; - let appraisal_profiles = collect_role_appraisal_profiles(options.agent_store.as_deref())?; - apply_personality_timing_profiles(&mut state, &appraisal_profiles); - let resonance = build_memory_resonance(&memory_records); - let incubation = build_incubation( - &previous_cognition - .as_ref() - .and_then(|entry| entry.incubation.clone()), - &previous_cognition - .as_ref() - .and_then(|entry| entry.bridge.clone()), - &previous_cognition - .as_ref() - .and_then(|entry| entry.candidate_interventions.clone()), - &resonance, - &memory_records, - ); - let thought_lanes = build_thought_lanes(&resonance, &incubation, &memory_records); - let bridge = build_thought_bridge( - &previous_cognition - .as_ref() - .and_then(|entry| entry.bridge.clone()), - &thought_lanes, - &resonance, - &incubation, - ); - let candidate_interventions = build_candidate_interventions(&bridge, &incubation); - let appraisals = - build_agent_appraisals(&appraisal_profiles, &thought_lanes, &incubation, &bridge); - let reactions = build_agent_reactions(&appraisals, &bridge); - apply_mood_timing_from_appraisals(&mut state, &appraisals); - let sleep_cycle = update_sleep_cycle( - previous_cognition - .as_ref() - .and_then(|entry| entry.sleep_cycle.as_ref()), - &incubation, - options.allow_dream, - ); - let run_id = format!("epiphany-void-routine-{}", now_stamp()); - let artifact_dir = artifact_dir.as_ref(); - fs::create_dir_all(artifact_dir) - .with_context(|| format!("failed to create {}", artifact_dir.display()))?; - let artifact_path = artifact_dir.join(format!("{run_id}.routine.json")); - let routine = serde_json::json!({ - "schema_version": VOID_ROUTINE_SCHEMA_VERSION, - "runId": run_id, - "source": options.source, - "referenceLineage": "VoidBot-style room stewardship, sleep, resonance, and dream maintenance rebuilt as Epiphany-native heartbeat physiology.", - "storeFile": store_path, - "agentStore": options.agent_store, - "updatedAt": now_iso(), - "sleepCycle": &sleep_cycle, - "memoryResonance": &resonance, - "incubation": &incubation, - "thoughtLanes": &thought_lanes, - "bridge": &bridge, - "candidateInterventions": &candidate_interventions, - "appraisals": &appraisals, - "reactions": &reactions, - "reviewNotes": [ - "Void is reference material, not a runtime dependency.", - "The routine mutates only typed heartbeat physiology fields; project truth and role memory mutation stay on their dedicated reviewed surfaces.", - "Analytic and associative lanes are cognition context, not hidden authority; the bridge decides draft, speech, silence, or further incubation.", - "Appraisal projects clustered thoughts through each agent's own personality vectors; reaction is derived from that appraisal and remains separate from state mutation.", - "Sleep is maintenance: slow rumination, memory compression, and dream residue, not absence." - ], - }); - - write_json_artifact(&artifact_path, &routine)?; - write_heartbeat_cognition_entry( - store_path, - &EpiphanyHeartbeatCognitionEntry { - schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(), - updated_at: now_iso(), - latest_run_id: Some(run_id), - latest_artifact_ref: Some(artifact_path.display().to_string()), - source: Some(options.source), - sleep_cycle: Some(sleep_cycle), - memory_resonance: Some(resonance), - incubation: Some(incubation), - thought_lanes: Some(thought_lanes), - bridge: Some(bridge), - candidate_interventions: Some(candidate_interventions), - appraisals: Some(appraisals), - reactions: Some(reactions), - }, - )?; - write_heartbeat_state_entry(store_path, &state)?; - - Ok(serde_json::json!({ - "ok": true, - "storeFile": store_path, - "artifactPath": artifact_path, - "routine": routine, - })) -} - pub fn tick_heartbeat_store( store_path: impl AsRef, artifact_dir: impl AsRef, @@ -194,15 +78,6 @@ pub fn tick_heartbeat_store( state.target_heartbeat_rate = options.target_heartbeat_rate; } patch_missing_participants(&mut state); - if let Some(agent_store) = options.agent_store.as_deref() { - let appraisal_profiles = collect_role_appraisal_profiles(Some(agent_store))?; - apply_personality_timing_profiles(&mut state, &appraisal_profiles); - } - if let Some(appraisals) = - load_heartbeat_cognition_entry(store_path)?.and_then(|entry| entry.appraisals) - { - apply_mood_timing_from_appraisals(&mut state, &appraisals); - } apply_initiative_heat_policy(&mut state); if let Some(resident_store) = options.resident_self_store.as_deref() && crate::pending_resident_self_pressure(resident_store)? @@ -296,7 +171,6 @@ pub fn tick_heartbeat_store( "stateFile": null, "event": result["event"].clone(), "schedule": result["schedule"].clone(), - "rumination": result["rumination"].clone(), })) } @@ -305,7 +179,6 @@ pub fn pulse_persona_heartbeat( artifact_dir: impl AsRef, schedule_id: &str, source_scene_ref: &str, - agent_store: Option, brake_engaged: bool, ) -> Result { let store_path = store_path.as_ref(); @@ -358,7 +231,6 @@ pub fn pulse_persona_heartbeat( schedule_id: schedule_id.to_string(), source_scene_ref: source_scene_ref.to_string(), defer_completion: true, - agent_store, resident_self_store: None, }, )?; @@ -480,7 +352,6 @@ pub fn pulse_resident_self_heartbeat( brake_engaged: bool, schedule_id: &str, source_scene_ref: &str, - agent_store: Option, ) -> Result { let heartbeat_store = heartbeat_store.as_ref(); let resident_store = resident_store.as_ref(); @@ -543,7 +414,6 @@ pub fn pulse_resident_self_heartbeat( schedule_id: schedule_id.into(), source_scene_ref: source_scene_ref.into(), defer_completion: true, - agent_store, resident_self_store: Some(resident_store.to_path_buf()), }, )?; @@ -595,7 +465,6 @@ pub fn pump_heartbeat_store( schedule_id: format!("{}.resident-self", options.schedule_id), source_scene_ref: options.source_scene_ref.clone(), defer_completion: true, - agent_store: options.agent_store.clone(), resident_self_store: Some(resident_store.to_path_buf()), }, )?; @@ -613,15 +482,6 @@ pub fn pump_heartbeat_store( state.target_heartbeat_rate = options.base_heartbeat_rate; } patch_missing_participants(&mut state); - if let Some(agent_store) = options.agent_store.as_deref() { - let appraisal_profiles = collect_role_appraisal_profiles(Some(agent_store))?; - apply_personality_timing_profiles(&mut state, &appraisal_profiles); - } - if let Some(appraisals) = - load_heartbeat_cognition_entry(store_path)?.and_then(|entry| entry.appraisals) - { - apply_mood_timing_from_appraisals(&mut state, &appraisals); - } apply_initiative_heat_policy(&mut state); let pacing = adaptive_swarm_pacing(&state, &options); @@ -663,7 +523,6 @@ pub fn pump_heartbeat_store( schedule_id: format!("{}.pump-{:03}", options.schedule_id, index + 1), source_scene_ref: options.source_scene_ref.clone(), defer_completion: true, - agent_store: options.agent_store.clone(), resident_self_store: options.resident_self_store.clone(), }; match tick_once(&mut state, &tick_options, false) { @@ -679,7 +538,6 @@ pub fn pump_heartbeat_store( tick_results.push(serde_json::json!({ "event": result["event"], "schedule": result["schedule"], - "rumination": result["rumination"], })); } Err(error) => { @@ -1188,8 +1046,6 @@ fn tick_once( completed_at: None, completed_scene_clock: None, next_ready_at: None, - personality_cooldown_multiplier: personality_cooldown_multiplier(&selected), - mood_cooldown_multiplier: mood_cooldown_multiplier(&selected), initiative_heat_multiplier: initiative_heat_multiplier(&selected), effective_cooldown_multiplier: effective_cooldown_multiplier(&selected), initiative_frozen: true, @@ -1259,8 +1115,6 @@ fn tick_once( "action_type": action.action_type, "action_scale": action.action_scale, "base_recovery": action.base_recovery, - "personality_cooldown_multiplier": personality_cooldown_multiplier(&selected_after), - "mood_cooldown_multiplier": mood_cooldown_multiplier(&selected_after), "initiative_heat_multiplier": initiative_heat_multiplier(&selected_after), "effective_cooldown_multiplier": effective_cooldown_multiplier(&selected_after), "initiative_cost": action.initiative_cost, @@ -1295,25 +1149,13 @@ fn tick_once( "pending_mentions": state.pending_mentions.clone(), "review_notes": [ "Epiphany heartbeat uses Ghostlight initiative timing as a harness scheduling receipt.", - "A selected idle lane may ruminate and request bounded self-memory mutation; it may not invent project work.", - "When no coordinator work is active, idle rumination is slowed by the sleep multiplier and shaped by personality and mood cooldowns so the swarm dreams instead of thrashing." + "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." ], }); - - let rumination = if event.action_type == "ruminate_memory" { - serde_json::json!({ - "roleId": selected_after.role_id, - "selfPatch": rumination_patch(&selected_after.role_id, &event.action_id), - "result": null, - "applied": false, - }) - } else { - Value::Null - }; Ok(serde_json::json!({ "event": history_event_json(event), "schedule": schedule, - "rumination": if rumination.is_null() { Value::Null } else { rumination }, })) } @@ -1741,10 +1583,10 @@ fn action_for_selection( .sleep_heartbeat_rate_multiplier .max(minimum_rate); let heartbeat_rate = (target_heartbeat_rate * sleep_multiplier).max(minimum_rate); - let action_id = format!("heartbeat.{}.ruminate", selected.role_id); + let action_id = format!("heartbeat.{}.idle", selected.role_id); HeartbeatAction { action_id, - action_type: "ruminate_memory", + action_type: "idle", action_scale: "short", base_recovery: state.pacing_policy.idle_base_recovery / heartbeat_rate, initiative_cost: 1.0, @@ -1752,11 +1594,11 @@ fn action_for_selection( commitment: 0.25, local_affordance_basis: vec![ format!( - "{} has no actionable lane work; ruminate on role quality and prepare candidate self-memory pressure.", + "{} 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, idle rumination is slowed by the sleep multiplier and shaped by personality and mood cooldowns so the swarm dreams instead of thrashing.".to_string(), + "When no coordinator work is active, cooldown and sleep pacing keep the swarm from thrashing.".to_string(), ], } } @@ -1945,2045 +1787,6 @@ fn write_json_artifact(path: impl AsRef, value: &Value) -> Result<()> { .with_context(|| format!("failed to write {}", path.display())) } -fn rumination_patch(role_id: &str, action_id: &str) -> Value { - let display_name = display_name_for_role(role_id); - serde_json::json!({ - "agentId": agent_id_for_role(role_id), - "reason": format!("{display_name} won an idle heartbeat slot and should preserve the habit of using idle wakeups to ruminate before sleep/review distills anything durable."), - "semanticMemories": [{ - "memoryId": format!("mem-{role_id}-heartbeat-rumination"), - "summary": "When a heartbeat wakes this lane and no coordinator-approved work is available, the correct move is to ruminate on role quality and surface candidate self-memory pressure rather than inventing project authority; durable distillation belongs in sleep or review.", - "salience": 0.78, - "confidence": 0.88, - }], - "goals": [{ - "goalId": format!("goal-{role_id}-heartbeat-rumination"), - "description": "Use idle heartbeat slots to ruminate on this lane's own work and prepare bounded memory pressure before touching project state.", - "scope": "life", - "priority": 0.82, - "emotionalStake": "An idle organ that invents work becomes noise in the bloodstream.", - "status": "active", - }], - "privateNotes": [format!("Last idle heartbeat action `{action_id}` chose rumination over fake urgency.")], - }) -} - -#[derive(Clone, Debug)] -struct RoleMemoryRecord { - role_id: String, - memory_id: String, - memory_kind: String, - summary: String, - salience: f64, - confidence: f64, - tokens: BTreeSet, -} - -#[derive(Clone, Debug)] -struct RoleAppraisalProfile { - role_id: String, - agent_id: String, - display_name: String, - traits: Vec, - reactivity: f64, - plasticity: f64, - expressiveness: f64, - guardedness: f64, - values: Vec, -} - -#[derive(Clone, Debug)] -struct PersonalityTraitProjection { - group: String, - name: String, - activation: f64, - plasticity: f64, - weight: f64, -} - -fn collect_role_memory_records(agent_store: Option<&Path>) -> Result> { - let Some(agent_store) = agent_store else { - return Ok(Vec::new()); - }; - let mut records = Vec::new(); - for role_id in ROLE_ORDER { - let Some(entry) = - crate::agent_memory::load_agent_memory_entry_for_role(agent_store, role_id)? - else { - continue; - }; - for memory in entry.agent.memories.semantic.iter() { - let tokens = summary_tokens(&memory.summary); - if tokens.is_empty() { - continue; - } - records.push(RoleMemoryRecord { - role_id: (*role_id).to_string(), - memory_id: memory.memory_id.clone(), - memory_kind: "semantic".to_string(), - summary: memory.summary.clone(), - salience: memory.salience, - confidence: memory.confidence, - tokens, - }); - } - for memory in &entry.agent.memories.episodic { - let tokens = summary_tokens(&memory.summary); - if tokens.is_empty() { - continue; - } - records.push(RoleMemoryRecord { - role_id: (*role_id).to_string(), - memory_id: memory.memory_id.clone(), - memory_kind: "episodic".to_string(), - summary: memory.summary.clone(), - salience: memory.salience, - confidence: memory.confidence, - tokens, - }); - } - for memory in &entry.agent.memories.relationship_summaries { - let tokens = summary_tokens(&memory.summary); - if tokens.is_empty() { - continue; - } - records.push(RoleMemoryRecord { - role_id: (*role_id).to_string(), - memory_id: memory.memory_id.clone(), - memory_kind: "relationship_summary".to_string(), - summary: memory.summary.clone(), - salience: memory.salience, - confidence: memory.confidence, - tokens, - }); - } - } - records.sort_by(|left, right| { - (right.salience * right.confidence) - .total_cmp(&(left.salience * left.confidence)) - .then_with(|| left.role_id.cmp(&right.role_id)) - .then_with(|| left.memory_id.cmp(&right.memory_id)) - }); - records.truncate(48); - Ok(records) -} - -fn collect_role_appraisal_profiles( - agent_store: Option<&Path>, -) -> Result> { - let Some(agent_store) = agent_store else { - return Ok(Vec::new()); - }; - let mut profiles = Vec::new(); - for role_id in ROLE_ORDER { - let Some(entry) = - crate::agent_memory::load_agent_memory_entry_for_role(agent_store, role_id)? - else { - continue; - }; - let mut traits = Vec::new(); - collect_trait_group( - &mut traits, - "underlying_organization", - &entry.agent.canonical_state.underlying_organization, - ); - collect_trait_group( - &mut traits, - "stable_dispositions", - &entry.agent.canonical_state.stable_dispositions, - ); - collect_trait_group( - &mut traits, - "behavioral_dimensions", - &entry.agent.canonical_state.behavioral_dimensions, - ); - collect_trait_group( - &mut traits, - "presentation_strategy", - &entry.agent.canonical_state.presentation_strategy, - ); - collect_trait_group( - &mut traits, - "voice_style", - &entry.agent.canonical_state.voice_style, - ); - collect_trait_group( - &mut traits, - "situational_state", - &entry.agent.canonical_state.situational_state, - ); - traits.sort_by(|left, right| right.weight.total_cmp(&left.weight)); - let top_weighted = traits.iter().take(8).collect::>(); - let reactivity = average(top_weighted.iter().map(|item| item.activation)).unwrap_or(0.5); - let plasticity = average(top_weighted.iter().map(|item| item.plasticity)).unwrap_or(0.5); - let expressiveness = average( - traits - .iter() - .filter(|item| item.group == "voice_style" || item.group == "presentation_strategy") - .map(|item| item.activation), - ) - .unwrap_or(reactivity); - let guardedness = average( - traits - .iter() - .filter(|item| { - let name = item.name.as_str(); - name.contains("guard") - || name.contains("shame") - || name.contains("risk") - || name.contains("caution") - || name.contains("contingent") - || name.contains("defens") - }) - .map(|item| item.activation), - ) - .unwrap_or((1.0 - expressiveness * 0.45).clamp(0.0, 1.0)); - let values = entry - .agent - .canonical_state - .values - .iter() - .take(5) - .map(|value| value.label.clone()) - .collect(); - profiles.push(RoleAppraisalProfile { - role_id: (*role_id).to_string(), - agent_id: entry.agent.agent_id, - display_name: entry.agent.identity.name, - traits, - reactivity: round3(reactivity), - plasticity: round3(plasticity), - expressiveness: round3(expressiveness), - guardedness: round3(guardedness), - values, - }); - } - Ok(profiles) -} - -fn collect_trait_group( - traits: &mut Vec, - group: &str, - source: &BTreeMap, -) { - for (name, vector) in source { - let activation = vector.current_activation.clamp(0.0, 1.0); - let plasticity = vector.plasticity.clamp(0.0, 1.0); - traits.push(PersonalityTraitProjection { - group: group.to_string(), - name: name.clone(), - activation, - plasticity, - weight: round3(activation * (0.65 + plasticity * 0.35)), - }); - } -} - -fn apply_personality_timing_profiles( - state: &mut EpiphanyHeartbeatStateEntry, - profiles: &[RoleAppraisalProfile], -) { - for participant in &mut state.participants { - let Some(profile) = profiles - .iter() - .find(|profile| profile.role_id == participant.role_id) - else { - continue; - }; - let timing = personality_timing_for_profile(profile); - participant.personality_cooldown_multiplier = timing.cooldown_multiplier; - participant.personality_timing = Some(HeartbeatPersonalityTiming { - schema_version: "epiphany.personality_timing.v0".to_string(), - source: "state/agents.msgpack".to_string(), - cooldown_multiplier: timing.cooldown_multiplier, - work_drive: timing.work_drive, - handsiness: timing.handsiness, - caution: timing.caution, - rumination_bias: timing.rumination_bias, - basis: timing.basis, - contract: "Cooldown is personality-shaped. Lower multipliers recover faster; higher multipliers yield the floor to other lanes.".to_string(), - }); - } -} - -#[derive(Clone, Debug)] -struct PersonalityTiming { - cooldown_multiplier: f64, - work_drive: f64, - handsiness: f64, - caution: f64, - rumination_bias: f64, - basis: Vec, -} - -fn personality_timing_for_profile(profile: &RoleAppraisalProfile) -> PersonalityTiming { - let work_drive = trait_match_score( - &profile.traits, - &[ - "objective", - "diff", - "implementation", - "hands", - "bloodhound", - "craft", - "act", - "source_touch", - "work", - "task", - ], - ); - let handsiness = trait_match_score( - &profile.traits, - &[ - "hands", - "implementation", - "diff_truth", - "objective_pursuit", - "bloodhound_pressure", - "source_touch_precision", - "small_reviewable_cut", - "craft", - ], - ); - let caution = trait_match_score( - &profile.traits, - &[ - "review", - "gate", - "caution", - "guard", - "risk", - "verification", - "truth", - "falsification", - "routing", - "discipline", - ], - ); - let rumination_bias = trait_match_score( - &profile.traits, - &[ - "dream", - "memory", - "reflection", - "imagination", - "future", - "translation", - "watch", - "continuity", - "self", - ], - ); - let cooldown = (1.05 + profile.guardedness * 0.28 + caution * 0.24 + rumination_bias * 0.08 - - profile.reactivity * 0.16 - - profile.plasticity * 0.10 - - work_drive * 0.18 - - handsiness * 0.34) - .clamp(0.55, 1.55); - let mut basis = Vec::new(); - for item in profile.traits.iter().take(6) { - basis.push(format!( - "{}.{} activation {:.2} plasticity {:.2}", - item.group, item.name, item.activation, item.plasticity - )); - } - PersonalityTiming { - cooldown_multiplier: round3(cooldown), - work_drive: round3(work_drive), - handsiness: round3(handsiness), - caution: round3(caution), - rumination_bias: round3(rumination_bias), - basis, - } -} - -fn trait_match_score(traits: &[PersonalityTraitProjection], needles: &[&str]) -> f64 { - let mut score = 0.0_f64; - let mut weight = 0.0_f64; - for item in traits { - let haystack = format!("{} {}", item.group, item.name); - let matched = needles - .iter() - .any(|needle| haystack.contains(&needle.replace('_', " "))); - let matched = matched || needles.iter().any(|needle| haystack.contains(*needle)); - if matched { - let item_weight = 0.7 + item.plasticity * 0.3; - score += item.activation * item_weight; - weight += item_weight; - } - } - if weight <= f64::EPSILON { - 0.0 - } else { - (score / weight).clamp(0.0, 1.0) - } -} - -fn apply_mood_timing_from_appraisals( - state: &mut EpiphanyHeartbeatStateEntry, - appraisals: &HeartbeatAgentThoughtAppraisals, -) { - for participant in &mut state.participants { - let Some(appraisal) = appraisals - .participant_appraisals - .iter() - .find(|item| item.role_id == participant.role_id) - else { - continue; - }; - let urgency = appraisal.emotional_appraisal.urgency.clamp(0.0, 1.0); - let arousal = appraisal.emotional_appraisal.arousal.clamp(0.0, 1.0); - let thought_pressure = appraisal - .emotional_appraisal - .thought_pressure - .clamp(0.0, 1.0); - let guardedness = appraisal.emotional_appraisal.guardedness.clamp(0.0, 1.0); - let reaction_intensity = appraisal - .candidate_implications - .reaction_intensity - .clamp(0.0, 1.0); - let anxiety = (urgency * 0.32 - + arousal * 0.22 - + thought_pressure * 0.24 - + guardedness * 0.12 - + reaction_intensity * 0.10) - .clamp(0.0, 1.0); - let multiplier = - (1.10 - urgency * 0.24 - anxiety * 0.38 - reaction_intensity * 0.12).clamp(0.55, 1.25); - let emotional_dimensions = - heartbeat_emotional_dimensions(appraisal, anxiety, reaction_intensity); - participant.mood_cooldown_multiplier = round3(multiplier); - participant.mood_timing = Some(HeartbeatMoodTiming { - schema_version: "epiphany.mood_timing.v0".to_string(), - source: Some(appraisal.appraisal_id.clone()), - cooldown_multiplier: round3(multiplier), - emotional_dimensions, - anxiety: round3(anxiety), - urgency: round3(urgency), - arousal: round3(arousal), - thought_pressure: round3(thought_pressure), - guardedness: round3(guardedness), - reaction_intensity: round3(reaction_intensity), - contract: "Mood bends personality timing. Anxiety and urgency lower cooldown so the lane that needs the floor most gets it sooner.".to_string(), - }); - } -} - -fn heartbeat_emotional_dimensions( - appraisal: &HeartbeatAgentThoughtAppraisal, - anxiety: f64, - reaction_intensity: f64, -) -> Vec { - let emotional = &appraisal.emotional_appraisal; - let dimensions = [ - ("valence", emotional.valence), - ("arousal", emotional.arousal), - ( - "dominance", - projected_emotion(appraisal, &["dominance", "command_force"]), - ), - ("urgency", emotional.urgency), - ( - "anger", - projected_emotion(appraisal, &["anger", "rage", "fury"]), - ), - ( - "despair", - projected_emotion(appraisal, &["despair", "hopelessness"]), - ), - ( - "sadness", - projected_emotion(appraisal, &["sadness", "grief", "sorrow"]), - ), - ( - "fear", - projected_emotion(appraisal, &["fear", "terror"]).max(anxiety), - ), - ("anxiety", anxiety), - ( - "disgust", - projected_emotion(appraisal, &["disgust", "revulsion"]), - ), - ("contempt", projected_emotion(appraisal, &["contempt"])), - ( - "annoyance", - projected_emotion(appraisal, &["annoyance", "irritation"]), - ), - ( - "dismissal", - projected_emotion(appraisal, &["dismissal", "dismissiveness"]), - ), - ( - "flippancy", - projected_emotion(appraisal, &["flippancy", "playfulness"]), - ), - ( - "playfulness", - projected_emotion(appraisal, &["playfulness", "play"]), - ), - ("irony", projected_emotion(appraisal, &["irony", "dryness"])), - ("tenderness", projected_emotion(appraisal, &["tenderness"])), - ("warmth", projected_emotion(appraisal, &["warmth"])), - ("joy", projected_emotion(appraisal, &["joy", "delight"])), - ( - "excitement", - projected_emotion(appraisal, &["excitement"]) - .max(emotional.arousal * emotional.curiosity), - ), - ( - "fatigue", - projected_emotion(appraisal, &["fatigue", "exhaustion"]), - ), - ("guardedness", emotional.guardedness), - ("confidence", projected_emotion(appraisal, &["confidence"])), - ("shame", projected_emotion(appraisal, &["shame"])), - ("pride", projected_emotion(appraisal, &["pride"])), - ( - "threat", - projected_emotion(appraisal, &["threat", "menace"]), - ), - ( - "secrecy", - projected_emotion(appraisal, &["secrecy", "low_projection"]), - ), - ("hesitation", projected_emotion(appraisal, &["hesitation"])), - ( - "emotionalContainment", - projected_emotion(appraisal, &["emotional_containment", "containment"]), - ), - ("thoughtPressure", emotional.thought_pressure), - ("reactionIntensity", reaction_intensity), - ( - "commandForce", - projected_emotion(appraisal, &["command_force"]), - ), - ]; - dimensions - .into_iter() - .map(|(name, value)| HeartbeatMoodDimension { - name: name.to_string(), - value: round3(value.clamp(0.0, 1.0)), - source_path: format!("heartbeat.appraisal.{}.{}", appraisal.appraisal_id, name), - }) - .collect() -} - -fn projected_emotion(appraisal: &HeartbeatAgentThoughtAppraisal, needles: &[&str]) -> f64 { - appraisal - .personality_projection - .iter() - .filter(|projection| { - let name = projection.name.to_ascii_lowercase(); - needles.iter().any(|needle| { - let spaced = needle.replace('_', " "); - name.contains(*needle) || name.contains(&spaced) - }) - }) - .map(|projection| projection.projection) - .fold(0.0_f64, f64::max) -} - -fn build_memory_resonance(records: &[RoleMemoryRecord]) -> HeartbeatMemoryResonance { - let mut pairs = Vec::new(); - for (left_index, left) in records.iter().enumerate() { - for right in records.iter().skip(left_index + 1) { - if left.role_id == right.role_id { - continue; - } - let overlap = token_overlap(&left.tokens, &right.tokens); - if overlap <= 0.0 { - continue; - } - let strength = round3( - overlap * ((left.salience * left.confidence) + (right.salience * right.confidence)) - / 2.0, - ); - if strength < 0.08 { - continue; - } - pairs.push(HeartbeatMemoryResonancePair { - left_role: left.role_id.clone(), - left_memory_id: left.memory_id.clone(), - left_memory_kind: left.memory_kind.clone(), - left_summary: left.summary.clone(), - right_role: right.role_id.clone(), - right_memory_id: right.memory_id.clone(), - right_memory_kind: right.memory_kind.clone(), - right_summary: right.summary.clone(), - strength, - shared_tokens: shared_tokens(&left.tokens, &right.tokens), - source_roles: vec![left.role_id.clone(), right.role_id.clone()], - source_kinds: vec![left.memory_kind.clone(), right.memory_kind.clone()], - evidence_refs: vec![left.memory_id.clone(), right.memory_id.clone()], - }); - } - } - pairs.sort_by(|left, right| right.strength.total_cmp(&left.strength)); - pairs.truncate(8); - HeartbeatMemoryResonance { - schema_version: "epiphany.memory_resonance.v0".to_string(), - updated_at: now_iso(), - source: "epiphany-native-void-routine".to_string(), - record_count: records.len(), - pairs, - } -} - -fn build_incubation( - previous: &Option, - bridge: &Option, - candidate_interventions: &Option, - resonance: &HeartbeatMemoryResonance, - records: &[RoleMemoryRecord], -) -> HeartbeatIncubation { - let previous_themes = previous - .as_ref() - .map(|value| value.themes.clone()) - .unwrap_or_default(); - let source_coverage = build_source_coverage(records); - let mut themes = Vec::new(); - for pair in resonance.pairs.iter().take(6) { - let left_role = pair.left_role.as_str(); - let right_role = pair.right_role.as_str(); - let tokens = pair - .shared_tokens - .iter() - .take(3) - .cloned() - .collect::>() - .join("/"); - let source_roles = pair.source_roles.clone(); - let source_kinds = pair.source_kinds.clone(); - let source_memory_ids = pair.evidence_refs.clone(); - let summary = format!( - "{} and {} keep touching {}; let the swarm decide whether that is signal, stale echo, or a branch worth following.", - display_name_for_role(left_role), - display_name_for_role(right_role), - if tokens.is_empty() { - "an unnamed seam" - } else { - &tokens - } - ); - let theme_id = format!( - "theme-{left_role}-{right_role}-{}", - stable_theme_suffix(&format!( - "{tokens}-{}-{}", - source_kinds.join("-"), - source_memory_ids.join("-") - )) - ); - let previous_theme = - best_matching_theme(&previous_themes, &theme_id, &summary, &source_memory_ids); - let support_count = previous_support_count(previous_theme) + 1; - let novelty_to_self = novelty_to_self( - &theme_id, - &summary, - &source_memory_ids, - &previous_themes, - bridge, - ); - let novelty_to_room = novelty_to_room(&theme_id, &summary, candidate_interventions, bridge); - let saturation = saturation_metrics( - &theme_id, - &summary, - &source_memory_ids, - &previous_themes, - bridge, - support_count, - ); - let refractory_penalty = refractory_penalty(&theme_id, &summary, bridge); - let evidence_diversity = - evidence_diversity(&source_roles, &source_kinds, &source_memory_ids); - let exploration_bonus = exploration_bonus(&source_roles, &source_kinds, &source_coverage); - let quiet_signal_ratio = 0.0; - let prior_maturation = previous_theme.map(|theme| theme.maturation).unwrap_or(0.32); - let strength = pair.strength; - let maturation = round3( - (prior_maturation * 0.44 - + strength * 0.24 - + evidence_diversity * 0.14 - + exploration_bonus * 0.12 - + novelty_to_self * 0.06 - + novelty_to_room * 0.06 - + (support_count as f64 / 10.0).min(1.0) * 0.08 - - saturation.score * 0.18 - - refractory_penalty * 0.14 - - quiet_signal_ratio * 0.12) - .clamp(0.0, 1.0), - ); - let novelty = round3( - (novelty_to_self * 0.55 + novelty_to_room * 0.45 - quiet_signal_ratio * 0.1) - .clamp(0.0, 1.0), - ); - let desire_to_speak = round3( - (strength * 0.18 - + maturation * 0.18 - + novelty_to_room * 0.18 - + novelty_to_self * 0.10 - + evidence_diversity * 0.12 - + exploration_bonus * 0.10 - - saturation.score * 0.24 - - refractory_penalty * 0.16) - .clamp(0.0, 1.0), - ); - let status = theme_status( - novelty_to_self, - novelty_to_room, - saturation.score, - refractory_penalty, - support_count, - evidence_diversity, - desire_to_speak, - maturation, - ); - let priority_score = round3( - (desire_to_speak * 0.34 - + novelty_to_self * 0.20 - + novelty_to_room * 0.18 - + evidence_diversity * 0.12 - + exploration_bonus * 0.16 - - saturation.score * 0.18 - - refractory_penalty * 0.14) - .clamp(0.0, 1.0), - ); - themes.push(HeartbeatIncubationTheme { - theme_id: previous_theme - .map(|theme| theme.theme_id.clone()) - .unwrap_or(theme_id), - summary, - strength: round3(strength), - source: "memory_resonance".to_string(), - source_roles: source_roles.clone(), - source_kinds: source_kinds.clone(), - source_memory_ids, - support_count, - evidence_diversity: round3(evidence_diversity), - exploration_bonus: round3(exploration_bonus), - novelty, - novelty_to_self: round3(novelty_to_self), - novelty_to_room: round3(novelty_to_room), - maturation, - desire_to_speak, - saturation_score: round3(saturation.score), - recent_match_count: saturation.recent_match_count, - refractory_penalty: round3(refractory_penalty), - priority_score, - status: status.to_string(), - latent_question: build_incubation_question(&source_roles, &source_kinds).to_string(), - why_it_pulls: build_incubation_attraction( - &source_roles, - &source_kinds, - novelty_to_self, - evidence_diversity, - ) - .to_string(), - holding_close_because: build_incubation_holding_line( - status, - saturation.score, - novelty_to_self, - exploration_bonus, - ) - .to_string(), - updated_at: now_iso(), - }); - } - if themes.is_empty() && !records.is_empty() { - let strongest = &records[0]; - themes.push(HeartbeatIncubationTheme { - theme_id: format!("theme-{}-strongest-memory", strongest.role_id), - summary: format!( - "{} carries the hottest current memory: {}", - display_name_for_role(&strongest.role_id), - strongest.summary - ), - strength: round3(strongest.salience * strongest.confidence), - source: "strongest_memory".to_string(), - source_roles: vec![strongest.role_id.clone()], - source_kinds: vec![strongest.memory_kind.clone()], - source_memory_ids: vec![strongest.memory_id.clone()], - support_count: 1, - evidence_diversity: 0.28, - exploration_bonus: 0.18, - novelty: 0.62, - novelty_to_self: 0.62, - novelty_to_room: 0.58, - maturation: round3((strongest.salience * strongest.confidence).clamp(0.18, 0.72)), - desire_to_speak: round3( - (strongest.salience * strongest.confidence * 0.6).clamp(0.12, 0.55), - ), - saturation_score: 0.0, - recent_match_count: 0, - refractory_penalty: 0.0, - priority_score: round3( - (strongest.salience * strongest.confidence * 0.7).clamp(0.14, 0.7), - ), - status: "incubating".to_string(), - latent_question: - "Does this hot memory deserve real follow-up, or is it just the loudest ember in the tray?" - .to_string(), - why_it_pulls: - "One strong memory is enough to seed a thought, but not enough to rule the room by default." - .to_string(), - holding_close_because: - "This is a seed, not a verdict. Give it one more pass before it starts issuing prophecies." - .to_string(), - updated_at: now_iso(), - }); - } - themes.sort_by(|left, right| { - right - .priority_score - .total_cmp(&left.priority_score) - .then_with(|| right.strength.total_cmp(&left.strength)) - }); - themes.truncate(12); - let last_incubation_summary = themes - .first() - .map(|theme| { - format!( - "Strongest incubating seam: {} ({}, self={:.2}, room={:.2}, speak={:.2}).", - theme.theme_id, - theme.status, - theme.novelty_to_self, - theme.novelty_to_room, - theme.desire_to_speak, - ) - }) - .unwrap_or_else(|| { - "No incubating thought currently has enough connective tissue to justify special treatment." - .to_string() - }); - HeartbeatIncubation { - schema_version: "epiphany.incubation.v0".to_string(), - updated_at: now_iso(), - source_coverage, - last_incubation_summary, - themes, - } -} - -fn build_thought_lanes( - resonance: &HeartbeatMemoryResonance, - incubation: &HeartbeatIncubation, - records: &[RoleMemoryRecord], -) -> HeartbeatThoughtLanes { - let analytic_threads = resonance - .pairs - .iter() - .take(4) - .enumerate() - .map(|(index, pair)| { - let left_role = pair.left_role.as_str(); - let right_role = pair.right_role.as_str(); - let strength = pair.strength; - HeartbeatAnalyticThread { - thread_id: format!("analytic-{left_role}-{right_role}-{index}"), - topic: format!("{left_role}/{right_role} evidence seam"), - claim: format!( - "{} and {} share a recurring memory edge; inspect whether this changes lane routing or evidence expectations.", - display_name_for_role(left_role), - display_name_for_role(right_role) - ), - evidence_refs: vec![pair.left_memory_id.clone(), pair.right_memory_id.clone()], - salience: round3(strength), - confidence: round3((0.55 + strength).min(0.95)), - desire_to_act: round3((strength * 1.4).min(0.85)), - counterweight: - "Shared vocabulary is not proof of shared truth; verify against artifacts before changing project state." - .to_string(), - last_touched_at: now_iso(), - } - }) - .collect::>(); - - let associative_threads = incubation - .themes - .iter() - .take(4) - .enumerate() - .map(|(index, theme)| { - let topic = theme.theme_id.as_str(); - HeartbeatAssociativeThread { - thread_id: format!("associative-{topic}-{index}"), - topic: topic.to_string(), - claim: theme.summary.clone(), - source_theme_id: topic.to_string(), - novelty: theme.novelty, - room_relevance: theme.novelty_to_room, - desire_to_speak: theme.desire_to_speak, - status: theme.status.clone(), - counterweight: - "A theme that keeps returning may be signal, obsession, or stale echo; the bridge must decide." - .to_string(), - last_touched_at: now_iso(), - } - }) - .collect::>(); - - let seed_threads = if analytic_threads.is_empty() && associative_threads.is_empty() { - records - .first() - .map(|record| { - vec![HeartbeatAnalyticThread { - thread_id: format!("analytic-{}-seed", record.role_id), - topic: format!("{} strongest memory", record.role_id), - claim: record.summary.clone(), - evidence_refs: vec![record.memory_id.clone()], - salience: round3(record.salience), - confidence: round3(record.confidence), - desire_to_act: 0.2, - counterweight: - "One hot memory is only a seed; do not let it annex the whole mind." - .to_string(), - last_touched_at: now_iso(), - }] - }) - .unwrap_or_default() - } else { - Vec::new() - }; - - HeartbeatThoughtLanes { - schema_version: "epiphany.cognition_lanes.v0".to_string(), - updated_at: now_iso(), - analytic: HeartbeatAnalyticLane { - description: - "Literal, evidence-facing lane: what is happening, what constraints matter, what action is justified." - .to_string(), - active_threads: if analytic_threads.is_empty() { - seed_threads - } else { - analytic_threads - }, - }, - associative: HeartbeatAssociativeLane { - description: - "Pattern-facing lane: what this rhymes with, what seam is ripening, what surprising branch may be worth a later retrieval hop." - .to_string(), - active_threads: associative_threads, - }, - } -} - -fn build_thought_bridge( - previous: &Option, - thought_lanes: &HeartbeatThoughtLanes, - resonance: &HeartbeatMemoryResonance, - incubation: &HeartbeatIncubation, -) -> HeartbeatCognitionBridge { - let analytic_count = thought_lanes.analytic.active_threads.len(); - let associative_count = thought_lanes.associative.active_threads.len(); - let resonance_count = resonance.pairs.len(); - let strongest_theme = incubation.themes.first().cloned(); - let strongest_status = strongest_theme - .as_ref() - .map(|theme| theme.status.as_str()) - .unwrap_or("incubating"); - let strongest_novelty_to_self = strongest_theme - .as_ref() - .map(|theme| theme.novelty_to_self) - .unwrap_or(0.0); - let strongest_novelty_to_room = strongest_theme - .as_ref() - .map(|theme| theme.novelty_to_room) - .unwrap_or(0.0); - let strongest_saturation = strongest_theme - .as_ref() - .map(|theme| theme.saturation_score) - .unwrap_or(0.0); - let strongest_refractory = strongest_theme - .as_ref() - .map(|theme| theme.refractory_penalty) - .unwrap_or(0.0); - let lane_balance = match analytic_count.cmp(&associative_count) { - std::cmp::Ordering::Greater => "analytic-heavy", - std::cmp::Ordering::Less => "associative-heavy", - std::cmp::Ordering::Equal => "balanced", - }; - let speak_decision = if strongest_theme.is_none() && resonance_count == 0 { - "silence" - } else if strongest_status == "ripe" - && strongest_novelty_to_room >= 0.58 - && strongest_saturation < 0.56 - && strongest_refractory < 0.18 - { - "draft" - } else if matches!(strongest_status, "stalled" | "refractory") { - "silence" - } else if resonance_count > 0 { - "hold" - } else { - "silence" - }; - let mut syntheses = previous - .as_ref() - .map(|value| value.recent_syntheses.clone()) - .unwrap_or_default(); - syntheses.push(HeartbeatBridgeSynthesis { - timestamp: now_iso(), - summary: if let Some(theme) = &strongest_theme { - bridge_summary(theme, speak_decision) - } else { - "No strong convergence yet; hold the lanes open without forcing speech.".to_string() - }, - dominant_topics: strongest_theme - .as_ref() - .map(|theme| vec![theme.theme_id.clone()]) - .unwrap_or_default(), - lane_balance: lane_balance.to_string(), - speak_decision: speak_decision.to_string(), - theme_status: strongest_status.to_string(), - novelty_to_self: round3(strongest_novelty_to_self), - novelty_to_room: round3(strongest_novelty_to_room), - saturation_score: round3(strongest_saturation), - saturation_note: synthesis_saturation_note( - strongest_status, - strongest_saturation, - strongest_novelty_to_self, - ) - .unwrap_or("No saturation warning.") - .to_string(), - }); - syntheses.reverse(); - syntheses.truncate(8); - syntheses.reverse(); - let source_coverage = incubation.source_coverage.clone(); - let topic_saturation = - topic_saturation_from_syntheses_and_themes(&syntheses, &incubation.themes); - let refractory_topics = refractory_topics_from_themes(previous, &incubation.themes); - - HeartbeatCognitionBridge { - schema_version: "epiphany.cognition_bridge.v0".to_string(), - updated_at: now_iso(), - recent_syntheses: syntheses, - source_coverage, - topic_saturation, - refractory_topics, - unresolved_tensions: vec![HeartbeatBridgeTension { - topic: "thought authority boundary".to_string(), - summary: - "Cognition lanes may shape attention and drafts, but only reviewed Epiphany state surfaces change project truth." - .to_string(), - opened_at: now_iso(), - }], - decision: HeartbeatBridgeDecision { - lane_balance: lane_balance.to_string(), - speak_decision: speak_decision.to_string(), - reason: bridge_decision_reason( - strongest_status, - strongest_novelty_to_self, - strongest_novelty_to_room, - strongest_saturation, - strongest_refractory, - speak_decision, - ) - .to_string(), - }, - } -} - -fn build_candidate_interventions( - bridge: &HeartbeatCognitionBridge, - incubation: &HeartbeatIncubation, -) -> HeartbeatCandidateInterventions { - let decision = bridge.decision.speak_decision.as_str(); - let strongest_theme = incubation.themes.first(); - let strongest_status = strongest_theme - .map(|theme| theme.status.as_str()) - .unwrap_or("incubating"); - let items = if decision == "draft" && strongest_status == "ripe" { - strongest_theme - .map(|theme| { - let theme_id = theme.theme_id.as_str(); - vec![HeartbeatCandidateIntervention { - intervention_id: format!("candidate-{theme_id}"), - summary: "Possible Aquarium-facing thought-weather note".to_string(), - draft: format!("I keep seeing {} rhyme across the swarm; this one finally has enough blood to inspect in the open.", theme_id), - decision: decision.to_string(), - requires_persona: true, - requires_review: true, - novelty_to_room: theme.novelty_to_room, - saturation_score: theme.saturation_score, - created_at: now_iso(), - }] - }) - .unwrap_or_default() - } else { - Vec::new() - }; - HeartbeatCandidateInterventions { - schema_version: "epiphany.candidate_interventions.v0".to_string(), - updated_at: now_iso(), - items, - } -} - -fn build_agent_appraisals( - profiles: &[RoleAppraisalProfile], - thought_lanes: &HeartbeatThoughtLanes, - incubation: &HeartbeatIncubation, - bridge: &HeartbeatCognitionBridge, -) -> HeartbeatAgentThoughtAppraisals { - let thought_tokens = cognition_tokens(thought_lanes, incubation, bridge); - let thought_pressure = thought_pressure(thought_lanes, incubation); - let bridge_decision = bridge.decision.speak_decision.as_str(); - let focus = incubation - .themes - .first() - .map(|theme| theme.theme_id.as_str()) - .unwrap_or("no-active-theme"); - let appraisals = profiles - .iter() - .map(|profile| { - let projection = personality_projection(profile, &thought_tokens); - let alignment = projection - .iter() - .map(|item| item.projection) - .next() - .unwrap_or(profile.reactivity); - let arousal = round3( - (thought_pressure * (0.35 + profile.reactivity * 0.45 + profile.plasticity * 0.2)) - .clamp(0.0, 1.0), - ); - let guardedness = round3( - (profile.guardedness * 0.65 + thought_pressure * 0.25 + (1.0 - alignment) * 0.1) - .clamp(0.0, 1.0), - ); - let curiosity = round3( - ((1.0 - profile.guardedness) * 0.25 + alignment * 0.45 + profile.plasticity * 0.3) - .clamp(0.0, 1.0), - ); - let urgency = round3((arousal * 0.65 + guardedness * 0.25 + thought_pressure * 0.1).clamp(0.0, 1.0)); - let valence = round3((0.55 + curiosity * 0.2 - guardedness * 0.18).clamp(0.0, 1.0)); - let label = interpretation_label(bridge_decision, arousal, guardedness, curiosity); - HeartbeatAgentThoughtAppraisal { - schema_version: "epiphany.agent_thought_appraisal.v0".to_string(), - appraisal_id: format!("appraisal-{}-{}", profile.role_id, stable_theme_suffix(focus)), - review_status: "generated_unreviewed".to_string(), - participant_agent_id: profile.agent_id.clone(), - role_id: profile.role_id.clone(), - current_character_state_ref: format!("state/agents.msgpack#{}", profile.role_id), - thought_cluster_ref: focus.to_string(), - participant_local_context: HeartbeatParticipantLocalContext { - display_name: profile.display_name.clone(), - values: profile.values.clone(), - reactivity: profile.reactivity, - plasticity: profile.plasticity, - expressiveness: profile.expressiveness, - guardedness: profile.guardedness, - }, - observable_thought_summary: strongest_thought_summary(thought_lanes, incubation), - personality_projection: projection, - interpretation: format!("{} appraises {} through its current personality vector; reaction should follow this appraisal rather than a global mood knob.", profile.display_name, focus), - emotional_appraisal: HeartbeatEmotionalAppraisal { - valence, - arousal, - urgency, - curiosity, - guardedness, - thought_pressure: round3(thought_pressure), - }, - interpretation_label: label.to_string(), - confidence_notes: "Deterministic first-pass appraisal from typed role personality vectors and clustered thought state; useful as reaction guidance, not reviewed truth.".to_string(), - candidate_implications: HeartbeatCandidateImplications { - reaction_mode: reaction_mode(&label, bridge_decision).to_string(), - reaction_intensity: round3((urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0)), - should_speak: bridge_decision == "draft" && profile.role_id == "Persona" && guardedness < 0.75, - should_incubate: bridge_decision != "silence" && guardedness >= 0.55, - }, - review: HeartbeatAppraisalReview { - accepted_for_mutation: false, - rationale: "Appraisal may steer reaction and display; state mutation still requires the explicit selfPatch or project-state review path.".to_string(), - }, - } - }) - .collect::>(); - HeartbeatAgentThoughtAppraisals { - schema_version: "epiphany.agent_thought_appraisals.v0".to_string(), - updated_at: now_iso(), - thought_cluster_ref: focus.to_string(), - participant_appraisals: appraisals, - } -} - -fn build_agent_reactions( - appraisals: &HeartbeatAgentThoughtAppraisals, - bridge: &HeartbeatCognitionBridge, -) -> HeartbeatAgentReactions { - let bridge_decision = bridge.decision.speak_decision.as_str(); - let reactions = appraisals - .participant_appraisals - .iter() - .map(|appraisal| { - let role_id = appraisal.role_id.as_str(); - let arousal = appraisal.emotional_appraisal.arousal; - let guardedness = appraisal.emotional_appraisal.guardedness; - let curiosity = appraisal.emotional_appraisal.curiosity; - let mode = appraisal.candidate_implications.reaction_mode.as_str(); - let intensity = appraisal.candidate_implications.reaction_intensity; - HeartbeatAgentReaction { - reaction_id: format!("reaction-{}-{}", role_id, now_stamp()), - role_id: role_id.to_string(), - participant_agent_id: appraisal.participant_agent_id.clone(), - appraisal_id: appraisal.appraisal_id.clone(), - mode: mode.to_string(), - mood_label: mood_label(arousal, guardedness, curiosity).to_string(), - intensity: round3(intensity), - bridge_decision: bridge_decision.to_string(), - surface: if role_id == "Persona" { - "aquarium" - } else { - "internal" - } - .to_string(), - recommended_use: reaction_recommended_use(role_id, mode).to_string(), - } - }) - .collect::>(); - HeartbeatAgentReactions { - schema_version: "epiphany.agent_reactions.v0".to_string(), - updated_at: now_iso(), - reactions, - contract: "Reaction is appraisal output. It may pace, color, draft, or display behavior; it does not mutate state without review.".to_string(), - } -} - -fn cognition_tokens( - thought_lanes: &HeartbeatThoughtLanes, - incubation: &HeartbeatIncubation, - bridge: &HeartbeatCognitionBridge, -) -> BTreeSet { - let mut tokens = BTreeSet::new(); - for thread in &thought_lanes.analytic.active_threads { - tokens.extend(summary_tokens(&thread.topic)); - tokens.extend(summary_tokens(&thread.claim)); - tokens.extend(summary_tokens(&thread.counterweight)); - } - for thread in &thought_lanes.associative.active_threads { - tokens.extend(summary_tokens(&thread.topic)); - tokens.extend(summary_tokens(&thread.claim)); - tokens.extend(summary_tokens(&thread.counterweight)); - } - for theme in &incubation.themes { - tokens.extend(summary_tokens(&theme.theme_id)); - tokens.extend(summary_tokens(&theme.summary)); - tokens.extend(summary_tokens(&theme.latent_question)); - tokens.extend(summary_tokens(&theme.why_it_pulls)); - tokens.extend(summary_tokens(&theme.holding_close_because)); - } - for synthesis in &bridge.recent_syntheses { - tokens.extend(summary_tokens(&synthesis.summary)); - for topic in &synthesis.dominant_topics { - tokens.extend(summary_tokens(topic)); - } - } - tokens.extend(summary_tokens(&bridge.decision.reason)); - tokens -} - -fn thought_pressure( - thought_lanes: &HeartbeatThoughtLanes, - incubation: &HeartbeatIncubation, -) -> f64 { - let analytic = thought_lanes - .analytic - .active_threads - .iter() - .map(|thread| thread.desire_to_act) - .max_by(f64::total_cmp) - .unwrap_or(0.0); - let associative = thought_lanes - .associative - .active_threads - .iter() - .map(|thread| thread.desire_to_speak) - .max_by(f64::total_cmp) - .unwrap_or(0.0); - let theme = incubation - .themes - .iter() - .map(|theme| theme.strength) - .max_by(f64::total_cmp) - .unwrap_or(0.0); - (analytic * 0.35 + associative * 0.25 + theme * 0.4).clamp(0.0, 1.0) -} - -fn personality_projection( - profile: &RoleAppraisalProfile, - thought_tokens: &BTreeSet, -) -> Vec { - let mut scored = profile - .traits - .iter() - .map(|item| { - let trait_tokens = summary_tokens(&format!("{} {}", item.group, item.name)); - let overlap = token_overlap(&trait_tokens, thought_tokens); - let projection = round3((item.weight * (0.55 + overlap * 1.8)).clamp(0.0, 1.0)); - HeartbeatPersonalityProjection { - group: item.group.clone(), - name: item.name.clone(), - activation: round3(item.activation), - plasticity: round3(item.plasticity), - token_overlap: round3(overlap), - projection, - } - }) - .collect::>(); - scored.sort_by(|left, right| right.projection.total_cmp(&left.projection)); - scored.truncate(6); - scored -} - -fn strongest_thought_summary( - thought_lanes: &HeartbeatThoughtLanes, - incubation: &HeartbeatIncubation, -) -> String { - incubation - .themes - .first() - .map(|theme| theme.summary.as_str()) - .or_else(|| { - thought_lanes - .analytic - .active_threads - .first() - .map(|thread| thread.claim.as_str()) - }) - .unwrap_or("No salient thought cluster is active.") - .to_string() -} - -fn interpretation_label( - bridge_decision: &str, - arousal: f64, - guardedness: f64, - curiosity: f64, -) -> String { - if guardedness > 0.72 && arousal > 0.35 { - "protective_appraisal".to_string() - } else if curiosity > 0.68 { - "investigative_appraisal".to_string() - } else if bridge_decision == "draft" { - "expressive_appraisal".to_string() - } else if arousal < 0.12 { - "low_pressure_appraisal".to_string() - } else { - "incubating_appraisal".to_string() - } -} - -fn reaction_mode(label: &str, bridge_decision: &str) -> &'static str { - match (label, bridge_decision) { - ("protective_appraisal", _) => "hold_and_verify", - ("investigative_appraisal", _) => "inspect", - ("expressive_appraisal", "draft") => "draft", - ("low_pressure_appraisal", _) => "sleep_ruminate", - _ => "incubate", - } -} - -fn mood_label(arousal: f64, guardedness: f64, curiosity: f64) -> &'static str { - if guardedness > 0.72 && arousal > 0.35 { - "wary" - } else if curiosity > 0.68 && arousal > 0.25 { - "keen" - } else if arousal < 0.12 { - "drowsy" - } else if guardedness > curiosity { - "watchful" - } else { - "interested" - } -} - -fn reaction_recommended_use(role_id: &str, mode: &str) -> &'static str { - match (role_id, mode) { - ("Persona", "draft") => { - "Prepare a reviewed Aquarium-facing draft; do not post automatically." - } - (_, "hold_and_verify") => "Bias toward verifier/modeler review before expression.", - (_, "inspect") => { - "Bias the next heartbeat toward a bounded retrieval or modeling inspection." - } - (_, "sleep_ruminate") => "Let this organ sleep-ruminate unless real work arrives.", - _ => "Keep the thought incubating and visible in Aquarium.", - } -} - -fn topic_saturation_from_syntheses_and_themes( - syntheses: &[HeartbeatBridgeSynthesis], - themes: &[HeartbeatIncubationTheme], -) -> Vec { - let mut counts: BTreeMap = BTreeMap::new(); - for synthesis in syntheses { - for topic in &synthesis.dominant_topics { - *counts.entry(topic.to_string()).or_default() += 1; - } - } - for theme in themes { - let bonus = if matches!(theme.status.as_str(), "refractory" | "stalled") { - 2 - } else { - 1 - }; - *counts.entry(theme.theme_id.clone()).or_default() += bonus; - } - counts - .into_iter() - .filter(|(_, count)| *count > 1) - .map(|(topic, count)| HeartbeatTopicSaturation { - topic, - dominance: round3((count as f64 / syntheses.len().max(1) as f64).min(1.0)), - recent_mentions: count, - cooling_advice: - "Require fresh evidence or a new angle before surfacing this topic again." - .to_string(), - }) - .collect() -} - -fn refractory_topics_from_themes( - previous_bridge: &Option, - themes: &[HeartbeatIncubationTheme], -) -> Vec { - let previous_topics = previous_bridge - .as_ref() - .map(|value| value.refractory_topics.clone()) - .unwrap_or_default(); - let now = chrono::Utc::now(); - themes - .iter() - .filter_map(|theme| { - let status = theme.status.as_str(); - let saturation = theme.saturation_score; - if !matches!(status, "refractory" | "stalled") && saturation < 0.62 { - return None; - } - let topic = theme.theme_id.as_str(); - let previous_penalty = previous_topics - .iter() - .find(|entry| theme_similarity(topic, "", &entry.topic, "") >= 0.48) - .map(|entry| entry.penalty) - .unwrap_or(0.18); - let penalty = round3(theme.refractory_penalty.max(previous_penalty)); - let hours = if penalty >= 0.28 { - 4 - } else if penalty >= 0.20 { - 3 - } else { - 2 - }; - Some(HeartbeatRefractoryTopic { - topic: topic.to_string(), - penalty, - cools_until: (now + Duration::hours(hours)) - .to_rfc3339_opts(chrono::SecondsFormat::Secs, false) - .replace('Z', "+00:00"), - reason: build_refractory_reason(theme), - last_triggered_at: now_iso(), - }) - }) - .take(6) - .collect() -} - -fn build_source_coverage(records: &[RoleMemoryRecord]) -> HeartbeatSourceCoverage { - let mut role_counts: BTreeMap = BTreeMap::new(); - let mut kind_counts: BTreeMap = BTreeMap::new(); - for record in records { - *role_counts.entry(record.role_id.clone()).or_default() += 1; - *kind_counts.entry(record.memory_kind.clone()).or_default() += 1; - } - HeartbeatSourceCoverage { - schema_version: "epiphany.source_coverage.v0".to_string(), - updated_at: now_iso(), - roles: role_counts - .into_iter() - .map(|(role_id, count)| HeartbeatSourceCoverageRole { role_id, count }) - .collect(), - memory_kinds: kind_counts - .into_iter() - .map(|(kind, count)| HeartbeatSourceCoverageKind { kind, count }) - .collect(), - } -} - -fn best_matching_theme<'a>( - previous_themes: &'a [HeartbeatIncubationTheme], - theme_id: &str, - summary: &str, - source_memory_ids: &[String], -) -> Option<&'a HeartbeatIncubationTheme> { - let best = - previous_themes.iter().max_by(|left, right| { - theme_match_score(theme_id, summary, source_memory_ids, left).total_cmp( - &theme_match_score(theme_id, summary, source_memory_ids, right), - ) - })?; - (theme_match_score(theme_id, summary, source_memory_ids, best) >= 0.42).then_some(best) -} - -fn previous_support_count(previous_theme: Option<&HeartbeatIncubationTheme>) -> usize { - previous_theme.map(|theme| theme.support_count).unwrap_or(0) -} - -fn novelty_to_self( - theme_id: &str, - summary: &str, - source_memory_ids: &[String], - previous_themes: &[HeartbeatIncubationTheme], - bridge: &Option, -) -> f64 { - let mut strongest_match = 0.0_f64; - for theme in previous_themes { - strongest_match = strongest_match.max( - theme_similarity(theme_id, summary, &theme.theme_id, &theme.summary).max( - overlap_ratio_strings(source_memory_ids, &theme.source_memory_ids), - ), - ); - } - for synthesis in bridge - .as_ref() - .into_iter() - .flat_map(|value| value.recent_syntheses.iter()) - .into_iter() - .take(6) - { - strongest_match = strongest_match.max(theme_similarity( - theme_id, - summary, - &synthesis.dominant_topics.join(" / "), - &synthesis.summary, - )); - } - (1.0 - strongest_match).clamp(0.0, 1.0) -} - -fn novelty_to_room( - theme_id: &str, - summary: &str, - previous_candidate_interventions: &Option, - bridge: &Option, -) -> f64 { - let mut score = 0.64_f64; - for intervention in previous_candidate_interventions - .as_ref() - .into_iter() - .flat_map(|value| value.items.iter()) - .take(8) - { - let similarity = theme_similarity( - theme_id, - summary, - &intervention.intervention_id, - &intervention.draft, - ); - if similarity >= 0.42 { - return 0.22; - } - } - for synthesis in bridge - .as_ref() - .into_iter() - .flat_map(|value| value.recent_syntheses.iter()) - .into_iter() - .take(6) - { - let similarity = theme_similarity( - theme_id, - summary, - &synthesis.dominant_topics.join(" / "), - &synthesis.summary, - ); - if similarity >= 0.42 { - score = score.min(0.44); - } - } - score -} - -#[derive(Clone, Copy, Debug)] -struct SaturationMetrics { - score: f64, - recent_match_count: usize, -} - -fn saturation_metrics( - theme_id: &str, - summary: &str, - source_memory_ids: &[String], - previous_themes: &[HeartbeatIncubationTheme], - bridge: &Option, - support_count: usize, -) -> SaturationMetrics { - let mut recent_match_count = 0_usize; - for theme in previous_themes { - let similarity = theme_similarity(theme_id, summary, &theme.theme_id, &theme.summary).max( - overlap_ratio_strings(source_memory_ids, &theme.source_memory_ids), - ); - if similarity >= 0.42 { - recent_match_count += 1; - } - } - for synthesis in bridge - .as_ref() - .into_iter() - .flat_map(|value| value.recent_syntheses.iter()) - .into_iter() - .take(5) - { - let similarity = theme_similarity( - theme_id, - summary, - &synthesis.dominant_topics.join(" / "), - &synthesis.summary, - ); - if similarity >= 0.42 { - recent_match_count += 1; - } - } - let existing_topic_saturation = bridge - .as_ref() - .into_iter() - .flat_map(|value| value.topic_saturation.iter()) - .filter_map(|entry| { - let similarity = theme_similarity(theme_id, summary, &entry.topic, ""); - (similarity >= 0.42).then_some(entry.dominance) - }) - .fold(0.0_f64, f64::max); - SaturationMetrics { - score: (recent_match_count as f64 * 0.16 - + existing_topic_saturation * 0.42 - + (support_count as f64 / 10.0).min(1.0) * 0.16) - .clamp(0.0, 1.0), - recent_match_count, - } -} - -fn refractory_penalty( - theme_id: &str, - summary: &str, - bridge: &Option, -) -> f64 { - let mut penalty = 0.0_f64; - let now = chrono::Utc::now(); - for topic in bridge - .as_ref() - .into_iter() - .flat_map(|value| value.refractory_topics.iter()) - { - let cools_until = chrono::DateTime::parse_from_rfc3339(&topic.cools_until) - .ok() - .map(|value| value.with_timezone(&chrono::Utc)); - if cools_until.is_some_and(|deadline| deadline < now) { - continue; - } - let similarity = theme_similarity(theme_id, summary, &topic.topic, &topic.reason); - if similarity >= 0.48 { - penalty = penalty.max(topic.penalty * similarity); - } - } - penalty.clamp(0.0, 0.45) -} - -fn evidence_diversity( - source_roles: &[String], - source_kinds: &[String], - source_memory_ids: &[String], -) -> f64 { - (source_roles.len() as f64 * 0.18 - + source_kinds.len() as f64 * 0.16 - + (source_memory_ids.len() as f64 / 4.0).min(1.0) * 0.10) - .clamp(0.0, 1.0) -} - -fn exploration_bonus( - source_roles: &[String], - source_kinds: &[String], - source_coverage: &HeartbeatSourceCoverage, -) -> f64 { - let mut scores = Vec::new(); - for role_id in source_roles { - scores.push(inverse_role_coverage_weight(source_coverage, role_id)); - } - for kind in source_kinds { - scores.push(inverse_kind_coverage_weight(source_coverage, kind)); - } - if scores.is_empty() { - return 0.18; - } - average(scores.into_iter()).unwrap_or(0.18).clamp(0.0, 1.0) -} - -fn inverse_role_coverage_weight(source_coverage: &HeartbeatSourceCoverage, needle: &str) -> f64 { - inverse_coverage_count( - source_coverage - .roles - .iter() - .find(|entry| entry.role_id == needle) - .map(|entry| entry.count) - .unwrap_or(0), - ) -} - -fn inverse_kind_coverage_weight(source_coverage: &HeartbeatSourceCoverage, needle: &str) -> f64 { - inverse_coverage_count( - source_coverage - .memory_kinds - .iter() - .find(|entry| entry.kind == needle) - .map(|entry| entry.count) - .unwrap_or(0), - ) -} - -fn inverse_coverage_count(count: usize) -> f64 { - match count { - 0 | 1 => 0.90, - 2 => 0.64, - 3 => 0.42, - _ => 0.20, - } -} - -fn theme_status( - novelty_to_self: f64, - novelty_to_room: f64, - saturation_score: f64, - refractory_penalty: f64, - support_count: usize, - evidence_diversity: f64, - desire_to_speak: f64, - maturation: f64, -) -> &'static str { - if novelty_to_self < 0.28 && saturation_score >= 0.62 { - "stalled" - } else if refractory_penalty >= 0.18 && novelty_to_room < 0.72 { - "refractory" - } else if support_count >= 6 && evidence_diversity < 0.34 { - "stalled" - } else if support_count >= 3 && novelty_to_self < 0.55 { - "cooling" - } else if saturation_score >= 0.56 && novelty_to_self < 0.42 { - "cooling" - } else if desire_to_speak >= 0.74 - && (novelty_to_self >= 0.55 || novelty_to_room >= 0.82) - && saturation_score < 0.50 - || maturation >= 0.82 - { - "ripe" - } else { - "incubating" - } -} - -fn build_incubation_question(source_roles: &[String], source_kinds: &[String]) -> &'static str { - if source_roles.len() > 1 && source_kinds.len() > 1 { - "Why are several organs and memory kinds rhyming here, and what would falsify the rhyme before it hardens into doctrine?" - } else if source_roles.len() > 1 { - "Is this cross-organ rhyme a real pressure seam or just shared vocabulary bouncing around the hull?" - } else { - "Does this hot local seam deserve reinforcement, or is it only loud because nothing else moved yet?" - } -} - -fn build_incubation_attraction( - source_roles: &[String], - source_kinds: &[String], - novelty_to_self: f64, - evidence_diversity: f64, -) -> &'static str { - if novelty_to_self >= 0.62 && evidence_diversity >= 0.45 { - "It keeps pulling because it is still finding genuinely different support instead of merely changing hats." - } else if source_roles.len() >= 2 { - "It keeps pulling because more than one organ is worrying the same seam at once." - } else if source_kinds.len() >= 2 { - "It keeps pulling because the same seam is surfacing across different memory kinds, not just one hot recollection." - } else { - "It keeps pulling because one live seam still has some blood in it; that does not make it king." - } -} - -fn build_incubation_holding_line( - status: &str, - saturation_score: f64, - novelty_to_self: f64, - exploration_bonus: f64, -) -> &'static str { - match status { - "stalled" => { - "This seam is repeating itself without earning new structure. Merge the receipts, cool it off, and go somewhere less domesticated." - } - "refractory" => { - "This seam has been chewing the same meat too recently. Let it cool unless a genuinely different source family forces it back open." - } - "ripe" => { - "This seam has enough connective tissue that silence should be a deliberate choice rather than a reflex." - } - _ if saturation_score >= 0.55 && novelty_to_self < 0.45 => { - "The family resemblance is getting too strong. Follow a stranger branch before this theme speaks again." - } - _ if exploration_bonus >= 0.45 => { - "This one is still drawing energy from underworked terrain, so another pass might actually change it." - } - _ => { - "Give it another pass so the thought can either grow teeth or admit it was only a pleasant loop." - } - } -} - -fn build_refractory_reason(theme: &HeartbeatIncubationTheme) -> String { - let topic = theme.theme_id.as_str(); - let novelty_to_self = theme.novelty_to_self; - let saturation_score = theme.saturation_score; - if novelty_to_self < 0.3 { - format!( - "{topic} is matching Epiphany's own recent thought history too closely to deserve another immediate pass." - ) - } else if saturation_score >= 0.62 { - format!( - "{topic} has dominated too many recent bridge syntheses and needs cooling before it becomes machine religion." - ) - } else { - format!("{topic} needs a brief cooling period before it earns attention again.") - } -} - -fn bridge_summary(theme: &HeartbeatIncubationTheme, speak_decision: &str) -> String { - let topic = theme.theme_id.as_str(); - let status = theme.status.as_str(); - let novelty_to_self = theme.novelty_to_self; - let novelty_to_room = theme.novelty_to_room; - match (speak_decision, status) { - ("draft", _) => format!( - "Analytic evidence edges and associative incubation currently converge on {topic}; this seam is still fresh enough to surface (self={novelty_to_self:.2}, room={novelty_to_room:.2})." - ), - (_, "refractory" | "stalled") => format!( - "{topic} is still loud, but the bridge is cooling it off because the machine has been worrying the same seam too hard." - ), - _ => format!( - "{topic} is still live, so the bridge is letting it sit and deepen instead of forcing novelty theater." - ), - } -} - -fn synthesis_saturation_note( - status: &str, - saturation_score: f64, - novelty_to_self: f64, -) -> Option<&'static str> { - if matches!(status, "refractory" | "stalled") { - Some( - "This topic is currently under cooling discipline; demand stranger support before resurfacing it.", - ) - } else if saturation_score >= 0.55 && novelty_to_self < 0.45 { - Some( - "Recent syntheses are getting too familial; rebranch before letting this seam become doctrine.", - ) - } else if status == "incubating" { - Some( - "A live thought is allowed to sit without performing a fresh retrieval errand every pass.", - ) - } else { - None - } -} - -fn bridge_decision_reason( - strongest_status: &str, - novelty_to_self: f64, - novelty_to_room: f64, - saturation_score: f64, - refractory_penalty: f64, - speak_decision: &str, -) -> &'static str { - match speak_decision { - "draft" => { - "Bridge surfaces this seam because it is fresh enough to the swarm-facing surface and not yet overworked." - } - "hold" if strongest_status == "incubating" => { - "Bridge keeps this seam in incubation because it is still live and grounded; retrieval is support, not throat-clearing ritual." - } - "hold" => { - "Bridge holds the seam open without surfacing it yet; there is enough blood for another pass, but not enough freshness for speech." - } - "silence" if strongest_status == "refractory" || refractory_penalty >= 0.18 => { - "Bridge silences this seam temporarily because the machine has been circling it too hard." - } - "silence" - if strongest_status == "stalled" - || saturation_score >= 0.62 - || novelty_to_self < 0.28 => - { - "Bridge silences this seam because it is mostly self-echo at the moment." - } - _ if novelty_to_room < 0.40 => { - "Bridge withholds surface speech because the thought is not novel enough to the swarm-facing room." - } - _ => { - "Bridge converts analytic and associative lanes into draft/hold/silence guidance without granting authority." - } - } -} - -fn theme_similarity( - left_topic: &str, - left_summary: &str, - right_topic: &str, - right_summary: &str, -) -> f64 { - let left_tokens = summary_tokens(&format!("{left_topic} {left_summary}")); - let right_tokens = summary_tokens(&format!("{right_topic} {right_summary}")); - token_overlap(&left_tokens, &right_tokens) -} - -fn theme_match_score( - theme_id: &str, - summary: &str, - source_memory_ids: &[String], - theme: &HeartbeatIncubationTheme, -) -> f64 { - theme_similarity(theme_id, summary, &theme.theme_id, &theme.summary).max(overlap_ratio_strings( - source_memory_ids, - &theme.source_memory_ids, - )) -} - -fn overlap_ratio_strings(left: &[String], right: &[String]) -> f64 { - if left.is_empty() || right.is_empty() { - return 0.0; - } - let left_set = left.iter().cloned().collect::>(); - let right_set = right.iter().cloned().collect::>(); - let shared = left_set.intersection(&right_set).count() as f64; - if shared <= 0.0 { - return 0.0; - } - let union = left_set.union(&right_set).count() as f64; - if union <= 0.0 { 0.0 } else { shared / union } -} - -fn update_sleep_cycle( - previous: Option<&HeartbeatSleepCycle>, - incubation: &HeartbeatIncubation, - allow_dream: bool, -) -> HeartbeatSleepCycle { - let cycle_hours = previous - .map(|cycle| cycle.cycle_hours) - .unwrap_or(4) - .clamp(2, 12); - let nap_duration_minutes = previous - .map(|cycle| cycle.nap_duration_minutes) - .unwrap_or(60) - .clamp(15, cycle_hours * 60 - 5); - let phase_offset_minutes = previous - .map(|cycle| cycle.phase_offset_minutes_local) - .unwrap_or(120) - .clamp(0, cycle_hours * 60 - 1); - let now = chrono::Utc::now(); - let cycle_minutes = cycle_hours * 60; - let shifted = now.timestamp().div_euclid(60) - phase_offset_minutes; - let minute_in_cycle = shifted.rem_euclid(cycle_minutes); - let is_napping = minute_in_cycle < nap_duration_minutes; - let cycle_start_minute = shifted - minute_in_cycle + phase_offset_minutes; - let nap_start = - chrono::DateTime::::from_timestamp(cycle_start_minute * 60, 0).unwrap_or(now); - let nap_end = nap_start + Duration::minutes(nap_duration_minutes); - let next_nap_start = if is_napping { - nap_start + Duration::minutes(cycle_minutes) - } else if minute_in_cycle >= nap_duration_minutes { - nap_start + Duration::minutes(cycle_minutes) - } else { - nap_start - }; - let active_themes = incubation - .themes - .iter() - .map(|theme| theme.theme_id.as_str()) - .take(4) - .map(str::to_string) - .collect::>(); - let previous_dream_count = previous - .map(|cycle| cycle.dream_count_in_current_nap) - .unwrap_or(0); - let dream_count = if is_napping && allow_dream { - previous_dream_count.saturating_add(1) - } else if is_napping { - previous_dream_count - } else { - 0 - }; - HeartbeatSleepCycle { - schema_version: "epiphany.sleep_cycle.v0".to_string(), - enabled: true, - cycle_hours, - nap_duration_minutes, - phase_offset_minutes_local: phase_offset_minutes, - reply_mode: "sleep_rumination".to_string(), - is_napping, - current_nap_started_at: if is_napping { - Some(nap_start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) - } else { - None - }, - current_nap_ends_at: if is_napping { - Some(nap_end.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) - } else { - None - }, - next_nap_starts_at: next_nap_start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), - last_dream_at: if is_napping && allow_dream { - Some(now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) - } else { - previous.and_then(|cycle| cycle.last_dream_at.clone()) - }, - dream_count_in_current_nap: dream_count, - active_dream_themes: active_themes, - last_distillation_summary: if is_napping { - "Sleep pass prefers memory compression, resonance cooling, and dream residue over active work." - } else { - "Awake pass keeps resonance/incubation fresh without speaking unless Persona has a real surface reason." - } - .to_string(), - } -} - -fn summary_tokens(summary: &str) -> BTreeSet { - summary - .split(|ch: char| !ch.is_ascii_alphanumeric()) - .map(|part| part.trim().to_ascii_lowercase()) - .filter(|part| part.len() >= 4 && !STOP_WORDS.contains(&part.as_str())) - .collect() -} - -fn token_overlap(left: &BTreeSet, right: &BTreeSet) -> f64 { - let shared = left.intersection(right).count() as f64; - if shared == 0.0 { - return 0.0; - } - let union = left.union(right).count() as f64; - if union <= 0.0 { 0.0 } else { shared / union } -} - -fn shared_tokens(left: &BTreeSet, right: &BTreeSet) -> Vec { - left.intersection(right).take(8).cloned().collect() -} - -fn stable_theme_suffix(value: &str) -> String { - let mut hash = 5381_u64; - for byte in value.as_bytes() { - hash = ((hash << 5).wrapping_add(hash)).wrapping_add(*byte as u64); - } - format!("{:x}", hash & 0xffff) -} - -pub(super) fn now_iso() -> String { - chrono::Utc::now() - .to_rfc3339_opts(chrono::SecondsFormat::Secs, false) - .replace('Z', "+00:00") -} - fn parse_heartbeat_time(label: &str, value: &str) -> Result> { DateTime::parse_from_rfc3339(value) .map(|time| time.with_timezone(&Utc)) @@ -3994,6 +1797,10 @@ 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 } @@ -4002,24 +1809,6 @@ pub(super) fn round3(value: f64) -> f64 { (value * 1_000.0).round() / 1_000.0 } -fn average(values: impl Iterator) -> Option { - let mut total = 0.0; - let mut count = 0_usize; - for value in values { - if value.is_finite() { - total += value; - count += 1; - } - } - (count > 0).then_some(total / count as f64) -} - -const STOP_WORDS: &[&str] = &[ - "about", "after", "agent", "before", "being", "between", "could", "from", "have", "into", - "lane", "memory", "more", "must", "should", "state", "than", "that", "their", "there", "this", - "through", "when", "with", "work", -]; - #[cfg(test)] mod tests { use super::*; @@ -4043,7 +1832,6 @@ mod tests { schedule_id: "native-work".to_string(), source_scene_ref: "test/native".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4075,7 +1863,6 @@ mod tests { schedule_id: "native-work-repeat".to_string(), source_scene_ref: "test/native".to_string(), defer_completion: false, - agent_store: None, resident_self_store: None, }, ) @@ -4140,7 +1927,6 @@ mod tests { false, "wake-health", "test://wake-health", - None, )?; assert_eq!(pulse.status, "granted"); @@ -4192,7 +1978,6 @@ mod tests { schedule_id: "hot-work".to_string(), source_scene_ref: "test/high-heat".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4225,7 +2010,6 @@ mod tests { schedule_id: "hot-work-repeat".to_string(), source_scene_ref: "test/high-heat".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, ) @@ -4255,7 +2039,6 @@ mod tests { schedule_id: "stale-work".to_string(), source_scene_ref: "test/stale".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4310,7 +2093,6 @@ mod tests { schedule_id: "after-stale-repair".to_string(), source_scene_ref: "test/stale".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4359,7 +2141,6 @@ mod tests { schedule_id: "pallas.turn-001".to_string(), source_scene_ref: "ghostlight/pallas-training-loop-v0".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4414,7 +2195,6 @@ mod tests { schedule_id: "Persona-mentioned".to_string(), source_scene_ref: "test/Persona-mentioned".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4518,7 +2298,6 @@ mod tests { schedule_id: "Persona-failure".to_string(), source_scene_ref: "test/Persona-failure".to_string(), defer_completion: true, - agent_store: None, resident_self_store: None, }, )?; @@ -4578,7 +2357,6 @@ mod tests { &artifact_dir, "Persona-unknown", "test/Persona-unknown", - None, false, )?; let request_id = pulse["requestId"].as_str().expect("request id").to_string(); @@ -4616,7 +2394,6 @@ mod tests { &artifact_dir, "Persona-unknown-repeat", "test/Persona-unknown", - None, false, )?; assert_eq!(next["status"], "idle"); @@ -4662,7 +2439,6 @@ mod tests { &artifact_dir, "persona-pulse-braked", "test/persona-pulse", - None, true, )?; assert_eq!(braked["status"], "refused-by-swarm-brake"); @@ -4678,7 +2454,6 @@ mod tests { &artifact_dir, "persona-pulse-one", "test/persona-pulse", - None, false, )?; assert_eq!(first["status"], "reserved"); @@ -4687,7 +2462,6 @@ mod tests { &artifact_dir, "persona-pulse-two", "test/persona-pulse", - None, false, )?; assert_eq!(second["status"], "already-running"); @@ -4695,71 +2469,4 @@ mod tests { assert_eq!(state.persona_turn_requests.len(), 1); Ok(()) } - - #[test] - fn appraisal_mood_timing_carries_affect_vector() { - let mut state = default_heartbeat_state(1.0); - patch_missing_participants(&mut state); - let appraisals = HeartbeatAgentThoughtAppraisals { - schema_version: "epiphany.agent_thought_appraisals.v0".to_string(), - updated_at: "2026-05-20T00:00:00+00:00".to_string(), - thought_cluster_ref: "test-affect".to_string(), - participant_appraisals: vec![HeartbeatAgentThoughtAppraisal { - appraisal_id: "appraisal-Persona-affect".to_string(), - participant_agent_id: "epiphany.Persona".to_string(), - role_id: "Persona".to_string(), - emotional_appraisal: HeartbeatEmotionalAppraisal { - valence: 0.2, - arousal: 0.81, - urgency: 0.66, - curiosity: 0.14, - guardedness: 0.72, - thought_pressure: 0.77, - }, - personality_projection: vec![ - HeartbeatPersonalityProjection { - name: "anger".to_string(), - projection: 0.88, - ..HeartbeatPersonalityProjection::default() - }, - HeartbeatPersonalityProjection { - name: "dismissal".to_string(), - projection: 0.63, - ..HeartbeatPersonalityProjection::default() - }, - HeartbeatPersonalityProjection { - name: "flippancy".to_string(), - projection: 0.41, - ..HeartbeatPersonalityProjection::default() - }, - ], - candidate_implications: HeartbeatCandidateImplications { - reaction_intensity: 0.79, - ..HeartbeatCandidateImplications::default() - }, - ..HeartbeatAgentThoughtAppraisal::default() - }], - }; - - apply_mood_timing_from_appraisals(&mut state, &appraisals); - let persona = state - .participants - .iter() - .find(|participant| participant.role_id == "Persona") - .expect("Persona participant"); - let mood = persona.mood_timing.as_ref().expect("Persona mood timing"); - assert_eq!(mood.emotional_dimensions.len(), 32); - assert_eq!(mood_dimension(mood, "anger"), Some(0.88)); - assert_eq!(mood_dimension(mood, "dismissal"), Some(0.63)); - assert_eq!(mood_dimension(mood, "flippancy"), Some(0.41)); - assert_eq!(mood_dimension(mood, "valence"), Some(0.2)); - assert_eq!(mood_dimension(mood, "arousal"), Some(0.81)); - } - - fn mood_dimension(mood: &HeartbeatMoodTiming, name: &str) -> Option { - mood.emotional_dimensions - .iter() - .find(|dimension| dimension.name == name) - .map(|dimension| dimension.value) - } } diff --git a/epiphany-core/src/heartbeat_state/heartbeat_documents.rs b/epiphany-core/src/heartbeat_state/heartbeat_documents.rs index 3c2617f9..1d0f39fc 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_documents.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_documents.rs @@ -2,17 +2,12 @@ use cultcache_rs::DatabaseEntry; use serde::Deserialize; use serde::Serialize; use serde_json::Value; -use std::collections::BTreeMap; 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.v0"; -pub const HEARTBEAT_COGNITION_TYPE: &str = "epiphany.agent_heartbeat_cognition"; -pub const HEARTBEAT_COGNITION_KEY: &str = "default"; -pub const HEARTBEAT_COGNITION_SCHEMA_VERSION: &str = "epiphany.agent_heartbeat_cognition.v0"; pub const HEARTBEAT_STATUS_SCHEMA_VERSION: &str = "epiphany.agent_heartbeat_status.v0"; pub const INITIATIVE_SCHEMA_VERSION: &str = "ghostlight.initiative_schedule.v0"; -pub const VOID_ROUTINE_SCHEMA_VERSION: &str = "epiphany.void_routine.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"; @@ -75,40 +70,6 @@ pub struct EpiphanyHeartbeatStateEntry { pub persona_conversation_retention_plan: Option, } -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "epiphany.agent_heartbeat_cognition", - schema = "EpiphanyHeartbeatCognitionEntry" -)] -pub struct EpiphanyHeartbeatCognitionEntry { - #[cultcache(key = 0)] - pub schema_version: String, - #[cultcache(key = 1)] - pub updated_at: String, - #[cultcache(key = 2, default)] - pub latest_run_id: Option, - #[cultcache(key = 3, default)] - pub latest_artifact_ref: Option, - #[cultcache(key = 4, default)] - pub source: Option, - #[cultcache(key = 5, default)] - pub sleep_cycle: Option, - #[cultcache(key = 6, default)] - pub memory_resonance: Option, - #[cultcache(key = 7, default)] - pub incubation: Option, - #[cultcache(key = 8, default)] - pub thought_lanes: Option, - #[cultcache(key = 9, default)] - pub bridge: Option, - #[cultcache(key = 10, default)] - pub candidate_interventions: Option, - #[cultcache(key = 11, default)] - pub appraisals: Option, - #[cultcache(key = 12, default)] - pub reactions: Option, -} - #[derive(Clone, Debug, PartialEq, DatabaseEntry)] #[cultcache( type = "epiphany.heartbeat.stale_turn_repair", @@ -203,48 +164,6 @@ pub struct EpiphanyHeartbeatArtifactRetentionReceipt { pub private_state_exposed: bool, } -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "epiphany.agent_heartbeat", - schema = "EpiphanyHeartbeatStateEntry" -)] -pub(super) struct LegacyHeartbeatStateWithCognition { - #[cultcache(key = 0)] - pub(super) schema_version: String, - #[cultcache(key = 1)] - pub(super) target_heartbeat_rate: f64, - #[cultcache(key = 2)] - pub(super) scene_clock: f64, - #[cultcache(key = 3)] - pub(super) selection_policy: HeartbeatSelectionPolicy, - #[cultcache(key = 4)] - pub(super) pacing_policy: HeartbeatPacingPolicy, - #[cultcache(key = 5)] - pub(super) participants: Vec, - #[cultcache(key = 6)] - pub(super) history: Vec, - #[cultcache(key = 7, default)] - pub(super) sleep_cycle: Option, - #[cultcache(key = 8, default)] - pub(super) memory_resonance: Option, - #[cultcache(key = 9, default)] - pub(super) incubation: Option, - #[cultcache(key = 10, default)] - pub(super) thought_lanes: Option, - #[cultcache(key = 11, default)] - pub(super) bridge: Option, - #[cultcache(key = 12, default)] - pub(super) candidate_interventions: Option, - #[cultcache(key = 13, default)] - pub(super) appraisals: Option, - #[cultcache(key = 14, default)] - pub(super) reactions: Option, - #[cultcache(key = 15, default)] - pub(super) extra: BTreeMap, - #[cultcache(key = 19, default)] - pub(super) pending_mentions: Vec, -} - #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct HeartbeatSelectionPolicy { pub mode: String, @@ -286,405 +205,10 @@ pub struct HeartbeatAdaptivePacing { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct HeartbeatAdaptivePacingSignals { pub external_urgency: f64, - pub max_anxiety: f64, - pub average_anxiety: f64, - pub max_urgency: f64, - pub max_arousal: f64, - pub max_thought_pressure: f64, - pub max_reaction_intensity: f64, pub pending_pressure: f64, pub contract: String, } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatMemoryResonance { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub source: String, - pub record_count: usize, - pub pairs: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatMemoryResonancePair { - pub left_role: String, - pub left_memory_id: String, - pub left_memory_kind: String, - pub left_summary: String, - pub right_role: String, - pub right_memory_id: String, - pub right_memory_kind: String, - pub right_summary: String, - pub strength: f64, - pub shared_tokens: Vec, - pub source_roles: Vec, - pub source_kinds: Vec, - pub evidence_refs: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatSourceCoverage { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub roles: Vec, - pub memory_kinds: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatSourceCoverageRole { - pub role_id: String, - pub count: usize, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatSourceCoverageKind { - pub kind: String, - pub count: usize, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatIncubation { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub source_coverage: HeartbeatSourceCoverage, - pub last_incubation_summary: String, - pub themes: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatIncubationTheme { - pub theme_id: String, - pub summary: String, - pub strength: f64, - pub source: String, - pub source_roles: Vec, - pub source_kinds: Vec, - pub source_memory_ids: Vec, - pub support_count: usize, - pub evidence_diversity: f64, - pub exploration_bonus: f64, - pub novelty: f64, - pub novelty_to_self: f64, - pub novelty_to_room: f64, - pub maturation: f64, - pub desire_to_speak: f64, - pub saturation_score: f64, - pub recent_match_count: usize, - pub refractory_penalty: f64, - pub priority_score: f64, - pub status: String, - pub latent_question: String, - pub why_it_pulls: String, - pub holding_close_because: String, - pub updated_at: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatThoughtLanes { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub analytic: HeartbeatAnalyticLane, - pub associative: HeartbeatAssociativeLane, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAnalyticLane { - pub description: String, - pub active_threads: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAssociativeLane { - pub description: String, - pub active_threads: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAnalyticThread { - pub thread_id: String, - pub topic: String, - pub claim: String, - pub evidence_refs: Vec, - pub salience: f64, - pub confidence: f64, - pub desire_to_act: f64, - pub counterweight: String, - pub last_touched_at: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAssociativeThread { - pub thread_id: String, - pub topic: String, - pub claim: String, - pub source_theme_id: String, - pub novelty: f64, - pub room_relevance: f64, - pub desire_to_speak: f64, - pub status: String, - pub counterweight: String, - pub last_touched_at: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatCognitionBridge { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub recent_syntheses: Vec, - pub source_coverage: HeartbeatSourceCoverage, - pub topic_saturation: Vec, - pub refractory_topics: Vec, - pub unresolved_tensions: Vec, - pub decision: HeartbeatBridgeDecision, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatBridgeSynthesis { - pub timestamp: String, - pub summary: String, - pub dominant_topics: Vec, - pub lane_balance: String, - pub speak_decision: String, - pub theme_status: String, - pub novelty_to_self: f64, - pub novelty_to_room: f64, - pub saturation_score: f64, - pub saturation_note: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatTopicSaturation { - pub topic: String, - pub dominance: f64, - pub recent_mentions: usize, - pub cooling_advice: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatRefractoryTopic { - pub topic: String, - pub penalty: f64, - pub cools_until: String, - pub reason: String, - pub last_triggered_at: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatBridgeTension { - pub topic: String, - pub summary: String, - pub opened_at: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatBridgeDecision { - pub lane_balance: String, - pub speak_decision: String, - pub reason: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatSleepCycle { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub enabled: bool, - pub cycle_hours: i64, - pub nap_duration_minutes: i64, - pub phase_offset_minutes_local: i64, - pub reply_mode: String, - pub is_napping: bool, - pub current_nap_started_at: Option, - pub current_nap_ends_at: Option, - pub next_nap_starts_at: String, - pub last_dream_at: Option, - pub dream_count_in_current_nap: u64, - pub active_dream_themes: Vec, - pub last_distillation_summary: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAgentThoughtAppraisals { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub thought_cluster_ref: String, - pub participant_appraisals: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAgentThoughtAppraisal { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub appraisal_id: String, - pub review_status: String, - pub participant_agent_id: String, - pub role_id: String, - pub current_character_state_ref: String, - pub thought_cluster_ref: String, - pub participant_local_context: HeartbeatParticipantLocalContext, - pub observable_thought_summary: String, - pub personality_projection: Vec, - pub interpretation: String, - pub emotional_appraisal: HeartbeatEmotionalAppraisal, - pub interpretation_label: String, - pub confidence_notes: String, - pub candidate_implications: HeartbeatCandidateImplications, - pub review: HeartbeatAppraisalReview, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatParticipantLocalContext { - pub display_name: String, - pub values: Vec, - pub reactivity: f64, - pub plasticity: f64, - pub expressiveness: f64, - pub guardedness: f64, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatPersonalityProjection { - pub group: String, - pub name: String, - pub activation: f64, - pub plasticity: f64, - pub token_overlap: f64, - pub projection: f64, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatEmotionalAppraisal { - pub valence: f64, - pub arousal: f64, - pub urgency: f64, - pub curiosity: f64, - pub guardedness: f64, - pub thought_pressure: f64, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatCandidateImplications { - pub reaction_mode: String, - pub reaction_intensity: f64, - pub should_speak: bool, - pub should_incubate: bool, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAppraisalReview { - pub accepted_for_mutation: bool, - pub rationale: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAgentReactions { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub reactions: Vec, - pub contract: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatAgentReaction { - pub reaction_id: String, - pub role_id: String, - pub participant_agent_id: String, - pub appraisal_id: String, - pub mode: String, - pub mood_label: String, - pub intensity: f64, - pub bridge_decision: String, - pub surface: String, - pub recommended_use: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatCandidateInterventions { - #[serde(rename = "schema_version")] - pub schema_version: String, - pub updated_at: String, - pub items: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct HeartbeatCandidateIntervention { - pub intervention_id: String, - pub summary: String, - pub draft: String, - pub decision: String, - pub requires_persona: bool, - pub requires_review: bool, - pub novelty_to_room: f64, - pub saturation_score: f64, - pub created_at: String, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct HeartbeatInitiativeHeatPolicy { #[serde(default = "default_heat_schema_version")] @@ -749,19 +273,9 @@ pub struct HeartbeatParticipant { #[serde(default)] pub groups: Vec, #[serde(default = "default_heat_global_multiplier")] - pub personality_cooldown_multiplier: f64, - #[serde(default = "default_heat_global_multiplier")] - pub mood_cooldown_multiplier: f64, - #[serde(default = "default_heat_global_multiplier")] pub initiative_heat_multiplier: f64, #[serde(default)] pub initiative_heat: Option, - #[serde(default)] - pub personality_timing: Option, - #[serde(default)] - pub mood_timing: Option, - #[serde(default)] - pub birth_personality_seed: Option, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -782,53 +296,6 @@ pub struct HeartbeatInitiativeHeatBasis { pub reason: String, } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatPersonalityTiming { - pub schema_version: String, - pub source: String, - pub cooldown_multiplier: f64, - pub work_drive: f64, - pub handsiness: f64, - pub caution: f64, - pub rumination_bias: f64, - pub basis: Vec, - pub contract: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatMoodTiming { - pub schema_version: String, - pub source: Option, - pub cooldown_multiplier: f64, - #[serde(default)] - pub emotional_dimensions: Vec, - pub anxiety: f64, - pub urgency: f64, - pub arousal: f64, - pub thought_pressure: f64, - pub guardedness: f64, - pub reaction_intensity: f64, - pub contract: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatMoodDimension { - pub name: String, - pub value: f64, - pub source_path: String, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct HeartbeatBirthPersonalitySeed { - pub schema_version: String, - pub source: String, - pub projection_id: String, - pub repo_id: String, - pub heartbeat_deltas: BTreeMap, - pub default_mood_pressure: BTreeMap, - pub contract: String, -} - #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct HeartbeatPendingTurn { pub status: String, @@ -857,16 +324,6 @@ pub struct HeartbeatPendingTurn { pub completed_scene_clock: Option, #[serde(rename = "nextReadyAt", default)] pub next_ready_at: Option, - #[serde( - rename = "personalityCooldownMultiplier", - default = "default_heat_global_multiplier" - )] - pub personality_cooldown_multiplier: f64, - #[serde( - rename = "moodCooldownMultiplier", - default = "default_heat_global_multiplier" - )] - pub mood_cooldown_multiplier: f64, #[serde( rename = "initiativeHeatMultiplier", default = "default_heat_global_multiplier" @@ -1079,7 +536,6 @@ pub struct HeartbeatTickOptions { pub schedule_id: String, pub source_scene_ref: String, pub defer_completion: bool, - pub agent_store: Option, pub resident_self_store: Option, } @@ -1096,7 +552,6 @@ pub struct HeartbeatPumpOptions { pub target_role: Option, pub schedule_id: String, pub source_scene_ref: String, - pub agent_store: Option, pub resident_self_store: Option, } @@ -1144,13 +599,6 @@ pub struct HeartbeatStaleTurnRepairOptions { pub reason: String, } -#[derive(Clone, Debug, PartialEq)] -pub struct VoidRoutineOptions { - pub agent_store: Option, - pub source: String, - pub allow_dream: bool, -} - #[derive(Clone, Debug, PartialEq)] pub struct GhostlightSceneParticipantSeed { pub agent_id: String, diff --git a/epiphany-core/src/heartbeat_state/heartbeat_pacing.rs b/epiphany-core/src/heartbeat_state/heartbeat_pacing.rs index 052af1f4..fd86550f 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_pacing.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_pacing.rs @@ -28,24 +28,16 @@ pub(super) fn adaptive_swarm_pacing( .filter(|participant| participant.status == "active") .count(); let running_turns = running_turn_count(state); - let mood_signals = swarm_mood_signals(state); let external_urgency = options.external_urgency.clamp(0.0, 1.0); let pending_pressure = if active_participants == 0 { 0.0 } else { (running_turns as f64 / active_participants as f64).clamp(0.0, 1.0) }; - let pressure = [ - external_urgency, - mood_signals.max_anxiety, - mood_signals.max_urgency, - mood_signals.max_reaction_intensity, - mood_signals.max_thought_pressure, - pending_pressure * 0.65, - ] - .into_iter() - .fold(0.0_f64, f64::max) - .clamp(0.0, 1.0); + let pressure = [external_urgency, pending_pressure * 0.65] + .into_iter() + .fold(0.0_f64, f64::max) + .clamp(0.0, 1.0); let base_rate = options .base_heartbeat_rate @@ -79,55 +71,12 @@ pub(super) fn adaptive_swarm_pacing( active_participants, signals: HeartbeatAdaptivePacingSignals { external_urgency: round3(external_urgency), - max_anxiety: round3(mood_signals.max_anxiety), - average_anxiety: round3(mood_signals.average_anxiety), - max_urgency: round3(mood_signals.max_urgency), - max_arousal: round3(mood_signals.max_arousal), - max_thought_pressure: round3(mood_signals.max_thought_pressure), - max_reaction_intensity: round3(mood_signals.max_reaction_intensity), pending_pressure: round3(pending_pressure), - contract: "Anxiety-like state raises tempo and concurrency; calm state lets the swarm sleep slow.".to_string(), + contract: "Explicit external urgency and running-turn pressure determine scheduler tempo and concurrency.".to_string(), }, } } -#[derive(Default)] -struct SwarmMoodSignals { - max_anxiety: f64, - average_anxiety: f64, - max_urgency: f64, - max_arousal: f64, - max_thought_pressure: f64, - max_reaction_intensity: f64, -} - -fn swarm_mood_signals(state: &EpiphanyHeartbeatStateEntry) -> SwarmMoodSignals { - let mut signals = SwarmMoodSignals::default(); - let mut anxiety_total = 0.0; - let mut anxiety_count = 0_usize; - for participant in &state.participants { - let Some(mood) = participant.mood_timing.as_ref() else { - continue; - }; - let anxiety = mood.anxiety; - let urgency = mood.urgency; - let arousal = mood.arousal; - let thought_pressure = mood.thought_pressure; - let reaction_intensity = mood.reaction_intensity; - signals.max_anxiety = signals.max_anxiety.max(anxiety); - signals.max_urgency = signals.max_urgency.max(urgency); - signals.max_arousal = signals.max_arousal.max(arousal); - signals.max_thought_pressure = signals.max_thought_pressure.max(thought_pressure); - signals.max_reaction_intensity = signals.max_reaction_intensity.max(reaction_intensity); - anxiety_total += anxiety; - anxiety_count += 1; - } - if anxiety_count > 0 { - signals.average_anxiety = anxiety_total / anxiety_count as f64; - } - signals -} - pub(super) fn running_turn_count(state: &EpiphanyHeartbeatStateEntry) -> usize { state .participants @@ -141,20 +90,8 @@ pub(super) fn running_turn_count(state: &EpiphanyHeartbeatStateEntry) -> usize { .count() } -pub(super) fn personality_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 { - nonzero_multiplier(participant.personality_cooldown_multiplier).clamp(0.25, 3.0) -} - -pub(super) fn mood_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 { - nonzero_multiplier(participant.mood_cooldown_multiplier).clamp(0.25, 3.0) -} - pub(super) fn effective_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 { - round3( - personality_cooldown_multiplier(participant) * mood_cooldown_multiplier(participant) - / initiative_heat_multiplier(participant), - ) - .clamp(0.05, 8.0) + round3(1.0 / initiative_heat_multiplier(participant)).clamp(0.05, 8.0) } pub(super) fn initiative_heat_multiplier(participant: &HeartbeatParticipant) -> f64 { diff --git a/epiphany-core/src/heartbeat_state/heartbeat_projection.rs b/epiphany-core/src/heartbeat_state/heartbeat_projection.rs index d648780a..94b43f2e 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_projection.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_projection.rs @@ -1,5 +1,3 @@ -use super::HEARTBEAT_COGNITION_KEY; -use super::HEARTBEAT_COGNITION_TYPE; use super::HEARTBEAT_STATE_KEY; use super::HEARTBEAT_STATE_TYPE; use super::HEARTBEAT_STATUS_SCHEMA_VERSION; @@ -7,15 +5,11 @@ use super::HeartbeatHistoryEvent; use super::HeartbeatParticipant; use super::HeartbeatPendingTurn; use super::HeartbeatSelectionPolicy; -use super::VOID_ROUTINE_SCHEMA_VERSION; use super::effective_cooldown_multiplier; use super::initiative_heat_multiplier; -use super::load_heartbeat_cognition_entry; use super::load_heartbeat_state_entry; -use super::mood_cooldown_multiplier; use super::participant_arena; use super::participant_kind; -use super::personality_cooldown_multiplier; use anyhow::Result; use serde_json::Value; use std::cmp::Reverse; @@ -48,7 +42,6 @@ pub fn heartbeat_status_projection( "availableActions": ["init", "tick", "pump", "complete", "status"], })); }; - let cognition = load_heartbeat_cognition_entry(store_path)?; let scheduler_status = if state.participants.is_empty() { "unconfigured" } else if state.participants.iter().all(|participant| { @@ -104,17 +97,8 @@ pub fn heartbeat_status_projection( "latestEvent": history.last().cloned(), "history": history, "latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit), - "cognitionQuarantine": cognition.as_ref().map(heartbeat_cognition_status_json), - "sleepCycle": cognition.as_ref().and_then(|entry| entry.sleep_cycle.clone()), - "memoryResonance": cognition.as_ref().and_then(|entry| entry.memory_resonance.clone()), - "incubation": cognition.as_ref().and_then(|entry| entry.incubation.clone()), - "thoughtLanes": cognition.as_ref().and_then(|entry| entry.thought_lanes.clone()), - "bridge": cognition.as_ref().and_then(|entry| entry.bridge.clone()), - "candidateInterventions": cognition.as_ref().and_then(|entry| entry.candidate_interventions.clone()), - "appraisals": cognition.as_ref().and_then(|entry| entry.appraisals.clone()), - "reactions": cognition.as_ref().and_then(|entry| entry.reactions.clone()), "adaptivePacing": state.adaptive_pacing.as_ref(), - "availableActions": ["init", "tick", "pump", "heat", "complete", "status", "routine"], + "availableActions": ["init", "tick", "pump", "heat", "complete", "status"], })) } @@ -156,18 +140,6 @@ fn latest_json_artifacts(artifact_dir: impl AsRef, limit: usize) -> Vec Value { - if payload.get("schema_version") - == Some(&Value::String(VOID_ROUTINE_SCHEMA_VERSION.to_string())) - { - return serde_json::json!({ - "runId": payload.get("runId"), - "isNapping": payload.pointer("/sleepCycle/isNapping"), - "resonanceCount": payload.pointer("/memoryResonance/pairs").and_then(Value::as_array).map(Vec::len), - "incubationCount": payload.pointer("/incubation/themes").and_then(Value::as_array).map(Vec::len), - "appraisalCount": payload.pointer("/appraisals/participantAppraisals").and_then(Value::as_array).map(Vec::len), - "reactionCount": payload.pointer("/reactions/reactions").and_then(Value::as_array).map(Vec::len), - }); - } let event = if payload.get("actionId").is_some() { Some(payload) } else { @@ -213,19 +185,6 @@ fn cultcache_status(store_path: &Path) -> Value { "modifiedAt": modified_at, "entryType": HEARTBEAT_STATE_TYPE, "entryKey": HEARTBEAT_STATE_KEY, - "cognitionEntryType": HEARTBEAT_COGNITION_TYPE, - "cognitionEntryKey": HEARTBEAT_COGNITION_KEY, - }) -} - -fn heartbeat_cognition_status_json(cognition: &super::EpiphanyHeartbeatCognitionEntry) -> Value { - serde_json::json!({ - "schema_version": cognition.schema_version, - "updatedAt": cognition.updated_at, - "latestRunId": cognition.latest_run_id, - "latestArtifactRef": cognition.latest_artifact_ref, - "source": cognition.source, - "contract": "Experimental Void/Ghostlight thought-weather is quarantined outside stable heartbeat scheduling state. Status may project it for inspection, but scheduler policy owns only typed timing physiology.", }) } @@ -237,8 +196,6 @@ fn participant_status_json(participant: &HeartbeatParticipant) -> Value { "arena": participant_arena(participant), "participantKind": participant_kind(participant), "initiativeSpeed": participant.initiative_speed, - "personalityCooldownMultiplier": personality_cooldown_multiplier(participant), - "moodCooldownMultiplier": mood_cooldown_multiplier(participant), "initiativeHeatMultiplier": initiative_heat_multiplier(participant), "initiativeHeat": participant.initiative_heat.as_ref(), "effectiveCooldownMultiplier": effective_cooldown_multiplier(participant), @@ -250,8 +207,6 @@ fn participant_status_json(participant: &HeartbeatParticipant) -> Value { .pending_turn .as_ref() .and_then(|turn| turn.initiative_freeze_reason.as_ref()), - "personalityTiming": participant.personality_timing.as_ref(), - "moodTiming": participant.mood_timing.as_ref(), "nextReadyAt": participant.next_ready_at, "reactionBias": participant.reaction_bias, "interruptThreshold": participant.interrupt_threshold, @@ -272,8 +227,6 @@ pub(super) fn schedule_participant_json(participant: &HeartbeatParticipant) -> V "arena": participant_arena(participant), "participant_kind": participant_kind(participant), "initiative_speed": participant.initiative_speed, - "personality_cooldown_multiplier": personality_cooldown_multiplier(participant), - "mood_cooldown_multiplier": mood_cooldown_multiplier(participant), "initiative_heat_multiplier": initiative_heat_multiplier(participant), "effective_cooldown_multiplier": effective_cooldown_multiplier(participant), "initiative_frozen": participant @@ -314,8 +267,6 @@ pub(super) fn pending_turn_json(turn: &HeartbeatPendingTurn) -> Value { "startedAt": turn.started_at, "startedSceneClock": turn.started_scene_clock, "baseRecovery": turn.base_recovery, - "personalityCooldownMultiplier": turn.personality_cooldown_multiplier, - "moodCooldownMultiplier": turn.mood_cooldown_multiplier, "initiativeHeatMultiplier": turn.initiative_heat_multiplier, "effectiveCooldownMultiplier": turn.effective_cooldown_multiplier, "initiativeFrozen": turn.initiative_frozen, diff --git a/epiphany-core/src/heartbeat_state/heartbeat_retention.rs b/epiphany-core/src/heartbeat_state/heartbeat_retention.rs index 4fe20cf0..f5d8f392 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_retention.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_retention.rs @@ -3,7 +3,7 @@ use super::{ 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, load_heartbeat_cognition_entry, + heartbeat_state_cache, }; use anyhow::{Context, Result, anyhow, bail}; use cultcache_rs::{DatabaseEntry, SingleFileMessagePackBackingStore}; @@ -53,16 +53,11 @@ pub fn retain_heartbeat_pulse_artifacts( return Ok(None); } let retire_count = batch_size.min(pulses.len() - retain_pulse_count); - let protected = protected_latest_cognition_pulse(store_path.as_ref(), &artifact_root)?; let members = pulses .iter() - .filter(|(_, path)| protected.as_deref() != path.file_name().and_then(|name| name.to_str())) .take(retire_count) .map(|(_, path)| artifact_member(&artifact_root, path)) .collect::>>()?; - if members.len() != retire_count { - bail!("heartbeat artifact retention cannot fill a batch without current cognition"); - } 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(), @@ -79,30 +74,6 @@ pub fn retain_heartbeat_pulse_artifacts( reconcile_retention_plan(store_path.as_ref(), &artifact_root, &plan, completed_at_utc).map(Some) } -fn protected_latest_cognition_pulse( - store_path: &Path, - artifact_root: &Path, -) -> Result> { - let Some(reference) = - load_heartbeat_cognition_entry(store_path)?.and_then(|entry| entry.latest_artifact_ref) - else { - return Ok(None); - }; - let path = PathBuf::from(reference); - if !path.exists() { - return Ok(None); - } - let canonical = path.canonicalize()?; - let Ok(relative) = canonical.strip_prefix(artifact_root) else { - return Ok(None); - }; - let Some(first) = relative.components().next() else { - return Ok(None); - }; - let name = first.as_os_str().to_string_lossy().to_string(); - Ok(pulse_sequence(&name).is_some().then_some(name)) -} - fn pending_retention_plan( store_path: &Path, ) -> Result> { @@ -409,10 +380,7 @@ fn write_retention_receipt( #[cfg(test)] mod tests { use super::*; - use crate::{ - EpiphanyHeartbeatCognitionEntry, HEARTBEAT_COGNITION_SCHEMA_VERSION, - initialize_heartbeat_store, write_heartbeat_cognition_entry, - }; + use crate::initialize_heartbeat_store; fn pulse(root: &Path, sequence: u64, content: &str) -> Result { let path = root.join(format!("pulse-{sequence:06}")); @@ -501,48 +469,4 @@ mod tests { assert!(artifacts.join("pulse-000001").exists()); Ok(()) } - - #[test] - fn latest_cognition_artifact_is_never_retired() -> 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 protected = pulse(&artifacts, 1, "protected")?; - for sequence in 2..=5 { - pulse(&artifacts, sequence, "stable")?; - } - write_heartbeat_cognition_entry( - &store, - &EpiphanyHeartbeatCognitionEntry { - schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(), - updated_at: "2026-08-08T21:03:00Z".to_string(), - latest_run_id: Some("protected-run".to_string()), - latest_artifact_ref: Some( - protected - .join("nested") - .join("artifact.json") - .display() - .to_string(), - ), - source: Some("test".to_string()), - sleep_cycle: None, - memory_resonance: None, - incubation: None, - thought_lanes: None, - bridge: None, - candidate_interventions: None, - appraisals: None, - reactions: None, - }, - )?; - - let receipt = - retain_heartbeat_pulse_artifacts(&store, &artifacts, 2, 2, "2026-08-08T21:03:01Z")? - .expect("retention should skip protected current cognition"); - assert!(!receipt.deleted_directories.contains(&"pulse-000001".into())); - assert!(protected.exists()); - Ok(()) - } } diff --git a/epiphany-core/src/heartbeat_state/heartbeat_roles.rs b/epiphany-core/src/heartbeat_state/heartbeat_roles.rs index 8c8b55fe..52814692 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_roles.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_roles.rs @@ -129,13 +129,8 @@ pub(super) fn default_participant(role_id: &str) -> HeartbeatParticipant { pending_turn: None, scene_id: None, groups: Vec::new(), - personality_cooldown_multiplier: 1.0, - mood_cooldown_multiplier: 1.0, initiative_heat_multiplier: 1.0, initiative_heat: None, - personality_timing: None, - mood_timing: None, - birth_personality_seed: None, } } @@ -163,13 +158,8 @@ fn ghostlight_scene_participant( pending_turn: None, scene_id: Some(scene_id.to_string()), groups: Vec::new(), - personality_cooldown_multiplier: 1.0, - mood_cooldown_multiplier: 1.0, initiative_heat_multiplier: 1.0, initiative_heat: None, - personality_timing: None, - mood_timing: None, - birth_personality_seed: None, } } diff --git a/epiphany-core/src/heartbeat_state/heartbeat_store.rs b/epiphany-core/src/heartbeat_state/heartbeat_store.rs index aac3c1d3..3a7dae0a 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_store.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_store.rs @@ -1,71 +1,43 @@ use super::EpiphanyHeartbeatArtifactRetentionPlan; use super::EpiphanyHeartbeatArtifactRetentionReceipt; -use super::EpiphanyHeartbeatCognitionEntry; use super::EpiphanyHeartbeatStaleTurnRepairReceipt; use super::EpiphanyHeartbeatStateEntry; use super::HEARTBEAT_ARENA_MAINTENANCE; use super::HEARTBEAT_ARENA_SCENE; -use super::HEARTBEAT_COGNITION_KEY; -use super::HEARTBEAT_COGNITION_SCHEMA_VERSION; 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::LegacyHeartbeatStateWithCognition; use super::PARTICIPANT_KIND_AGENT; use super::PARTICIPANT_KIND_CHARACTER; use super::PERSONA_CONVERSATION_RETENTION_HEAD_SCHEMA_VERSION; use super::PERSONA_CONVERSATION_RETENTION_PLAN_SCHEMA_VERSION; use super::PERSONA_TURN_REQUEST_SCHEMA_VERSION; use super::PERSONA_TURN_TERMINAL_RECEIPT_SCHEMA_VERSION; -use super::now_iso; 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 serde_json::Value; 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::()?; cache.register_entry_type::()?; - import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref(), false)?; + import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref())?; Ok(cache) } -fn legacy_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::()?; - cache.register_entry_type::()?; - import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref(), true)?; - Ok(cache) -} - -fn import_owned_heartbeat_envelopes( - cache: &mut CultCache, - store_path: &Path, - legacy: bool, -) -> Result<()> { - let state_type = if legacy { - ::TYPE - } else { - ::TYPE - }; +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 - && envelope.key == HEARTBEAT_COGNITION_KEY) || envelope.r#type == ::TYPE; let owned = owned || envelope.r#type == ::TYPE @@ -82,15 +54,7 @@ fn import_owned_heartbeat_envelopes( )); } if envelope.r#type == state_type { - if legacy { - // The legacy and current state documents intentionally share a - // polymorphic identity. A current payload is not legacy cargo. - let _ = cache.load_envelope::(envelope); - } else { - cache.load_envelope::(envelope)?; - } - } else if envelope.r#type == ::TYPE { - cache.load_envelope::(envelope)?; + cache.load_envelope::(envelope)?; } else if envelope.r#type == ::TYPE { cache.load_envelope::(envelope)?; @@ -184,34 +148,6 @@ pub fn commit_heartbeat_state_transaction( Ok(()) } -pub fn load_heartbeat_cognition_entry( - store_path: impl AsRef, -) -> Result> { - let store_path = store_path.as_ref(); - let cache = heartbeat_state_cache(store_path)?; - if let Some(cognition) = - cache.get::(HEARTBEAT_COGNITION_KEY)? - { - return Ok(Some(cognition)); - } - let legacy_cache = legacy_heartbeat_state_cache(store_path)?; - let legacy = match legacy_cache.get::(HEARTBEAT_STATE_KEY) { - Ok(Some(legacy)) => legacy, - Ok(None) | Err(_) => return Ok(None), - }; - legacy_heartbeat_cognition_entry(legacy) -} - -pub fn write_heartbeat_cognition_entry( - store_path: impl AsRef, - cognition: &EpiphanyHeartbeatCognitionEntry, -) -> Result { - validate_heartbeat_cognition(cognition)?; - let store_path = store_path.as_ref(); - let cache = heartbeat_state_cache(store_path)?; - commit_owned_entry(store_path, &cache, HEARTBEAT_COGNITION_KEY, cognition) -} - pub fn write_heartbeat_stale_turn_repair_receipt( store_path: impl AsRef, receipt: &EpiphanyHeartbeatStaleTurnRepairReceipt, @@ -290,72 +226,6 @@ pub fn validate_heartbeat_stale_turn_repair_receipt( Ok(()) } -pub fn validate_heartbeat_cognition(cognition: &EpiphanyHeartbeatCognitionEntry) -> Result<()> { - if cognition.schema_version != HEARTBEAT_COGNITION_SCHEMA_VERSION { - return Err(anyhow!( - "heartbeat cognition schema_version is {:?}, expected {:?}", - cognition.schema_version, - HEARTBEAT_COGNITION_SCHEMA_VERSION - )); - } - Ok(()) -} - -fn legacy_heartbeat_cognition_entry( - legacy: LegacyHeartbeatStateWithCognition, -) -> Result> { - if legacy.sleep_cycle.is_none() - && legacy.memory_resonance.is_none() - && legacy.incubation.is_none() - && legacy.thought_lanes.is_none() - && legacy.bridge.is_none() - && legacy.candidate_interventions.is_none() - && legacy.appraisals.is_none() - && legacy.reactions.is_none() - { - return Ok(None); - } - let routine = legacy.extra.get("voidRoutine"); - let latest_run_id = routine - .and_then(|value| value.get("lastRunId")) - .and_then(Value::as_str) - .map(str::to_string); - let source = routine - .and_then(|value| value.get("source")) - .and_then(Value::as_str) - .map(str::to_string); - let updated_at = routine - .and_then(|value| value.get("lastRunAt")) - .and_then(Value::as_str) - .map(str::to_string) - .unwrap_or_else(now_iso); - Ok(Some(EpiphanyHeartbeatCognitionEntry { - schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(), - updated_at, - latest_run_id, - latest_artifact_ref: None, - source, - sleep_cycle: decode_legacy_document(legacy.sleep_cycle)?, - memory_resonance: decode_legacy_document(legacy.memory_resonance)?, - incubation: decode_legacy_document(legacy.incubation)?, - thought_lanes: decode_legacy_document(legacy.thought_lanes)?, - bridge: decode_legacy_document(legacy.bridge)?, - candidate_interventions: decode_legacy_document(legacy.candidate_interventions)?, - appraisals: decode_legacy_document(legacy.appraisals)?, - reactions: decode_legacy_document(legacy.reactions)?, - })) -} - -fn decode_legacy_document(value: Option) -> Result> -where - T: serde::de::DeserializeOwned, -{ - value - .map(serde_json::from_value) - .transpose() - .map_err(Into::into) -} - pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<()> { if state.schema_version != HEARTBEAT_STATE_SCHEMA_VERSION { return Err(anyhow!( @@ -608,7 +478,7 @@ mod tests { } #[test] - fn round_trips_state_and_cognition_documents() -> Result<()> { + 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); @@ -620,31 +490,6 @@ mod tests { assert_eq!(loaded.schema_version, HEARTBEAT_STATE_SCHEMA_VERSION); assert_eq!(loaded.participants.len(), state.participants.len()); - let cognition = EpiphanyHeartbeatCognitionEntry { - schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(), - updated_at: "2026-05-17T00:00:00Z".to_string(), - latest_run_id: Some("run-1".to_string()), - latest_artifact_ref: None, - source: Some("unit-test".to_string()), - sleep_cycle: None, - memory_resonance: None, - incubation: None, - thought_lanes: None, - bridge: None, - candidate_interventions: None, - appraisals: None, - reactions: None, - }; - - write_heartbeat_cognition_entry(&store_path, &cognition)?; - let loaded_cognition = load_heartbeat_cognition_entry(&store_path)? - .expect("heartbeat cognition should round-trip through CultCache"); - - assert_eq!( - loaded_cognition.schema_version, - HEARTBEAT_COGNITION_SCHEMA_VERSION - ); - assert_eq!(loaded_cognition.latest_run_id.as_deref(), Some("run-1")); Ok(()) } diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 2c3faa6b..bfc88a61 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -543,7 +543,6 @@ 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::EpiphanyHeartbeatCognitionEntry; pub use heartbeat_state::EpiphanyHeartbeatStaleTurnRepairReceipt; pub use heartbeat_state::EpiphanyHeartbeatStateEntry; pub use heartbeat_state::GhostlightSceneParticipantSeed; @@ -553,35 +552,22 @@ 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_COGNITION_KEY; -pub use heartbeat_state::HEARTBEAT_COGNITION_SCHEMA_VERSION; -pub use heartbeat_state::HEARTBEAT_COGNITION_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::HeartbeatAgentReaction; -pub use heartbeat_state::HeartbeatAgentThoughtAppraisal; -pub use heartbeat_state::HeartbeatAppraisalReview; pub use heartbeat_state::HeartbeatArtifactRetentionMember; -pub use heartbeat_state::HeartbeatBirthPersonalitySeed; -pub use heartbeat_state::HeartbeatCandidateImplications; pub use heartbeat_state::HeartbeatCompleteOptions; -pub use heartbeat_state::HeartbeatEmotionalAppraisal; pub use heartbeat_state::HeartbeatHeatUpdateOptions; pub use heartbeat_state::HeartbeatHistoryEvent; pub use heartbeat_state::HeartbeatInitiativeHeatPolicy; pub use heartbeat_state::HeartbeatInitiativeMultiplier; -pub use heartbeat_state::HeartbeatMoodDimension; -pub use heartbeat_state::HeartbeatMoodTiming; pub use heartbeat_state::HeartbeatPacingPolicy; pub use heartbeat_state::HeartbeatParticipant; -pub use heartbeat_state::HeartbeatParticipantLocalContext; pub use heartbeat_state::HeartbeatPendingMention; pub use heartbeat_state::HeartbeatPendingTurn; -pub use heartbeat_state::HeartbeatPersonalityProjection; pub use heartbeat_state::HeartbeatPumpOptions; pub use heartbeat_state::HeartbeatQueueMentionOptions; pub use heartbeat_state::HeartbeatSelectionPolicy; @@ -593,7 +579,6 @@ pub use heartbeat_state::PersonaTurnRequest; pub use heartbeat_state::PersonaTurnTerminalOptions; pub use heartbeat_state::PersonaTurnTerminalReceipt; pub use heartbeat_state::ResidentSelfHeartbeatPulse; -pub use heartbeat_state::VoidRoutineOptions; pub use heartbeat_state::complete_heartbeat_store; pub use heartbeat_state::complete_persona_turn_request_store; pub use heartbeat_state::default_heartbeat_state; @@ -602,7 +587,6 @@ pub use heartbeat_state::heartbeat_state_cache; pub use heartbeat_state::heartbeat_status_projection; pub use heartbeat_state::initialize_ghostlight_scene_heartbeat_store; pub use heartbeat_state::initialize_heartbeat_store; -pub use heartbeat_state::load_heartbeat_cognition_entry; pub use heartbeat_state::load_heartbeat_state_entry; pub use heartbeat_state::load_latest_heartbeat_stale_turn_repair_receipt; pub use heartbeat_state::pulse_persona_heartbeat; @@ -612,12 +596,9 @@ pub use heartbeat_state::queue_heartbeat_pending_mention_store; 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::run_void_routine_store; pub use heartbeat_state::tick_heartbeat_store; pub use heartbeat_state::update_heartbeat_heat_store; -pub use heartbeat_state::validate_heartbeat_cognition; pub use heartbeat_state::validate_heartbeat_state; -pub use heartbeat_state::write_heartbeat_cognition_entry; pub use heartbeat_state::write_heartbeat_stale_turn_repair_receipt; pub use heartbeat_state::write_heartbeat_state_entry; pub use host_identity::{ @@ -718,7 +699,6 @@ pub use memory_graph::load_memory_semantic_projection_readiness; pub use memory_graph::load_memory_semantic_projection_success; pub use memory_graph::memory_graph_domain_id; pub use memory_graph::memory_graph_edge_id; -pub use memory_graph::memory_graph_from_heartbeat_cognition; pub use memory_graph::memory_graph_model_hash; pub use memory_graph::memory_graph_node_id; pub use memory_graph::memory_semantic_projection_query_eligible; diff --git a/epiphany-core/src/memory_graph/mod.rs b/epiphany-core/src/memory_graph/mod.rs index 90d96f37..2bdd3a01 100644 --- a/epiphany-core/src/memory_graph/mod.rs +++ b/epiphany-core/src/memory_graph/mod.rs @@ -38,7 +38,6 @@ pub use freshness::derive_memory_graph_freshness; pub use ids::memory_graph_domain_id; pub use ids::memory_graph_edge_id; pub use ids::memory_graph_node_id; -pub use profiles::memory_graph_from_heartbeat_cognition; pub use semantic_index::*; pub use semantic_projection::*; pub use semantic_projector::*; diff --git a/epiphany-core/src/memory_graph/profiles/heartbeat.rs b/epiphany-core/src/memory_graph/profiles/heartbeat.rs deleted file mode 100644 index 2d42c584..00000000 --- a/epiphany-core/src/memory_graph/profiles/heartbeat.rs +++ /dev/null @@ -1,479 +0,0 @@ -use crate::heartbeat_state::EpiphanyHeartbeatCognitionEntry; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatBridgeDecision; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatBridgeSynthesis; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatBridgeTension; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatCandidateIntervention; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatCandidateInterventions; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatCognitionBridge; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatIncubation; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatIncubationTheme; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatMemoryResonance; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatMemoryResonancePair; -#[cfg(test)] -use crate::heartbeat_state::HeartbeatSourceCoverage; -use crate::memory_graph::EpiphanyMemoryDomain; -use crate::memory_graph::EpiphanyMemoryGraphSnapshot; -use crate::memory_graph::EpiphanyMemoryLifecycle; -use crate::memory_graph::EpiphanyMemoryNode; -use crate::memory_graph::EpiphanyMemoryNodeKind; -use crate::memory_graph::EpiphanyMemoryProfile; -use crate::memory_graph::EpiphanyMemorySummary; -use crate::memory_graph::memory_graph_domain_id; -use crate::memory_graph::memory_graph_node_id; - -pub fn memory_graph_from_heartbeat_cognition( - graph_id: impl Into, - cognition: &EpiphanyHeartbeatCognitionEntry, -) -> EpiphanyMemoryGraphSnapshot { - let mut domains = Vec::new(); - let mut nodes = Vec::new(); - let mut summaries = Vec::new(); - - import_resonance(cognition, &mut domains, &mut nodes, &mut summaries); - import_incubation(cognition, &mut domains, &mut nodes, &mut summaries); - import_candidate_interventions(cognition, &mut domains, &mut nodes, &mut summaries); - import_bridge_pressure(cognition, &mut domains, &mut nodes, &mut summaries); - - EpiphanyMemoryGraphSnapshot { - schema_version: Some("epiphany.memory_graph.v1".to_string()), - graph_id: graph_id.into(), - domains, - nodes, - summaries, - freshness: Some(crate::memory_graph::EpiphanyMemoryFreshness { - status: crate::memory_graph::EpiphanyMemoryFreshnessStatus::Ready, - note: Some( - "Imported from heartbeat cognition as provisional memory pressure.".to_string(), - ), - ..Default::default() - }), - ..Default::default() - } -} - -fn import_resonance( - cognition: &EpiphanyHeartbeatCognitionEntry, - domains: &mut Vec, - nodes: &mut Vec, - summaries: &mut Vec, -) { - let Some(resonance) = cognition.memory_resonance.as_ref() else { - return; - }; - let domain_id = push_domain( - domains, - EpiphanyMemoryProfile::ShortTerm, - "heartbeat resonance", - ); - let before = nodes.len(); - for pair in resonance.pairs.iter().take(8) { - let left = pair.left_summary.clone(); - let right = pair.right_summary.clone(); - let left_role = pair.left_role.clone(); - let right_role = pair.right_role.clone(); - let id_seed = format!("{}:{}:{}", left_role, right_role, pair.left_memory_id); - nodes.push(EpiphanyMemoryNode { - id: memory_graph_node_id(&domain_id, "resonance", id_seed, None), - domain_id: domain_id.clone(), - profile: EpiphanyMemoryProfile::ShortTerm, - kind: EpiphanyMemoryNodeKind::ShortTermThought, - title: format!("{left_role}/{right_role} resonance"), - claim: format!("{left} / {right}"), - question: "Is this resonance signal, stale echo, or a branch worth following?" - .to_string(), - tension: "Shared vocabulary is not proof of shared truth.".to_string(), - action_implication: - "Treat as provisional attention pressure until sleep or review accounts for it." - .to_string(), - source_hashes: vec!["anchor:missing".to_string()], - lifecycle: EpiphanyMemoryLifecycle::Active, - salience: score_to_u32(pair.strength), - confidence: 55, - ..Default::default() - }); - } - push_summary( - summaries, - &domain_id, - "summary-heartbeat-resonance", - "heartbeat resonance", - "Heartbeat resonance produced provisional short-term thought nodes.", - &nodes[before..], - EpiphanyMemoryProfile::ShortTerm, - ); -} - -fn import_incubation( - cognition: &EpiphanyHeartbeatCognitionEntry, - domains: &mut Vec, - nodes: &mut Vec, - summaries: &mut Vec, -) { - let Some(incubation) = cognition.incubation.as_ref() else { - return; - }; - let domain_id = push_domain( - domains, - EpiphanyMemoryProfile::Incubation, - "heartbeat incubation", - ); - let before = nodes.len(); - for theme in incubation.themes.iter().take(12) { - let theme_id = theme.theme_id.clone(); - let status = theme.status.clone(); - nodes.push(EpiphanyMemoryNode { - id: memory_graph_node_id(&domain_id, "incubation", &theme_id, None), - domain_id: domain_id.clone(), - profile: EpiphanyMemoryProfile::Incubation, - kind: EpiphanyMemoryNodeKind::IncubationThread, - title: theme_id.clone(), - claim: theme.summary.clone(), - question: if theme.latent_question.is_empty() { - "What is this incubation theme trying to become?".to_string() - } else { - theme.latent_question.clone() - }, - tension: if theme.holding_close_because.is_empty() { - "Heartbeat marked this theme but did not preserve a tension.".to_string() - } else { - theme.holding_close_because.clone() - }, - action_implication: if theme.why_it_pulls.is_empty() { - "Keep in incubation until sleep or review accounts for it.".to_string() - } else { - theme.why_it_pulls.clone() - }, - source_hashes: vec!["anchor:missing".to_string()], - lifecycle: incubation_lifecycle(&status), - salience: score_to_u32(theme.priority_score), - confidence: score_to_u32(theme.maturation), - ..Default::default() - }); - } - push_summary( - summaries, - &domain_id, - "summary-heartbeat-incubation", - "heartbeat incubation", - "Heartbeat incubation produced provisional incubation threads.", - &nodes[before..], - EpiphanyMemoryProfile::Incubation, - ); -} - -fn import_candidate_interventions( - cognition: &EpiphanyHeartbeatCognitionEntry, - domains: &mut Vec, - nodes: &mut Vec, - summaries: &mut Vec, -) { - let Some(candidates) = cognition.candidate_interventions.as_ref() else { - return; - }; - let domain_id = push_domain( - domains, - EpiphanyMemoryProfile::CandidateIntervention, - "heartbeat candidate interventions", - ); - let before = nodes.len(); - for item in candidates.items.iter().take(8) { - let intervention_id = item.intervention_id.clone(); - nodes.push(EpiphanyMemoryNode { - id: memory_graph_node_id(&domain_id, "candidate", &intervention_id, None), - domain_id: domain_id.clone(), - profile: EpiphanyMemoryProfile::CandidateIntervention, - kind: EpiphanyMemoryNodeKind::CandidateIntervention, - title: item.summary.clone(), - claim: item.draft.clone(), - question: "Should this candidate intervention be spoken, deferred, or retired?" - .to_string(), - tension: "Candidate speech is not authority and still requires review.".to_string(), - action_implication: "Route through Persona/coordinator review before public surfacing." - .to_string(), - source_hashes: vec!["anchor:missing".to_string()], - lifecycle: EpiphanyMemoryLifecycle::Queued, - salience: score_to_u32(item.novelty_to_room), - confidence: 55, - ..Default::default() - }); - } - push_summary( - summaries, - &domain_id, - "summary-heartbeat-candidates", - "heartbeat candidate interventions", - "Heartbeat produced review-gated candidate interventions.", - &nodes[before..], - EpiphanyMemoryProfile::CandidateIntervention, - ); -} - -fn import_bridge_pressure( - cognition: &EpiphanyHeartbeatCognitionEntry, - domains: &mut Vec, - nodes: &mut Vec, - summaries: &mut Vec, -) { - let Some(bridge) = cognition.bridge.as_ref() else { - return; - }; - let decision = bridge.decision.speak_decision.as_str(); - if decision == "silence" { - return; - } - let domain_id = push_domain( - domains, - EpiphanyMemoryProfile::AgencyPressure, - "heartbeat bridge pressure", - ); - let reason = if bridge.decision.reason.is_empty() { - "Bridge pressure has no explicit reason." - } else { - bridge.decision.reason.as_str() - }; - let node = EpiphanyMemoryNode { - id: memory_graph_node_id(&domain_id, "bridge-pressure", decision, None), - domain_id: domain_id.clone(), - profile: EpiphanyMemoryProfile::AgencyPressure, - kind: EpiphanyMemoryNodeKind::AgencyPressure, - title: format!("bridge decision: {decision}"), - claim: reason.to_string(), - question: "Does this bridge pressure require action, cooling, or silence?".to_string(), - tension: "Bridge pressure can steer attention but cannot mutate durable truth.".to_string(), - action_implication: "Coordinator or Persona may review this pressure; no automatic speech or state write follows.".to_string(), - source_hashes: vec!["anchor:missing".to_string()], - lifecycle: if decision == "draft" { - EpiphanyMemoryLifecycle::Obligated - } else { - EpiphanyMemoryLifecycle::Active - }, - salience: 65, - confidence: 55, - ..Default::default() - }; - nodes.push(node); - push_summary( - summaries, - &domain_id, - "summary-heartbeat-bridge-pressure", - "heartbeat bridge pressure", - "Heartbeat bridge produced provisional agency pressure.", - nodes.last().map(std::slice::from_ref).unwrap_or(&[]), - EpiphanyMemoryProfile::AgencyPressure, - ); -} - -fn push_domain( - domains: &mut Vec, - profile: EpiphanyMemoryProfile, - name: &str, -) -> String { - let domain_id = memory_graph_domain_id(profile, "heartbeat", name); - if !domains.iter().any(|domain| domain.id == domain_id) { - domains.push(EpiphanyMemoryDomain { - id: domain_id.clone(), - profile, - title: name.to_string(), - description: Some( - "Heartbeat cognition imported as provisional memory graph pressure.".to_string(), - ), - lifecycle: domain_lifecycle(profile), - }); - } - domain_id -} - -fn domain_lifecycle(profile: EpiphanyMemoryProfile) -> EpiphanyMemoryLifecycle { - match profile { - EpiphanyMemoryProfile::CandidateIntervention => EpiphanyMemoryLifecycle::Queued, - _ => EpiphanyMemoryLifecycle::Active, - } -} - -fn push_summary( - summaries: &mut Vec, - domain_id: &str, - id: &str, - target: &str, - claim: &str, - nodes: &[EpiphanyMemoryNode], - profile: EpiphanyMemoryProfile, -) { - if nodes.is_empty() { - return; - } - summaries.push(EpiphanyMemorySummary { - id: id.to_string(), - domain_id: domain_id.to_string(), - covers_node_ids: nodes.iter().map(|node| node.id.clone()).collect(), - target: target.to_string(), - claim: claim.to_string(), - question: "What should sleep or review do with this provisional pressure?".to_string(), - tension: "Heartbeat cognition is provisional physiology, not durable doctrine.".to_string(), - action_implication: "Do not promote without a later sleep/review operation.".to_string(), - anchor_count: 0, - freshness: crate::memory_graph::EpiphanyMemoryFreshnessStatus::Ready, - confidence: if profile == EpiphanyMemoryProfile::CandidateIntervention { - 60 - } else { - 55 - }, - ..Default::default() - }); -} - -fn score_to_u32(value: f64) -> u32 { - if value.is_finite() { - (value.clamp(0.0, 1.0) * 100.0).round() as u32 - } else { - 0 - } -} - -fn incubation_lifecycle(status: &str) -> EpiphanyMemoryLifecycle { - match status { - "ripe" | "deepening" => EpiphanyMemoryLifecycle::Deepening, - "refractory" | "stalled" => EpiphanyMemoryLifecycle::Cooling, - _ => EpiphanyMemoryLifecycle::Active, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::heartbeat_state::HEARTBEAT_COGNITION_SCHEMA_VERSION; - use crate::memory_graph::validate_memory_graph_snapshot; - - #[test] - fn heartbeat_profile_imports_provisional_pressure_without_promoting_it() { - let cognition = EpiphanyHeartbeatCognitionEntry { - schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(), - updated_at: "2026-05-18T00:00:00Z".to_string(), - latest_run_id: Some("run-1".to_string()), - latest_artifact_ref: None, - source: Some("unit-test".to_string()), - sleep_cycle: None, - memory_resonance: Some(HeartbeatMemoryResonance { - schema_version: "epiphany.memory_resonance.v0".to_string(), - updated_at: "2026-05-18T00:00:00Z".to_string(), - source: "unit-test".to_string(), - record_count: 2, - pairs: vec![HeartbeatMemoryResonancePair { - left_role: "body".to_string(), - left_memory_id: "mem-a".to_string(), - left_memory_kind: "note".to_string(), - left_summary: "Architecture needs anchors.".to_string(), - right_role: "soul".to_string(), - right_memory_id: "mem-b".to_string(), - right_memory_kind: "note".to_string(), - right_summary: "Verification needs receipts.".to_string(), - strength: 0.8, - shared_tokens: vec!["evidence".to_string()], - source_roles: vec!["body".to_string(), "soul".to_string()], - source_kinds: vec!["note".to_string()], - evidence_refs: vec!["mem-a".to_string(), "mem-b".to_string()], - }], - }), - incubation: Some(HeartbeatIncubation { - schema_version: "epiphany.incubation.v0".to_string(), - updated_at: "2026-05-18T00:00:00Z".to_string(), - source_coverage: HeartbeatSourceCoverage::default(), - last_incubation_summary: "Modeling/Soul evidence seam is live.".to_string(), - themes: vec![ - HeartbeatIncubationTheme { - theme_id: "theme-body-soul".to_string(), - summary: "Modeling and Soul keep touching evidence boundaries.".to_string(), - latent_question: "Should this become doctrine?".to_string(), - holding_close_because: "It is live but not settled.".to_string(), - why_it_pulls: "It affects future review gates.".to_string(), - status: "deepening".to_string(), - priority_score: 0.72, - maturation: 0.6, - ..Default::default() - }, - HeartbeatIncubationTheme { - theme_id: "theme-live-store-shape".to_string(), - summary: "Live heartbeat stores may omit optional explanatory fields." - .to_string(), - status: "deepening".to_string(), - ..Default::default() - }, - ], - }), - thought_lanes: None, - bridge: Some(HeartbeatCognitionBridge { - schema_version: "epiphany.cognition_bridge.v0".to_string(), - updated_at: "2026-05-18T00:00:00Z".to_string(), - decision: HeartbeatBridgeDecision { - speak_decision: "hold".to_string(), - reason: "The thought is live but not ready for speech.".to_string(), - ..Default::default() - }, - recent_syntheses: vec![HeartbeatBridgeSynthesis { - summary: "The thought is live but not ready for speech.".to_string(), - ..Default::default() - }], - unresolved_tensions: vec![HeartbeatBridgeTension { - topic: "thought authority boundary".to_string(), - summary: "Bridge pressure is not authority.".to_string(), - opened_at: "2026-05-18T00:00:00Z".to_string(), - }], - ..Default::default() - }), - candidate_interventions: Some(HeartbeatCandidateInterventions { - schema_version: "epiphany.candidate_interventions.v0".to_string(), - updated_at: "2026-05-18T00:00:00Z".to_string(), - items: vec![HeartbeatCandidateIntervention { - intervention_id: "candidate-theme-body-soul".to_string(), - summary: "Possible note".to_string(), - draft: "Evidence boundaries are lighting up.".to_string(), - decision: "draft".to_string(), - requires_persona: true, - requires_review: true, - novelty_to_room: 0.7, - saturation_score: 0.1, - created_at: "2026-05-18T00:00:00Z".to_string(), - }], - }), - appraisals: None, - reactions: None, - }; - - let snapshot = memory_graph_from_heartbeat_cognition("heartbeat-profile", &cognition); - let errors = validate_memory_graph_snapshot(&snapshot); - - assert!(errors.is_empty(), "{errors:?}"); - assert!(snapshot.nodes.iter().any(|node| { - node.profile == EpiphanyMemoryProfile::ShortTerm - && node.lifecycle == EpiphanyMemoryLifecycle::Active - })); - assert!(snapshot.nodes.iter().any(|node| { - node.profile == EpiphanyMemoryProfile::Incubation - && node.lifecycle == EpiphanyMemoryLifecycle::Deepening - })); - assert!(snapshot.nodes.iter().any(|node| { - node.profile == EpiphanyMemoryProfile::CandidateIntervention - && node.lifecycle == EpiphanyMemoryLifecycle::Queued - })); - assert!(snapshot.nodes.iter().any(|node| { - node.profile == EpiphanyMemoryProfile::AgencyPressure - && node.lifecycle == EpiphanyMemoryLifecycle::Active - })); - assert!( - snapshot - .nodes - .iter() - .all(|node| node.lifecycle != EpiphanyMemoryLifecycle::Promoted) - ); - } -} diff --git a/epiphany-core/src/memory_graph/profiles/mod.rs b/epiphany-core/src/memory_graph/profiles/mod.rs index 0aabbf8d..8b137891 100644 --- a/epiphany-core/src/memory_graph/profiles/mod.rs +++ b/epiphany-core/src/memory_graph/profiles/mod.rs @@ -1,3 +1 @@ -pub mod heartbeat; -pub use heartbeat::memory_graph_from_heartbeat_cognition;