Add adaptive heartbeat pump

This commit is contained in:
Meta Vens 2026-05-06 20:18:21 +01:00
commit 72debc77c1
6 changed files with 394 additions and 9 deletions

View file

@ -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<PathBuf> = None;
let mut artifact_dir: Option<PathBuf> = 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<String> = None;
let mut target_role: Option<String> = 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<Item = String>, name: &str) -> Result<Str
fn usage() -> Result<()> {
Err(anyhow!(
"usage: epiphany-heartbeat-store init --store <path> [--profile epiphany|ghostlight-scene] [--scene-id <id>] [--scene-participant <id|name|speed|reaction|threshold|constraint;constraint>]\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store routine --store <path> --artifact-dir <path> [--agent-store <path>] [--source <source>] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store <path>]"
"usage: epiphany-heartbeat-store init --store <path> [--profile epiphany|ghostlight-scene] [--scene-id <id>] [--scene-participant <id|name|speed|reaction|threshold|constraint;constraint>]\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store pump --store <path> --artifact-dir <path> [--agent-store <path>] [--urgency <0..1>] [--min-heartbeat-rate <n>] [--max-heartbeat-rate <n>] [--min-concurrency <n>] [--max-concurrency <n>] [--max-ticks <n>]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store routine --store <path> --artifact-dir <path> [--agent-store <path>] [--source <source>] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store <path>]"
))
}
@ -360,6 +417,46 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
},
)?;
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<serde_json::Value> {
})
== 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<serde_json::Value> {
"idleRumination": idle["rumination"],
"voidRoutine": routine["routine"],
"personalityTiming": routine_status["participants"],
"relaxedPump": relaxed_pump["pump"],
"alarmedPump": alarmed_pump["pump"],
"validationErrors": validation_errors,
"initiativeErrors": initiative_errors,
});

View file

