From 72debc77c153754b975d77898cae82f05e35d1a9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 6 May 2026 20:18:21 +0100 Subject: [PATCH] Add adaptive heartbeat pump --- .../src/bin/epiphany-heartbeat-store.rs | 108 ++++++- epiphany-core/src/heartbeat_state.rs | 283 +++++++++++++++++- epiphany-core/src/lib.rs | 2 + notes/fresh-workspace-handoff.md | 4 +- state/ledgers.msgpack | Bin 53515 -> 53939 bytes state/map.yaml | 6 +- 6 files changed, 394 insertions(+), 9 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-heartbeat-store.rs b/epiphany-core/src/bin/epiphany-heartbeat-store.rs index 9f095cba..2d8477b7 100644 --- a/epiphany-core/src/bin/epiphany-heartbeat-store.rs +++ b/epiphany-core/src/bin/epiphany-heartbeat-store.rs @@ -3,6 +3,7 @@ use anyhow::Result; use anyhow::anyhow; use epiphany_core::GhostlightSceneParticipantSeed; use epiphany_core::HeartbeatCompleteOptions; +use epiphany_core::HeartbeatPumpOptions; use epiphany_core::HeartbeatTickOptions; use epiphany_core::VoidRoutineOptions; use epiphany_core::apply_agent_self_patch; @@ -11,6 +12,7 @@ use epiphany_core::heartbeat_status_projection; use epiphany_core::initialize_ghostlight_scene_heartbeat_store; use epiphany_core::initialize_heartbeat_store; use epiphany_core::load_heartbeat_state_entry; +use epiphany_core::pump_heartbeat_store; use epiphany_core::run_void_routine_store; use epiphany_core::tick_heartbeat_store; use epiphany_core::validate_agent_memory_store; @@ -28,9 +30,15 @@ fn main() -> Result<()> { let mut store_path: Option = None; let mut artifact_dir: Option = None; let mut target_heartbeat_rate = 1.0_f64; + let mut min_heartbeat_rate = 0.05_f64; + let mut max_heartbeat_rate = 4.0_f64; + let mut min_concurrency = 1_usize; + let mut max_concurrency = 8_usize; + let mut max_ticks = 8_usize; let mut coordinator_action: Option = None; let mut target_role: Option = None; let mut urgency = 0.75_f64; + let mut urgency_explicit = false; let mut schedule_id = "epiphany-heartbeat".to_string(); let mut source_scene_ref = "epiphany/coordinator".to_string(); let mut defer_completion = false; @@ -52,11 +60,27 @@ fn main() -> Result<()> { "--target-heartbeat-rate" => { target_heartbeat_rate = next_value(&mut args, "--target-heartbeat-rate")?.parse()? } + "--min-heartbeat-rate" => { + min_heartbeat_rate = next_value(&mut args, "--min-heartbeat-rate")?.parse()? + } + "--max-heartbeat-rate" => { + max_heartbeat_rate = next_value(&mut args, "--max-heartbeat-rate")?.parse()? + } + "--min-concurrency" => { + min_concurrency = next_value(&mut args, "--min-concurrency")?.parse()? + } + "--max-concurrency" => { + max_concurrency = next_value(&mut args, "--max-concurrency")?.parse()? + } + "--max-ticks" => max_ticks = next_value(&mut args, "--max-ticks")?.parse()?, "--coordinator-action" => { coordinator_action = Some(next_value(&mut args, "--coordinator-action")?) } "--target-role" => target_role = Some(next_value(&mut args, "--target-role")?), - "--urgency" => urgency = next_value(&mut args, "--urgency")?.parse()?, + "--urgency" => { + urgency = next_value(&mut args, "--urgency")?.parse()?; + urgency_explicit = true; + } "--schedule-id" => schedule_id = next_value(&mut args, "--schedule-id")?, "--source-scene-ref" => source_scene_ref = next_value(&mut args, "--source-scene-ref")?, "--defer-completion" => defer_completion = true, @@ -150,6 +174,39 @@ fn main() -> Result<()> { )?; println!("{}", result); } + "pump" => { + 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, + HeartbeatPumpOptions { + base_heartbeat_rate: target_heartbeat_rate, + min_heartbeat_rate, + max_heartbeat_rate, + min_concurrency, + max_concurrency, + max_ticks, + external_urgency: if urgency_explicit { urgency } else { 0.0 }, + coordinator_action, + target_role, + schedule_id, + source_scene_ref, + agent_store, + }, + )?; + println!("{}", result); + } "status" => { let store_path = store_path.ok_or_else(|| anyhow!("status requires --store"))?; if let Some(artifact_dir) = artifact_dir { @@ -228,7 +285,7 @@ fn next_value(args: &mut impl Iterator, name: &str) -> Result Result<()> { Err(anyhow!( - "usage: epiphany-heartbeat-store init --store [--profile epiphany|ghostlight-scene] [--scene-id ] [--scene-participant ]\n epiphany-heartbeat-store tick --store --artifact-dir [--coordinator-action ] [--agent-store --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store complete --store --artifact-dir --role [--action-id ]\n epiphany-heartbeat-store status --store [--artifact-dir ]\n epiphany-heartbeat-store routine --store --artifact-dir [--agent-store ] [--source ] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store ]" + "usage: epiphany-heartbeat-store init --store [--profile epiphany|ghostlight-scene] [--scene-id ] [--scene-participant ]\n epiphany-heartbeat-store tick --store --artifact-dir [--coordinator-action ] [--agent-store --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store pump --store --artifact-dir [--agent-store ] [--urgency <0..1>] [--min-heartbeat-rate ] [--max-heartbeat-rate ] [--min-concurrency ] [--max-concurrency ] [--max-ticks ]\n epiphany-heartbeat-store complete --store --artifact-dir --role [--action-id ]\n epiphany-heartbeat-store status --store [--artifact-dir ]\n epiphany-heartbeat-store routine --store --artifact-dir [--agent-store ] [--source ] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store ]" )) } @@ -360,6 +417,46 @@ fn run_smoke(agent_store: &Path) -> Result { }, )?; 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()), + }, + )?; + 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()), + }, + )?; let initiative_errors = validate_schedule_shape(&work["schedule"]) .into_iter() .chain(validate_schedule_shape(&idle["schedule"])) @@ -417,6 +514,11 @@ fn run_smoke(agent_store: &Path) -> Result { }) == 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().unwrap_or(0) >= 6 + && alarmed_pump["pump"]["pacing"]["targetConcurrency"].as_u64() == Some(8); let result = serde_json::json!({ "ok": ok, @@ -427,6 +529,8 @@ fn run_smoke(agent_store: &Path) -> Result { "idleRumination": idle["rumination"], "voidRoutine": routine["routine"], "personalityTiming": routine_status["participants"], + "relaxedPump": relaxed_pump["pump"], + "alarmedPump": alarmed_pump["pump"], "validationErrors": validation_errors, "initiativeErrors": initiative_errors, }); diff --git a/epiphany-core/src/heartbeat_state.rs b/epiphany-core/src/heartbeat_state.rs index d3495f8f..a7ce9ca4 100644 --- a/epiphany-core/src/heartbeat_state.rs +++ b/epiphany-core/src/heartbeat_state.rs @@ -203,6 +203,22 @@ pub struct HeartbeatTickOptions { pub agent_store: Option, } +#[derive(Clone, Debug, PartialEq)] +pub struct HeartbeatPumpOptions { + pub base_heartbeat_rate: f64, + pub min_heartbeat_rate: f64, + pub max_heartbeat_rate: f64, + pub min_concurrency: usize, + pub max_concurrency: usize, + pub max_ticks: usize, + pub external_urgency: f64, + pub coordinator_action: Option, + pub target_role: Option, + pub schedule_id: String, + pub source_scene_ref: String, + pub agent_store: Option, +} + #[derive(Clone, Debug, PartialEq)] pub struct HeartbeatCompleteOptions { pub role: String, @@ -427,7 +443,7 @@ pub fn heartbeat_status_projection( "latestEvent": null, "history": [], "latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit), - "availableActions": ["init", "tick", "complete", "status"], + "availableActions": ["init", "tick", "pump", "complete", "status"], })); }; let history: Vec<_> = state @@ -463,7 +479,8 @@ pub fn heartbeat_status_projection( "candidateInterventions": state.candidate_interventions, "appraisals": state.appraisals, "reactions": state.reactions, - "availableActions": ["init", "tick", "complete", "status", "routine"], + "adaptivePacing": state.extra.get("adaptivePacing"), + "availableActions": ["init", "tick", "pump", "complete", "status", "routine"], })) } @@ -595,6 +612,132 @@ pub fn tick_heartbeat_store( })) } +pub fn pump_heartbeat_store( + store_path: impl AsRef, + artifact_dir: impl AsRef, + options: HeartbeatPumpOptions, +) -> Result { + let store_path = store_path.as_ref(); + let mut state = load_heartbeat_state_entry(store_path)? + .unwrap_or_else(|| default_heartbeat_state(options.base_heartbeat_rate.max(0.001))); + if options.base_heartbeat_rate > 0.0 { + state.target_heartbeat_rate = options.base_heartbeat_rate; + } + patch_missing_participants(&mut state); + if let Some(agent_store) = options.agent_store.as_deref() { + let appraisal_profiles = collect_role_appraisal_profiles(Some(agent_store))?; + apply_personality_timing_profiles(&mut state, &appraisal_profiles); + } + if let Some(appraisals) = state.appraisals.clone() { + apply_mood_timing_from_appraisals(&mut state, &appraisals); + } + + let pacing = adaptive_swarm_pacing(&state, &options); + state.target_heartbeat_rate = pacing.effective_heartbeat_rate; + state.extra.insert( + "adaptivePacing".to_string(), + serde_json::json!({ + "schema_version": "epiphany.adaptive_heartbeat_pacing.v0", + "contract": "Swarm pressure controls both heartbeat tempo and concurrency. Relaxed systems sleep slow; urgent systems fill more lanes without re-waking unfinished turns.", + "pressure": pacing.pressure, + "effectiveHeartbeatRate": pacing.effective_heartbeat_rate, + "targetConcurrency": pacing.target_concurrency, + "runningTurns": pacing.running_turns, + "activeParticipants": pacing.active_participants, + "signals": pacing.signals, + }), + ); + + let artifact_dir = artifact_dir.as_ref(); + fs::create_dir_all(artifact_dir) + .with_context(|| format!("failed to create {}", artifact_dir.display()))?; + + let mut tick_results = Vec::new(); + let mut errors = Vec::new(); + let ticks_allowed = options.max_ticks.min(pacing.target_concurrency); + for index in 0..ticks_allowed { + if running_turn_count(&state) >= pacing.target_concurrency { + break; + } + let use_coordinator_action = index == 0; + let tick_options = HeartbeatTickOptions { + target_heartbeat_rate: pacing.effective_heartbeat_rate, + coordinator_action: use_coordinator_action + .then(|| options.coordinator_action.clone()) + .flatten(), + target_role: use_coordinator_action + .then(|| options.target_role.clone()) + .flatten(), + urgency: pacing + .pressure + .max(options.external_urgency.clamp(0.0, 1.0)), + schedule_id: format!("{}.pump-{:03}", options.schedule_id, index + 1), + source_scene_ref: options.source_scene_ref.clone(), + defer_completion: true, + agent_store: options.agent_store.clone(), + }; + match tick_once(&mut state, &tick_options) { + Ok(result) => { + write_json_artifact( + artifact_dir.join(format!("{}.initiative.json", tick_options.schedule_id)), + &result["schedule"], + )?; + write_json_artifact( + artifact_dir.join(format!("{}.event.json", tick_options.schedule_id)), + &result["event"], + )?; + tick_results.push(serde_json::json!({ + "event": result["event"], + "schedule": result["schedule"], + "rumination": result["rumination"], + })); + } + Err(error) => { + errors.push(error.to_string()); + break; + } + } + } + let launched = tick_results.len(); + let final_running_turns = running_turn_count(&state); + write_heartbeat_state_entry(store_path, &state)?; + + let pump = serde_json::json!({ + "schema_version": "epiphany.adaptive_heartbeat_pump.v0", + "storeFile": store_path, + "artifactDir": artifact_dir, + "sourceSceneRef": options.source_scene_ref, + "scheduleId": options.schedule_id, + "pacing": { + "schema_version": "epiphany.adaptive_heartbeat_pacing.v0", + "pressure": pacing.pressure, + "effectiveHeartbeatRate": pacing.effective_heartbeat_rate, + "targetConcurrency": pacing.target_concurrency, + "runningTurnsBefore": pacing.running_turns, + "runningTurnsAfter": final_running_turns, + "activeParticipants": pacing.active_participants, + "signals": pacing.signals, + }, + "launched": launched, + "ticks": tick_results, + "errors": errors, + "reviewNotes": [ + "The pump controls opportunity pressure, not authority.", + "A relaxed swarm may launch nothing; an urgent swarm may fill most available lanes.", + "Per-lane pending turns remain hard locks, so no agent is re-heartbeaten while its previous turn is running." + ], + }); + write_json_artifact( + artifact_dir.join(format!("{}.pump.json", options.schedule_id)), + &pump, + )?; + + Ok(serde_json::json!({ + "ok": errors.is_empty(), + "pump": pump, + })) +} + pub fn complete_heartbeat_store( store_path: impl AsRef, artifact_dir: impl AsRef, @@ -1847,6 +1990,142 @@ fn apply_mood_timing_from_appraisals(state: &mut EpiphanyHeartbeatStateEntry, ap } } +#[derive(Clone, Debug)] +struct AdaptiveSwarmPacing { + pressure: f64, + effective_heartbeat_rate: f64, + target_concurrency: usize, + running_turns: usize, + active_participants: usize, + signals: Value, +} + +fn adaptive_swarm_pacing( + state: &EpiphanyHeartbeatStateEntry, + options: &HeartbeatPumpOptions, +) -> AdaptiveSwarmPacing { + let active_participants = state + .participants + .iter() + .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 base_rate = options + .base_heartbeat_rate + .max(state.pacing_policy.minimum_effective_rate) + .max(0.001); + let min_rate = options.min_heartbeat_rate.max(0.001).min(base_rate); + let max_rate = options.max_heartbeat_rate.max(base_rate).max(min_rate); + let pressure_curve = pressure * pressure; + let effective_heartbeat_rate = round6(min_rate + (max_rate - min_rate) * pressure_curve); + + let max_concurrency = options + .max_concurrency + .max(1) + .min(active_participants.max(1)); + let relaxed_floor = if pressure < 0.18 { + 0 + } else { + options.min_concurrency.min(max_concurrency) + }; + let target_concurrency = if pressure < 0.18 { + relaxed_floor + } else { + let span = max_concurrency.saturating_sub(relaxed_floor); + (relaxed_floor + (span as f64 * pressure).ceil() as usize).min(max_concurrency) + }; + + AdaptiveSwarmPacing { + pressure: round3(pressure), + effective_heartbeat_rate, + target_concurrency, + running_turns, + active_participants, + signals: serde_json::json!({ + "externalUrgency": round3(external_urgency), + "maxAnxiety": round3(mood_signals.max_anxiety), + "averageAnxiety": round3(mood_signals.average_anxiety), + "maxUrgency": round3(mood_signals.max_urgency), + "maxArousal": round3(mood_signals.max_arousal), + "maxThoughtPressure": round3(mood_signals.max_thought_pressure), + "maxReactionIntensity": round3(mood_signals.max_reaction_intensity), + "pendingPressure": round3(pending_pressure), + "contract": "Anxiety-like state raises tempo and concurrency; calm state lets the swarm sleep slow.", + }), + } +} + +#[derive(Clone, Debug, 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.extra.get("moodTiming") else { + continue; + }; + let anxiety = number_at(mood, "/anxiety"); + let urgency = number_at(mood, "/urgency"); + let arousal = number_at(mood, "/arousal"); + let thought_pressure = number_at(mood, "/thoughtPressure"); + let reaction_intensity = number_at(mood, "/reactionIntensity"); + 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 +} + +fn running_turn_count(state: &EpiphanyHeartbeatStateEntry) -> usize { + state + .participants + .iter() + .filter(|participant| is_turn_pending(participant)) + .count() +} + +fn number_at(value: &Value, pointer: &str) -> f64 { + value + .pointer(pointer) + .and_then(Value::as_f64) + .unwrap_or(0.0) +} + fn personality_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 { participant .extra diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 0e92f84c..06852882 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -39,6 +39,7 @@ pub use heartbeat_state::HeartbeatHistoryEvent; pub use heartbeat_state::HeartbeatPacingPolicy; pub use heartbeat_state::HeartbeatParticipant; pub use heartbeat_state::HeartbeatPendingTurn; +pub use heartbeat_state::HeartbeatPumpOptions; pub use heartbeat_state::HeartbeatSelectionPolicy; pub use heartbeat_state::HeartbeatTickOptions; pub use heartbeat_state::VoidRoutineOptions; @@ -50,6 +51,7 @@ 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_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_state; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 512c360f..dadf749a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -43,7 +43,7 @@ remembers doctrine; the branch remembers the blade. - The prompt doctrine pass is landed. Shared Epiphany prompts now carry distilled memory/evidence discipline. Rendered state intro/doctrine text lives in `epiphany-core/src/prompts/`, and lane/control prompt text lives in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`: modeling is the Body, implementation is the Hands and GUI-launched main coding lane, verification is the Soul, reorientation is Life, coordinator remains the read-only Self, and CRRC owns the pre-compaction intervention template. - The Ghostlight memory pass is landed. `epiphany_specialists.toml` now has a shared persistent-memory projection prepended by the harness to fixed role specialists, reorientation workers, coordinator notes, and CRRC checkpoint interventions. The rendered base doctrine also states the Perfect Machine rule directly: prompt is projection, durable typed state is the mind, every lane must improve its own memory/model/prompt/evidence habit or name the repair, and each lane phrases that duty in its own organ language so the salience sticks. - The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`. -- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with arena, participant kind, speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, personality cooldown, mood cooldown, effective cooldown, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Role dossiers set baseline timing; appraisal mood/anxiety bends it through urgency, arousal, thought pressure, guardedness, and reaction intensity, so Hands-like work pressure and high-need anxious lanes recover sooner without being heartbeaten while a prior turn is running. Idle turns are for rumination: light thought shuffling, role-quality attention, and candidate selfPatch pressure. Sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god. +- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with arena, participant kind, speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, personality cooldown, mood cooldown, effective cooldown, adaptive pacing, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Role dossiers set baseline timing; appraisal mood/anxiety bends it through urgency, arousal, thought pressure, guardedness, and reaction intensity, so Hands-like work pressure and high-need anxious lanes recover sooner without being heartbeaten while a prior turn is running. `epiphany-heartbeat-store pump` computes pressure from external urgency, mood/anxiety, reaction intensity, thought pressure, and current pending load, then chooses tempo plus target concurrency: calm can launch zero and sleep slow; alarm can fill most active lanes. Idle turns are for rumination: light thought shuffling, role-quality attention, and candidate selfPatch pressure. Sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god. - The first Ghostlight-derived timing slice is landed in Epiphany, but Ghostlight is reference lineage rather than a sibling runtime to preserve. `epiphany-heartbeat-store init --profile ghostlight-scene --scene-id --scene-participant ` creates a typed CultCache MessagePack scene heartbeat store whose participants are `arena=scene`, `participantKind=character`; `tick` emits `ghostlight.initiative_schedule.v0` receipts with `scene_turn` actions and local-affordance basis. The generic Epiphany maintenance lanes are not auto-patched into scene stores. - The first Void-derived routine slice is landed in Epiphany, but VoidBot is reference lineage rather than a runtime dependency. `epiphany-heartbeat-store routine --store .\state\agent-heartbeats.msgpack --artifact-dir .\.epiphany-heartbeats --agent-store .\state\agents.msgpack` reads typed role dossiers, computes bounded memory resonance, maintains incubation themes, runs analytic and associative cognition lanes, writes bridge syntheses/saturation/tension, projects the active thought cluster through each role's Ghostlight-shaped personality vectors, derives participant-local reactions, applies personality/mood timing, advances the sleep/dream cycle, updates the typed heartbeat store, and emits an auditable `epiphany.void_routine.v0` receipt. This mutates only heartbeat physiology fields: project truth and role memory mutation remain on their reviewed surfaces. - Manual Codex-run rumination still has an explicit aftercare rule until Epiphany owns the full sleep consolidator. In the intended Epiphany cycle, sub-agents ruminate when idle and distill when they sleep; in this supervising Codex thread, a heartbeat/routine vigil is only physiology until a separate closing pass reviews the receipts and decides whether map, handoff, evidence, or role self-memory should change. @@ -139,7 +139,7 @@ The current spine, blessed but not yet finished: - durable investigation checkpoint packet through typed state, prompt, scene, and context - distilled shared and config-backed prompt doctrine through the base prompt, rendered Epiphany state prompt files, modeling/Body, implementation/Hands, verification/Soul, reorientation/Life, coordinator/Self, and CRRC intervention surfaces - shared Ghostlight-derived persistent memory projection across Imagination, Body, Soul, Life, Self, CRRC checkpoint steering, and the GUI-launched Hands implementation lane -- Ghostlight-derived heartbeat initiative scheduling through the native `epiphany-heartbeat-store` binary, with Self and Face included as first-class Epiphany maintenance participants, Ghostlight scene characters supported through the same timing law, role-personality cooldowns, appraisal mood/anxiety cooldowns, effective recovery timing, idle rumination feeding candidate self-memory pressure, sleep/dream passes serving as the distillation window, and heartbeat state persisted through typed CultCache MessagePack stores +- Ghostlight-derived heartbeat initiative scheduling through the native `epiphany-heartbeat-store` binary, with Self and Face included as first-class Epiphany maintenance participants, Ghostlight scene characters supported through the same timing law, role-personality cooldowns, appraisal mood/anxiety cooldowns, effective recovery timing, adaptive pressure-based pump tempo/concurrency, idle rumination feeding candidate self-memory pressure, sleep/dream passes serving as the distillation window, and heartbeat state persisted through typed CultCache MessagePack stores - Void-derived native routine physiology through `epiphany-heartbeat-store routine`, with sleep cycle, memory resonance, incubation, analytic/associative cognition lanes, bridge judgment, candidate interventions, Ghostlight-style personality appraisals, mood/anxiety timing, derived reactions, and dream maintenance stored in the typed heartbeat store - Epiphany-native character-loop packet projection through `epiphany-character-loop`, with Face as the first public-surface actor over its typed role dossier - Face as the public #aquarium-only surface for translating agent thought-weather into short chats or candidate drafts, without moderator authority diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 896db7c6e81e6bcef92781a4a132d5080ce88f50..99a72fda352ff457f70159baf34c82222c3a4dd9 100644 GIT binary patch delta 405 zcmYk2u}T9$5Qd2&)`AZ(Vrin6hymjZ*a}uwj<<8Ug}vEjch*a^h+t_W1bhI+2as6V z*w`yRk$Z`RbTcsjKi~g9Jg?7Q*B^J*esAA)pIWW^*+=W-d;0RVwRt?~_6OPF^z3s_ zsZ&BPSA+sCRu0(KfC`-i5UQL7WCgk9JbF)@k3lSi8hr)MQwY)10B9uS84W}~Y;M|6 zS*s3^N17yU)61XjiIg@D%k<@6)}A{%Vx@Tq#-aj6$*0mxKt(~h=xc&7VmvBSgg2ajEFjogd