Delete heartbeat cognition simulation

This commit is contained in:
Meta Vens 2026-08-23 03:18:21 +02:00
commit d1685df830
13 changed files with 190 additions and 4579 deletions

View file

@ -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::<Vec<_>>();
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<f64>,
situational: &BTreeMap<String, GhostlightTraitVector>,
arousal: f64,
_situational: &BTreeMap<String, GhostlightTraitVector>,
) -> 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()
};

View file

@ -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<String> = None;
let mut action_id: Option<String> = None;
let mut limit = 8_usize;
let mut agent_store: Option<PathBuf> = None;
let mut resident_self_store: Option<PathBuf> = None;
let mut resident_runtime_store: Option<PathBuf> = 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::<GhostlightSceneParticipantSeed>::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 <path> --artifact-dir <path> [--retain-pulse-artifacts <n>] [--retention-batch-size <n>]\n",
" epiphany-heartbeat-store queue-mention --store <path> [options]\n",
" epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n",
" epiphany-heartbeat-store routine --store <path> --artifact-dir <path> [options]\n",
" epiphany-heartbeat-store serve --store <path> --artifact-dir <path> [--local-verse-store <path>] [--resident-self-store <path> --resident-runtime-store <path>] [--agent-store <path>] [--source <source>] [--no-dream] [--interval-seconds <n>] [--max-iterations <n>]\n",
" epiphany-heartbeat-store smoke [--agent-store <path>]"
" epiphany-heartbeat-store serve --store <path> --artifact-dir <path> [--local-verse-store <path>] [--resident-self-store <path> --resident-runtime-store <path>] [--interval-seconds <n>] [--max-iterations <n>]"
)))
}
@ -1034,538 +930,3 @@ fn parse_scene_participant(raw: &str) -> Result<GhostlightSceneParticipantSeed>
.collect(),
})
}
fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
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::<Vec<_>>();
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<String> {
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::<Vec<_>>();
if !extra_keys.is_empty() {
errors.push(format!(
"readiness snapshot has Ghostlight-incompatible keys: {extra_keys:?}"
));
}
}
}
errors
}
fn scoped_temp_dir(prefix: &str) -> Result<PathBuf> {
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)
}

File diff suppressed because it is too large Load diff

View file