@ -203,6 +203,22 @@ pub struct HeartbeatTickOptions {
pub agent_store: Option<std::path::PathBuf>,
}
#[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<String>,
pub target_role: Option<String>,
pub schedule_id: String,
pub source_scene_ref: String,
pub agent_store: Option<std::path::PathBuf>,
}
#[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<Path>,
artifact_dir: impl AsRef<Path>,
options: HeartbeatPumpOptions,
) -> Result<Value> {
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<Path>,
artifact_dir: impl AsRef<Path>,
@ -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

View file

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

View file

@ -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 <id> --scene-participant <id|name|speed|reaction|threshold|constraints>` 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

Binary file not shown.

View file

@ -135,14 +135,14 @@ accepted_design:
- Specialist prompts are role-distilled instead of generic and their live text is config-backed in app-server: modeling/checkpoint is the Body of code, dataflow, seams, and frontier; research/scout is the Eyes prompt surface for existing work, vendor guidance, libraries, papers, and anti-Greenspun source selection; implementation is the Hands, the main coding lane and actuator rather than a `roleLaunch` specialist; verification/review is the Soul of promise, invariants, evidence, and user truth; reorientation is Life across compaction and source drift; coordinator is the read-only Self that routes attention without implementing, verifying, promoting, or accepting; Face is the public Discord #aquarium surface that translates agent thought-weather without moderating or leaking sealed thought.
- Epiphany prompt surfaces now carry a shared Ghostlight-derived persistent memory projection before lane-specific text: prompt is not memory, durable typed state is the mind, personality and protected values are first-class salience handles, and every lane is explicitly responsible for making its own memory/model/prompt/evidence habit sharper or naming the needed repair. Each lane states self-improvement in its own rite-language: Imagination selects futures, Body clarifies anatomy, Eyes sees earlier, Hands touches source more precisely, Soul becomes harder to fool, Life loses less continuity, Self routes repair, and Face becomes clearer at turning inner weather into useful social presence.
- Specialist self-memory now has a bounded Ghostlight-shaped CultCache persistence surface in `state/agents.msgpack`: Imagination, Body, Eyes, Hands, Soul, Life, Self, and Face each have an individual typed role dossier for identity, personality, semantic/episodic/relationship memory, goals, and values. These entries are lane memory only; project truth remains in authoritative `EpiphanyThreadState`.
- `state/agent-heartbeats.msgpack`, `epiphany-core::EpiphanyHeartbeatStateEntry`, and the native `epiphany-heartbeat-store` binary are the Ghostlight-derived heartbeat initiative seam. The harness sets a target heartbeat rate; participants carry arena, participant kind, initiative speed, next-ready time, reaction bias, interrupt threshold, current load, status, constraints, personality cooldown, mood cooldown, and effective cooldown; pending coordinator work can pull its owning lane through a reaction window; otherwise the earliest ready active participant wins the slot. Role dossiers set the personality baseline, appraisals bend it with mood/anxiety signals such as urgency, arousal, thought pressure, guardedness, and reaction intensity, and lower effective cooldown means the lane recovers sooner and gets the floor more often. If the winner has no routeable maintenance work, it ruminates on role quality and prepares bounded selfPatch pressure rather than pretending rumination has already consolidated doctrine. Sleep/dream cycle passes are the intended distillation window for reviewed self-memory and doctrine. Face can win `surfaceAgentThoughts` or `discordAquariumChat` work slots. Epiphany also owns native character-loop projection through `epiphany-character-loop`, so role dossiers can respond to visible stimuli without importing Ghostlight as runtime. The same heartbeat store now owns the Void-derived routine fields `sleep_cycle`, `memory_resonance`, `incubation`, `thought_lanes`, `bridge`, and `candidate_interventions` through the native `epiphany-heartbeat-store routine` command.
- `state/agent-heartbeats.msgpack`, `epiphany-core::EpiphanyHeartbeatStateEntry`, and the native `epiphany-heartbeat-store` binary are the Ghostlight-derived heartbeat initiative seam. The harness sets a target heartbeat rate; participants carry arena, participant kind, initiative speed, next-ready time, reaction bias, interrupt threshold, current load, status, constraints, personality cooldown, mood cooldown, and effective cooldown; pending coordinator work can pull its owning lane through a reaction window; otherwise the earliest ready active participant wins the slot. Role dossiers set the personality baseline, appraisals bend it with mood/anxiety signals such as urgency, arousal, thought pressure, guardedness, and reaction intensity, and lower effective cooldown means the lane recovers sooner and gets the floor more often. The native `pump` command computes adaptive swarm pressure from external urgency, mood/anxiety, reaction intensity, thought pressure, and current pending load; relaxed pressure can launch nothing and run at crawl tempo, while alarm pressure can fill most or all active lanes up to a configured concurrency cap without waking any lane already running. If the winner has no routeable maintenance work, it ruminates on role quality and prepares bounded selfPatch pressure rather than pretending rumination has already consolidated doctrine. Sleep/dream cycle passes are the intended distillation window for reviewed self-memory and doctrine. Face can win `surfaceAgentThoughts` or `discordAquariumChat` work slots. Epiphany also owns native character-loop projection through `epiphany-character-loop`, so role dossiers can respond to visible stimuli without importing Ghostlight as runtime. The same heartbeat store now owns the Void-derived routine fields `sleep_cycle`, `memory_resonance`, `incubation`, `thought_lanes`, `bridge`, and `candidate_interventions` through the native `epiphany-heartbeat-store routine` command.
- `state/ledgers.msgpack`, `epiphany-core::EpiphanyStateLedgerEntry`, the `epiphany-state-ledger-store` binary, and the native `epiphany-state` CLI now own durable branch and evidence ledgers. The old tracked JSON ledger files and the migration/projection CLI hatches are gone.
- CultCache inspection, cross-language schema synchronization, and debug display tooling belong in CultLib, not in EpiphanyAgent. Epiphany should consume typed CultCache documents and expose product-specific status through its API, while general polyglot domain loading/inspection lives with the shared CultCache ecosystem.
- VoidBot's current routine is reference lineage only. Epiphany's native routine pass lives in Rust as `epiphany-heartbeat-store routine`: it reads typed role dossiers, projects memory resonance, maintains incubation themes, runs analytic and associative cognition lanes, writes bridge syntheses/saturation/tension, produces reviewed candidate interventions, advances the sleep/dream cycle, and writes an auditable routine receipt while leaving project truth and role memory mutation on their dedicated reviewed surfaces. Qdrant-backed memory vectors remain a future enhancement; the current native pass uses bounded local resonance over typed memories.
- `epiphany-character-loop` now advertises `epiphany.cognition_lanes.v0` and `epiphany.cognition_bridge.v0` in every character-turn packet. Face and future role-local actors must run an analytic lane and an associative lane, then let the bridge choose bubble, draft, finding, silence, or bounded `selfPatch`; those lanes shape attention but do not mutate project truth.
- The Rust CultCache-compatible contract now lives in dedicated upstream `https://github.com/GameCult/cultcache-rs` and is vendored as `vendor/cultcache-rs`. Epiphany should consume it as the canonical typed MessagePack persistence seam instead of growing an embedded local copy like an unsanctioned organ.
- `state/face-discord.toml` and the native `epiphany-face-discord` binary are the first Face Discord boundary. Face may interact only through #aquarium; missing or mismatched channel configuration produces candidate chat artifacts instead of posting elsewhere.
- The native `epiphany-heartbeat-store status`, the `heartbeat` block in native `epiphany-mvp-status`, and GUI actions `heartbeatStatus` / `runHeartbeat` are the first operator API surface for heartbeat initiative state and outputs. `runHeartbeat` advances one slot and emits a Discord-independent Face bubble artifact. Status now reflects the backing CultCache store so Aquarium can see whether the typed heartbeat memory is present.
- The native `epiphany-heartbeat-store status`, the `heartbeat` block in native `epiphany-mvp-status`, and GUI actions `heartbeatStatus` / `runHeartbeat` are the first operator API surface for heartbeat initiative state and outputs. `runHeartbeat` advances one slot and emits a Discord-independent Face bubble artifact; native `epiphany-heartbeat-store pump` adaptively launches concurrent deferred turns when swarm pressure justifies it. Status reflects the backing CultCache store and latest adaptive pacing receipt so Aquarium can see whether the typed heartbeat memory is present and whether the swarm is sleeping, stirring, or alarmed.
- The native `epiphany-face-discord bubble`, the `face` block in native `epiphany-mvp-status`, and GUI action `faceBubble` are the first Aquarium bubble surface. Face can open a local chat bubble artifact even when Discord channel id/token configuration is absent.
- `thread/epiphany/roleResult` and `thread/epiphany/roleAccept` now project optional specialist `selfPatch` requests with coordinator review through `selfPersistence`: good mutations are role-matched, bounded, evidence-grounded lane memory/goal/value/private-note updates; project truth, authority, code edits, graphs, checkpoints, planning, scratch, jobs, and raw transcripts are refused with reasons. GUI/coordinator acceptance paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary.
- The base Epiphany prompt now keeps Codex's useful harness DNA explicitly: AGENTS scope, concise progress updates, plan discipline, scoped edits, non-destructive filesystem/git behavior, focused validation, and concise honest finals remain first-class rather than overwritten by Epiphany metaphor. Good old scars are holy when they still protect the machine.
@ -197,7 +197,7 @@ landed_baseline:
- Phase 6 role self-memory slice: Ghostlight-shaped role dossiers live in typed CultCache `state/agents.msgpack`, specialists may request bounded `selfPatch` lane-memory mutations, coordinator review projects acceptance/refusal reasons through `selfPersistence`, and GUI/coordinator accept paths apply accepted requests with the native `epiphany-agent-memory-store` binary.
- Phase 6 Face public-surface slice: Face's typed role dossier, Face prompt text, `state/face-discord.toml`, and the native `epiphany-face-discord` binary establish Face as the public #aquarium-only translator of Epiphany agent thought-weather, without VoidBot's moderation authority.
- Phase 6 character-loop slice: native `epiphany-character-loop` loads any role's Ghostlight-shaped typed dossier from `state/agents.msgpack`, combines it with a visible stimulus, and emits an auditable `epiphany.character_turn_packet.v0` local projection for response, bubble, finding, silence, or bounded `selfPatch`. Face is the first intended public-surface consumer; Ghostlight is reference material, not a runtime dependency.
- Phase 6 heartbeat initiative slice: `state/agent-heartbeats.msgpack`, `epiphany-core::EpiphanyHeartbeatStateEntry`, the native `epiphany-heartbeat-store` binary, and heartbeat smoke coverage borrow Ghostlight initiative scheduling so Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life compete for heartbeat slots; idle slots become bounded rumination instead of fake project work, and sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. The live state surface now ticks/completes/applies heartbeat physiology through typed CultCache MessagePack instead of JSON or Python wrapper state. Role personality and current mood/anxiety both shape initiative recovery through personality, mood, and effective cooldown multipliers, so Hands-like work pressure and anxious high-need lanes can recover sooner without waking lanes whose previous turn is still running. The same store can now initialize a `ghostlight-scene` profile for character scene turns.
- Phase 6 heartbeat initiative slice: `state/agent-heartbeats.msgpack`, `epiphany-core::EpiphanyHeartbeatStateEntry`, the native `epiphany-heartbeat-store` binary, and heartbeat smoke coverage borrow Ghostlight initiative scheduling so Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life compete for heartbeat slots; idle slots become bounded rumination instead of fake project work, and sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. The live state surface now ticks/completes/pumps/applies heartbeat physiology through typed CultCache MessagePack instead of JSON or Python wrapper state. Role personality and current mood/anxiety both shape initiative recovery through personality, mood, and effective cooldown multipliers, so Hands-like work pressure and anxious high-need lanes can recover sooner without waking lanes whose previous turn is still running. Adaptive pump pressure controls tempo and concurrency: calm can launch zero and sleep slow; alarm can fill most active lanes. The same store can now initialize a `ghostlight-scene` profile for character scene turns.
- Phase 6 cognition-lanes/Void routine slice: native `epiphany-heartbeat-store routine` rebuilds VoidBot-style sleep, resonance, incubation, parallel analytic/associative thought lanes, bridge judgment, candidate interventions, Ghostlight-style participant-local thought appraisal, derived reactions, dream maintenance, and mood/anxiety timing inside Epiphany's heartbeat store. It reads typed role dossiers from `state/agents.msgpack`, projects clustered thought state through each role's personality vectors, updates `state/agent-heartbeats.msgpack`, and emits an auditable `epiphany.void_routine.v0` receipt without importing VoidBot or granting speech/project authority. `epiphany-character-loop` also carries the same cognition lane/bridge/appraisal contract for role-local actor turns; reactions may pace, color, draft, or display behavior, but state mutation remains on reviewed `selfPatch` or project-state surfaces.
- Native runtime spine slice: `epiphany-core::EpiphanyRuntimeIdentity`, `EpiphanyRuntimeSession`, `EpiphanyRuntimeJob`, `EpiphanyRuntimeJobResult`, and `EpiphanyRuntimeEvent` plus the native `epiphany-runtime-spine` binary establish a CultCache-backed spine, native job/result lifecycle, status projection, and framed CultNet hello output so the old Codex app-server runtime can be starved down to a temporary evacuation bridge instead of remaining the throne.
guardrails: