Delete heartbeat cognition simulation
This commit is contained in:
parent
169207e07e
commit
d1685df830
13 changed files with 190 additions and 4579 deletions
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1 @@
|
|||
pub mod heartbeat;
|
||||
|
||||
pub use heartbeat::memory_graph_from_heartbeat_cognition;
|
||||
|
|
|
|||
Reference in a new issue