Quarantine heartbeat cognition state

This commit is contained in:
Meta Vens 2026-05-12 04:05:05 +01:00
commit 0005ef4ed2
6 changed files with 326 additions and 89 deletions

View file

@ -520,6 +520,10 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
&& 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| {

View file

@ -17,6 +17,9 @@ use std::path::Path;
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";
@ -57,26 +60,86 @@ pub struct EpiphanyHeartbeatStateEntry {
pub participants: Vec<HeartbeatParticipant>,
#[cultcache(key = 6)]
pub history: Vec<HeartbeatHistoryEvent>,
#[cultcache(key = 7, default)]
pub sleep_cycle: Option<Value>,
#[cultcache(key = 8, default)]
pub memory_resonance: Option<Value>,
#[cultcache(key = 9, default)]
pub incubation: Option<Value>,
#[cultcache(key = 10, default)]
pub thought_lanes: Option<Value>,
#[cultcache(key = 11, default)]
pub bridge: Option<Value>,
#[cultcache(key = 12, default)]
pub candidate_interventions: Option<Value>,
#[cultcache(key = 13, default)]
pub appraisals: Option<Value>,
#[cultcache(key = 14, default)]
pub reactions: Option<Value>,
#[cultcache(key = 15, default)]
pub extra: BTreeMap<String, Value>,
}
#[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<Value>,
#[cultcache(key = 6, default)]
pub memory_resonance: Option<Value>,
#[cultcache(key = 7, default)]
pub incubation: Option<Value>,
#[cultcache(key = 8, default)]
pub thought_lanes: Option<Value>,
#[cultcache(key = 9, default)]
pub bridge: Option<Value>,
#[cultcache(key = 10, default)]
pub candidate_interventions: Option<Value>,
#[cultcache(key = 11, default)]
pub appraisals: Option<Value>,
#[cultcache(key = 12, default)]
pub reactions: Option<Value>,
#[cultcache(key = 13, default)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.agent_heartbeat",
schema = "EpiphanyHeartbeatStateEntry"
)]
struct LegacyHeartbeatStateWithCognition {
#[cultcache(key = 0)]
schema_version: String,
#[cultcache(key = 1)]
target_heartbeat_rate: f64,
#[cultcache(key = 2)]
scene_clock: f64,
#[cultcache(key = 3)]
selection_policy: HeartbeatSelectionPolicy,
#[cultcache(key = 4)]
pacing_policy: HeartbeatPacingPolicy,
#[cultcache(key = 5)]
participants: Vec<HeartbeatParticipant>,
#[cultcache(key = 6)]
history: Vec<HeartbeatHistoryEvent>,
#[cultcache(key = 7, default)]
sleep_cycle: Option<Value>,
#[cultcache(key = 8, default)]
memory_resonance: Option<Value>,
#[cultcache(key = 9, default)]
incubation: Option<Value>,
#[cultcache(key = 10, default)]
thought_lanes: Option<Value>,
#[cultcache(key = 11, default)]
bridge: Option<Value>,
#[cultcache(key = 12, default)]
candidate_interventions: Option<Value>,
#[cultcache(key = 13, default)]
appraisals: Option<Value>,
#[cultcache(key = 14, default)]
reactions: Option<Value>,
#[cultcache(key = 15, default)]
extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatSelectionPolicy {
pub mode: String,
@ -257,6 +320,16 @@ struct HeartbeatAction {
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.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref()));
cache.pull_all_backing_stores()?;
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.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref()));
cache.pull_all_backing_stores()?;
Ok(cache)
@ -278,6 +351,90 @@ pub fn write_heartbeat_state_entry(
cache.put(HEARTBEAT_STATE_KEY, state)
}
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 Some(legacy) =
legacy_cache.get::<LegacyHeartbeatStateWithCognition>(HEARTBEAT_STATE_KEY)?
else {
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 mut cache = heartbeat_state_cache(store_path)?;
cache.put(HEARTBEAT_COGNITION_KEY, cognition)
}
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: legacy.sleep_cycle,
memory_resonance: legacy.memory_resonance,
incubation: legacy.incubation,
thought_lanes: legacy.thought_lanes,
bridge: legacy.bridge,
candidate_interventions: legacy.candidate_interventions,
appraisals: legacy.appraisals,
reactions: legacy.reactions,
extra: BTreeMap::new(),
}))
}
pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<()> {
if state.schema_version != HEARTBEAT_STATE_SCHEMA_VERSION {
return Err(anyhow!(
@ -363,14 +520,6 @@ pub fn default_heartbeat_state(target_heartbeat_rate: f64) -> EpiphanyHeartbeatS
.map(|role_id| default_participant(role_id))
.collect(),
history: Vec::new(),
sleep_cycle: None,
memory_resonance: None,
incubation: None,
thought_lanes: None,
bridge: None,
candidate_interventions: None,
appraisals: None,
reactions: None,
extra: BTreeMap::new(),
}
}
@ -446,6 +595,7 @@ pub fn heartbeat_status_projection(
"availableActions": ["init", "tick", "pump", "complete", "status"],
}));
};
let cognition = load_heartbeat_cognition_entry(store_path)?;
let history: Vec<_> = state
.history
.iter()
@ -471,14 +621,15 @@ pub fn heartbeat_status_projection(
"latestEvent": history.last().cloned(),
"history": history,
"latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit),
"sleepCycle": state.sleep_cycle,
"memoryResonance": state.memory_resonance,
"incubation": state.incubation,
"thoughtLanes": state.thought_lanes,
"bridge": state.bridge,
"candidateInterventions": state.candidate_interventions,
"appraisals": state.appraisals,
"reactions": state.reactions,
"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.extra.get("adaptivePacing"),
"availableActions": ["init", "tick", "pump", "complete", "status", "routine"],
}))
@ -493,28 +644,45 @@ pub fn run_void_routine_store(
let mut state =
load_heartbeat_state_entry(store_path)?.unwrap_or_else(|| default_heartbeat_state(1.0));
patch_missing_participants(&mut state);
let previous_cognition = load_heartbeat_cognition_entry(store_path)?;
let memory_records = collect_role_memory_records(options.agent_store.as_deref())?;
let appraisal_profiles = collect_role_appraisal_profiles(options.agent_store.as_deref())?;
apply_personality_timing_profiles(&mut state, &appraisal_profiles);
let resonance = build_memory_resonance(&memory_records);
let incubation = build_incubation(
&state.incubation,
&state.bridge,
&state.candidate_interventions,
&previous_cognition.as_ref().and_then(|entry| entry.incubation.clone()),
&previous_cognition.as_ref().and_then(|entry| entry.bridge.clone()),
&previous_cognition
.as_ref()
.and_then(|entry| entry.candidate_interventions.clone()),
&resonance,
&memory_records,
);
let thought_lanes = build_thought_lanes(&resonance, &incubation, &memory_records);
let bridge = build_thought_bridge(&state.bridge, &thought_lanes, &resonance, &incubation);
let bridge = build_thought_bridge(
&previous_cognition.as_ref().and_then(|entry| entry.bridge.clone()),
&thought_lanes,
&resonance,
&incubation,
);
let candidate_interventions = build_candidate_interventions(&bridge, &incubation);
let appraisals =
build_agent_appraisals(&appraisal_profiles, &thought_lanes, &incubation, &bridge);
let reactions = build_agent_reactions(&appraisals, &bridge);
apply_mood_timing_from_appraisals(&mut state, &appraisals);
let sleep_cycle =
update_sleep_cycle(state.sleep_cycle.as_ref(), &incubation, options.allow_dream);
let sleep_cycle = update_sleep_cycle(
previous_cognition
.as_ref()
.and_then(|entry| entry.sleep_cycle.as_ref()),
&incubation,
options.allow_dream,
);
let run_id = format!("epiphany-void-routine-{}", now_stamp());
let artifact_dir = artifact_dir.as_ref();
fs::create_dir_all(artifact_dir)
.with_context(|| format!("failed to create {}", artifact_dir.display()))?;
let artifact_path = artifact_dir.join(format!("{run_id}.routine.json"));
let routine = serde_json::json!({
"schema_version": VOID_ROUTINE_SCHEMA_VERSION,
"runId": run_id,
@ -540,30 +708,27 @@ pub fn run_void_routine_store(
],
});
state.sleep_cycle = Some(sleep_cycle);
state.memory_resonance = Some(resonance);
state.incubation = Some(incubation);
state.thought_lanes = Some(thought_lanes);
state.bridge = Some(bridge);
state.candidate_interventions = Some(candidate_interventions);
state.appraisals = Some(appraisals);
state.reactions = Some(reactions);
state.extra.insert(
"voidRoutine".to_string(),
serde_json::json!({
"lastRunId": run_id,
"lastRunAt": now_iso(),
"source": options.source,
"referenceLineage": "VoidBot reference; Epiphany-native implementation.",
}),
);
write_heartbeat_state_entry(store_path, &state)?;
let artifact_dir = artifact_dir.as_ref();
fs::create_dir_all(artifact_dir)
.with_context(|| format!("failed to create {}", artifact_dir.display()))?;
let artifact_path = artifact_dir.join(format!("{run_id}.routine.json"));
write_json_artifact(&artifact_path, &routine)?;
write_heartbeat_cognition_entry(
store_path,
&EpiphanyHeartbeatCognitionEntry {
schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(),
updated_at: now_iso(),
latest_run_id: Some(run_id),
latest_artifact_ref: Some(artifact_path.display().to_string()),
source: Some(options.source),
sleep_cycle: Some(sleep_cycle),
memory_resonance: Some(resonance),
incubation: Some(incubation),
thought_lanes: Some(thought_lanes),
bridge: Some(bridge),
candidate_interventions: Some(candidate_interventions),
appraisals: Some(appraisals),
reactions: Some(reactions),
extra: BTreeMap::new(),
},
)?;
write_heartbeat_state_entry(store_path, &state)?;
Ok(serde_json::json!({
"ok": true,
@ -589,7 +754,9 @@ pub fn tick_heartbeat_store(
let appraisal_profiles = collect_role_appraisal_profiles(Some(agent_store))?;
apply_personality_timing_profiles(&mut state, &appraisal_profiles);
}
if let Some(appraisals) = state.appraisals.clone() {
if let Some(appraisals) =
load_heartbeat_cognition_entry(store_path)?.and_then(|entry| entry.appraisals)
{
apply_mood_timing_from_appraisals(&mut state, &appraisals);
}
let result = tick_once(&mut state, &options)?;
@ -634,7 +801,9 @@ pub fn pump_heartbeat_store(
let appraisal_profiles = collect_role_appraisal_profiles(Some(agent_store))?;
apply_personality_timing_profiles(&mut state, &appraisal_profiles);
}
if let Some(appraisals) = state.appraisals.clone() {
if let Some(appraisals) =
load_heartbeat_cognition_entry(store_path)?.and_then(|entry| entry.appraisals)
{
apply_mood_timing_from_appraisals(&mut state, &appraisals);
}
@ -1468,6 +1637,19 @@ 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: &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.",
})
}

View file

@ -37,7 +37,11 @@ pub use distillation::EpiphanyDistillInput;
pub use distillation::EpiphanyDistillProposal;
pub use distillation::distill_observation;
pub use heartbeat_state::EpiphanyHeartbeatStateEntry;
pub use heartbeat_state::EpiphanyHeartbeatCognitionEntry;
pub use heartbeat_state::GhostlightSceneParticipantSeed;
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_STATE_KEY;
pub use heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION;
pub use heartbeat_state::HEARTBEAT_STATE_TYPE;
@ -57,11 +61,14 @@ 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::pump_heartbeat_store;
pub use heartbeat_state::run_void_routine_store;
pub use heartbeat_state::tick_heartbeat_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_state_entry;
pub use promotion::EpiphanyPromotionDecision;
pub use promotion::EpiphanyPromotionInput;

View file

@ -0,0 +1,65 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-heartbeat-cognition.schema.json",
"title": "Epiphany Heartbeat Cognition Quarantine",
"description": "Experimental Void/Ghostlight routine thought-weather kept outside stable heartbeat scheduler state. These values are inspectable receipts, not project truth or scheduling authority.",
"type": "object",
"additionalProperties": true,
"properties": {
"schemaVersion": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"latestRunId": {
"type": ["string", "null"]
},
"latestArtifactRef": {
"type": ["string", "null"]
},
"source": {
"type": ["string", "null"]
},
"sleepCycle": {
"type": "object",
"additionalProperties": true
},
"memoryResonance": {
"type": "object",
"additionalProperties": true
},
"incubation": {
"type": "object",
"additionalProperties": true
},
"thoughtLanes": {
"type": "object",
"additionalProperties": true
},
"bridge": {
"type": "object",
"additionalProperties": true
},
"candidateInterventions": {
"type": "object",
"additionalProperties": true
},
"appraisals": {
"type": "object",
"additionalProperties": true
},
"reactions": {
"type": "object",
"additionalProperties": true
},
"extra": {
"type": "object",
"additionalProperties": true
}
},
"required": [
"schemaVersion",
"updatedAt"
]
}

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-heartbeat.schema.json",
"title": "Epiphany Heartbeat State",
"description": "Typed heartbeat store entry for initiative, rumination, and routine physiology.",
"description": "Stable typed heartbeat scheduler state for initiative, readiness, pacing, pending turns, and history. Experimental cognition thought-weather lives in epiphany.agent-heartbeat-cognition.",
"type": "object",
"additionalProperties": true,
"properties": {
@ -37,38 +37,6 @@
"additionalProperties": true
}
},
"sleepCycle": {
"type": "object",
"additionalProperties": true
},
"memoryResonance": {
"type": "object",
"additionalProperties": true
},
"incubation": {
"type": "object",
"additionalProperties": true
},
"thoughtLanes": {
"type": "object",
"additionalProperties": true
},
"bridge": {
"type": "object",
"additionalProperties": true
},
"candidateInterventions": {
"type": "object",
"additionalProperties": true
},
"appraisals": {
"type": "object",
"additionalProperties": true
},
"reactions": {
"type": "object",
"additionalProperties": true
},
"extra": {
"type": "object",
"additionalProperties": true

View file

@ -78,6 +78,17 @@
"title": "Epiphany Heartbeat State",
"path": "epiphany.agent-heartbeat.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-heartbeat-cognition.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.agent_heartbeat_cognition.v0",
"documentType": "epiphany.agent_heartbeat_cognition",
"title": "Epiphany Heartbeat Cognition Quarantine",
"path": "epiphany.agent-heartbeat-cognition.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.state-ledger.schema.json",
"kind": "document_payload",