@ -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<PersonaConversationRetentionPlan>,
}
#[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<String>,
#[cultcache(key = 3, default)]
pub latest_artifact_ref: Option<String>,
#[cultcache(key = 4, default)]
pub source: Option<String>,
#[cultcache(key = 5, default)]
pub sleep_cycle: Option<HeartbeatSleepCycle>,
#[cultcache(key = 6, default)]
pub memory_resonance: Option<HeartbeatMemoryResonance>,
#[cultcache(key = 7, default)]
pub incubation: Option<HeartbeatIncubation>,
#[cultcache(key = 8, default)]
pub thought_lanes: Option<HeartbeatThoughtLanes>,
#[cultcache(key = 9, default)]
pub bridge: Option<HeartbeatCognitionBridge>,
#[cultcache(key = 10, default)]
pub candidate_interventions: Option<HeartbeatCandidateInterventions>,
#[cultcache(key = 11, default)]
pub appraisals: Option<HeartbeatAgentThoughtAppraisals>,
#[cultcache(key = 12, default)]
pub reactions: Option<HeartbeatAgentReactions>,
}
#[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<HeartbeatParticipant>,
#[cultcache(key = 6)]
pub(super) history: Vec<HeartbeatHistoryEvent>,
#[cultcache(key = 7, default)]
pub(super) sleep_cycle: Option<Value>,
#[cultcache(key = 8, default)]
pub(super) memory_resonance: Option<Value>,
#[cultcache(key = 9, default)]
pub(super) incubation: Option<Value>,
#[cultcache(key = 10, default)]
pub(super) thought_lanes: Option<Value>,
#[cultcache(key = 11, default)]
pub(super) bridge: Option<Value>,
#[cultcache(key = 12, default)]
pub(super) candidate_interventions: Option<Value>,
#[cultcache(key = 13, default)]
pub(super) appraisals: Option<Value>,
#[cultcache(key = 14, default)]
pub(super) reactions: Option<Value>,
#[cultcache(key = 15, default)]
pub(super) extra: BTreeMap<String, Value>,
#[cultcache(key = 19, default)]
pub(super) pending_mentions: Vec<HeartbeatPendingMention>,
}
#[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<HeartbeatMemoryResonancePair>,
}
#[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<String>,
pub source_roles: Vec<String>,
pub source_kinds: Vec<String>,
pub evidence_refs: Vec<String>,
}
#[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<HeartbeatSourceCoverageRole>,
pub memory_kinds: Vec<HeartbeatSourceCoverageKind>,
}
#[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<HeartbeatIncubationTheme>,
}
#[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<String>,
pub source_kinds: Vec<String>,
pub source_memory_ids: Vec<String>,
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<HeartbeatAnalyticThread>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAssociativeLane {
pub description: String,
pub active_threads: Vec<HeartbeatAssociativeThread>,
}
#[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<String>,
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<HeartbeatBridgeSynthesis>,
pub source_coverage: HeartbeatSourceCoverage,
pub topic_saturation: Vec<HeartbeatTopicSaturation>,
pub refractory_topics: Vec<HeartbeatRefractoryTopic>,
pub unresolved_tensions: Vec<HeartbeatBridgeTension>,
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<String>,
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<String>,
pub current_nap_ends_at: Option<String>,
pub next_nap_starts_at: String,
pub last_dream_at: Option<String>,
pub dream_count_in_current_nap: u64,
pub active_dream_themes: Vec<String>,
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<HeartbeatAgentThoughtAppraisal>,
}
#[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<HeartbeatPersonalityProjection>,
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<String>,
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<HeartbeatAgentReaction>,
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<HeartbeatCandidateIntervention>,
}
#[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<String>,
#[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<HeartbeatInitiativeHeatProjection>,
#[serde(default)]
pub personality_timing: Option<HeartbeatPersonalityTiming>,
#[serde(default)]
pub mood_timing: Option<HeartbeatMoodTiming>,
#[serde(default)]
pub birth_personality_seed: Option<HeartbeatBirthPersonalitySeed>,
}
#[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<String>,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatMoodTiming {
pub schema_version: String,
pub source: Option<String>,
pub cooldown_multiplier: f64,
#[serde(default)]
pub emotional_dimensions: Vec<HeartbeatMoodDimension>,
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<String, f64>,
pub default_mood_pressure: BTreeMap<String, f64>,
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<f64>,
#[serde(rename = "nextReadyAt", default)]
pub next_ready_at: Option<f64>,
#[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<std::path::PathBuf>,
pub resident_self_store: Option<std::path::PathBuf>,
}
@ -1096,7 +552,6 @@ pub struct HeartbeatPumpOptions {
pub target_role: Option<String>,
pub schedule_id: String,
pub source_scene_ref: String,
pub agent_store: Option<std::path::PathBuf>,
pub resident_self_store: Option<std::path::PathBuf>,
}
@ -1144,13 +599,6 @@ pub struct HeartbeatStaleTurnRepairOptions {
pub reason: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VoidRoutineOptions {
pub agent_store: Option<std::path::PathBuf>,
pub source: String,
pub allow_dream: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GhostlightSceneParticipantSeed {
pub agent_id: String,

View file

@ -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 {

View file

@ -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<Path>, limit: usize) -> Vec<Va
}
fn artifact_summary(payload: &Value) -> 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,

View file

@ -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::<Result<Vec<_>>>()?;
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<Option<String>> {
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<Option<EpiphanyHeartbeatArtifactRetentionPlan>> {
@ -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<PathBuf> {
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(())
}
}

View file

@ -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,
}
}

View file

@ -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<Path>) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<EpiphanyHeartbeatStateEntry>()?;
cache.register_entry_type::<EpiphanyHeartbeatCognitionEntry>()?;
cache.register_entry_type::<EpiphanyHeartbeatStaleTurnRepairReceipt>()?;
cache.register_entry_type::<EpiphanyHeartbeatArtifactRetentionPlan>()?;
cache.register_entry_type::<EpiphanyHeartbeatArtifactRetentionReceipt>()?;
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<Path>) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<LegacyHeartbeatStateWithCognition>()?;
cache.register_entry_type::<EpiphanyHeartbeatCognitionEntry>()?;
cache.register_entry_type::<EpiphanyHeartbeatStaleTurnRepairReceipt>()?;
cache.register_entry_type::<EpiphanyHeartbeatArtifactRetentionPlan>()?;
cache.register_entry_type::<EpiphanyHeartbeatArtifactRetentionReceipt>()?;
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 {
<LegacyHeartbeatStateWithCognition as DatabaseEntry>::TYPE
} else {
<EpiphanyHeartbeatStateEntry as DatabaseEntry>::TYPE
};
fn import_owned_heartbeat_envelopes(cache: &mut CultCache, store_path: &Path) -> Result<()> {
let state_type = <EpiphanyHeartbeatStateEntry as DatabaseEntry>::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 == <EpiphanyHeartbeatCognitionEntry as DatabaseEntry>::TYPE
&& envelope.key == HEARTBEAT_COGNITION_KEY)
|| envelope.r#type == <EpiphanyHeartbeatStaleTurnRepairReceipt as DatabaseEntry>::TYPE;
let owned = owned
|| envelope.r#type == <EpiphanyHeartbeatArtifactRetentionPlan as DatabaseEntry>::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::<LegacyHeartbeatStateWithCognition>(envelope);
} else {
cache.load_envelope::<EpiphanyHeartbeatStateEntry>(envelope)?;
}
} else if envelope.r#type == <EpiphanyHeartbeatCognitionEntry as DatabaseEntry>::TYPE {
cache.load_envelope::<EpiphanyHeartbeatCognitionEntry>(envelope)?;
cache.load_envelope::<EpiphanyHeartbeatStateEntry>(envelope)?;
} else if envelope.r#type == <EpiphanyHeartbeatArtifactRetentionPlan as DatabaseEntry>::TYPE
{
cache.load_envelope::<EpiphanyHeartbeatArtifactRetentionPlan>(envelope)?;
@ -184,34 +148,6 @@ pub fn commit_heartbeat_state_transaction(
Ok(())
}
pub fn load_heartbeat_cognition_entry(
store_path: impl AsRef<Path>,
) -> Result<Option<EpiphanyHeartbeatCognitionEntry>> {
let store_path = store_path.as_ref();
let cache = heartbeat_state_cache(store_path)?;
if let Some(cognition) =
cache.get::<EpiphanyHeartbeatCognitionEntry>(HEARTBEAT_COGNITION_KEY)?
{
return Ok(Some(cognition));
}
let legacy_cache = legacy_heartbeat_state_cache(store_path)?;
let legacy = match legacy_cache.get::<LegacyHeartbeatStateWithCognition>(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<Path>,
cognition: &EpiphanyHeartbeatCognitionEntry,
) -> Result<EpiphanyHeartbeatCognitionEntry> {
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<Path>,
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<Option<EpiphanyHeartbeatCognitionEntry>> {
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<T>(value: Option<Value>) -> Result<Option<T>>
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(())
}

View file

@ -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;

View file

@ -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::*;

View file

@ -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<String>,
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<EpiphanyMemoryDomain>,
nodes: &mut Vec<EpiphanyMemoryNode>,
summaries: &mut Vec<EpiphanyMemorySummary>,
) {
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<EpiphanyMemoryDomain>,
nodes: &mut Vec<EpiphanyMemoryNode>,
summaries: &mut Vec<EpiphanyMemorySummary>,
) {
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<EpiphanyMemoryDomain>,
nodes: &mut Vec<EpiphanyMemoryNode>,
summaries: &mut Vec<EpiphanyMemorySummary>,
) {
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<EpiphanyMemoryDomain>,
nodes: &mut Vec<EpiphanyMemoryNode>,
summaries: &mut Vec<EpiphanyMemorySummary>,
) {
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<EpiphanyMemoryDomain>,
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<EpiphanyMemorySummary>,
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)
);
}
}

View file

@ -1,3 +1 @@
pub mod heartbeat;
pub use heartbeat::memory_graph_from_heartbeat_cognition;