Add native Void routine physiology

This commit is contained in:
Meta Vens 2026-05-06 15:09:08 +01:00
commit 5793d074f6
7 changed files with 438 additions and 8 deletions

View file

@ -4,12 +4,14 @@ use anyhow::anyhow;
use epiphany_core::GhostlightSceneParticipantSeed;
use epiphany_core::HeartbeatCompleteOptions;
use epiphany_core::HeartbeatTickOptions;
use epiphany_core::VoidRoutineOptions;
use epiphany_core::apply_agent_self_patch;
use epiphany_core::complete_heartbeat_store;
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::run_void_routine_store;
use epiphany_core::tick_heartbeat_store;
use epiphany_core::validate_agent_memory_store;
use std::env;
@ -40,6 +42,8 @@ fn main() -> Result<()> {
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;
while let Some(arg) = args.next() {
match arg.as_str() {
@ -66,6 +70,8 @@ fn main() -> Result<()> {
"--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,
_ => return Err(anyhow!("unknown argument {arg:?}")),
}
}
@ -171,6 +177,30 @@ fn main() -> Result<()> {
);
}
}
"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);
}
"smoke" => {
let agent_store = agent_store.unwrap_or_else(|| PathBuf::from("state/agents.msgpack"));
let result = run_smoke(&agent_store)?;
@ -197,7 +227,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 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 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>]"
))
}
@ -316,6 +346,16 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
let validation_errors = validate_agent_memory_store(&temp_agent_store)?;
let status = heartbeat_status_projection(&store_path, &artifact_dir, 1.0, 8)?;
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 initiative_errors = validate_schedule_shape(&work["schedule"])
.into_iter()
.chain(validate_schedule_shape(&idle["schedule"]))
@ -338,7 +378,12 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
&& 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(8);
&& status["participants"].as_array().map(Vec::len) == Some(8)
&& 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_status["sleepCycle"]["schema_version"] == "epiphany.sleep_cycle.v0";
let result = serde_json::json!({
"ok": ok,
@ -347,6 +392,7 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
"completionEvent": completed["event"],
"idleEvent": idle["event"],
"idleRumination": idle["rumination"],
"voidRoutine": routine["routine"],
"validationErrors": validation_errors,
"initiativeErrors": initiative_errors,
});

View file

@ -1,6 +1,7 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use chrono::Duration;
use cultcache_rs::CultCache;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
@ -9,6 +10,7 @@ use serde::Serialize;
use serde_json::Value;
use std::cmp::Reverse;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
@ -17,6 +19,7 @@ pub const HEARTBEAT_STATE_KEY: &str = "default";
pub const HEARTBEAT_STATE_SCHEMA_VERSION: &str = "epiphany.agent_heartbeat.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";
const HEARTBEAT_ARENA_MAINTENANCE: &str = "maintenance";
const HEARTBEAT_ARENA_SCENE: &str = "scene";
@ -195,6 +198,13 @@ pub struct HeartbeatCompleteOptions {
pub action_id: Option<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,
@ -429,7 +439,72 @@ pub fn heartbeat_status_projection(
"latestEvent": history.last().cloned(),
"history": history,
"latestArtifacts": latest_json_artifacts(artifact_dir, artifact_limit),
"availableActions": ["init", "tick", "complete", "status"],
"sleepCycle": state.sleep_cycle,
"memoryResonance": state.memory_resonance,
"incubation": state.incubation,
"availableActions": ["init", "tick", "complete", "status", "routine"],
}))
}
pub fn run_void_routine_store(
store_path: impl AsRef<Path>,
artifact_dir: impl AsRef<Path>,
options: VoidRoutineOptions,
) -> Result<Value> {
let store_path = store_path.as_ref();
let mut state =
load_heartbeat_state_entry(store_path)?.unwrap_or_else(|| default_heartbeat_state(1.0));
patch_missing_participants(&mut state);
let memory_records = collect_role_memory_records(options.agent_store.as_deref())?;
let resonance = build_memory_resonance(&memory_records);
let incubation = build_incubation(&state.incubation, &resonance, &memory_records);
let sleep_cycle =
update_sleep_cycle(state.sleep_cycle.as_ref(), &incubation, options.allow_dream);
let run_id = format!("epiphany-void-routine-{}", now_stamp());
let routine = serde_json::json!({
"schema_version": VOID_ROUTINE_SCHEMA_VERSION,
"runId": run_id,
"source": options.source,
"referenceLineage": "VoidBot-style room stewardship, sleep, resonance, and dream maintenance rebuilt as Epiphany-native heartbeat physiology.",
"storeFile": store_path,
"agentStore": options.agent_store,
"updatedAt": now_iso(),
"sleepCycle": sleep_cycle,
"memoryResonance": resonance,
"incubation": incubation,
"reviewNotes": [
"Void is reference material, not a runtime dependency.",
"The routine mutates only typed heartbeat physiology fields; project truth and role memory mutation stay on their dedicated reviewed surfaces.",
"Sleep is maintenance: slow rumination, memory compression, and dream residue, not absence."
],
});
state.sleep_cycle = Some(sleep_cycle);
state.memory_resonance = Some(resonance);
state.incubation = Some(incubation);
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)?;
Ok(serde_json::json!({
"ok": true,
"storeFile": store_path,
"artifactPath": artifact_path,
"routine": routine,
}))
}
@ -1122,6 +1197,16 @@ 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),
});
}
let event = if payload.get("actionId").is_some() {
Some(payload)
} else {
@ -1280,16 +1365,303 @@ fn rumination_patch(role_id: &str, action_id: &str) -> Value {
})
}
#[derive(Clone, Debug)]
struct RoleMemoryRecord {
role_id: String,
memory_id: String,
summary: String,
salience: f64,
confidence: f64,
tokens: BTreeSet<String>,
}
fn collect_role_memory_records(agent_store: Option<&Path>) -> Result<Vec<RoleMemoryRecord>> {
let Some(agent_store) = agent_store else {
return Ok(Vec::new());
};
let mut records = Vec::new();
for role_id in ROLE_ORDER {
let Some(entry) =
crate::agent_memory::load_agent_memory_entry_for_role(agent_store, role_id)?
else {
continue;
};
for memory in entry
.agent
.memories
.semantic
.iter()
.chain(entry.agent.memories.episodic.iter())
.chain(entry.agent.memories.relationship_summaries.iter())
{
let tokens = summary_tokens(&memory.summary);
if tokens.is_empty() {
continue;
}
records.push(RoleMemoryRecord {
role_id: (*role_id).to_string(),
memory_id: memory.memory_id.clone(),
summary: memory.summary.clone(),
salience: memory.salience,
confidence: memory.confidence,
tokens,
});
}
}
records.sort_by(|left, right| {
(right.salience * right.confidence)
.total_cmp(&(left.salience * left.confidence))
.then_with(|| left.role_id.cmp(&right.role_id))
.then_with(|| left.memory_id.cmp(&right.memory_id))
});
records.truncate(48);
Ok(records)
}
fn build_memory_resonance(records: &[RoleMemoryRecord]) -> Value {
let mut pairs = Vec::new();
for (left_index, left) in records.iter().enumerate() {
for right in records.iter().skip(left_index + 1) {
if left.role_id == right.role_id {
continue;
}
let overlap = token_overlap(&left.tokens, &right.tokens);
if overlap <= 0.0 {
continue;
}
let strength = round3(
overlap * ((left.salience * left.confidence) + (right.salience * right.confidence))
/ 2.0,
);
if strength < 0.08 {
continue;
}
pairs.push(serde_json::json!({
"leftRole": left.role_id,
"leftMemoryId": left.memory_id,
"leftSummary": left.summary,
"rightRole": right.role_id,
"rightMemoryId": right.memory_id,
"rightSummary": right.summary,
"strength": strength,
"sharedTokens": shared_tokens(&left.tokens, &right.tokens),
}));
}
}
pairs.sort_by(|left, right| {
right["strength"]
.as_f64()
.unwrap_or_default()
.total_cmp(&left["strength"].as_f64().unwrap_or_default())
});
pairs.truncate(8);
serde_json::json!({
"schema_version": "epiphany.memory_resonance.v0",
"updatedAt": now_iso(),
"source": "epiphany-native-void-routine",
"recordCount": records.len(),
"pairs": pairs,
})
}
fn build_incubation(
previous: &Option<Value>,
resonance: &Value,
records: &[RoleMemoryRecord],
) -> Value {
let mut themes = previous
.as_ref()
.and_then(|value| value.get("themes"))
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
for pair in resonance
.get("pairs")
.and_then(Value::as_array)
.into_iter()
.flatten()
.take(4)
{
let left_role = pair
.get("leftRole")
.and_then(Value::as_str)
.unwrap_or("left");
let right_role = pair
.get("rightRole")
.and_then(Value::as_str)
.unwrap_or("right");
let tokens = pair
.get("sharedTokens")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.take(3)
.collect::<Vec<_>>()
.join("/")
})
.unwrap_or_default();
themes.push(serde_json::json!({
"themeId": format!("theme-{left_role}-{right_role}-{}", stable_theme_suffix(&tokens)),
"summary": format!("{left_role} and {right_role} keep touching {tokens}; let the swarm dream on whether that is signal or stale echo."),
"strength": pair.get("strength").cloned().unwrap_or(Value::Null),
"source": "memory_resonance",
"updatedAt": now_iso(),
}));
}
if themes.is_empty() && !records.is_empty() {
let strongest = &records[0];
themes.push(serde_json::json!({
"themeId": format!("theme-{}-strongest-memory", strongest.role_id),
"summary": format!("{} carries the hottest current memory: {}", display_name_for_role(&strongest.role_id), strongest.summary),
"strength": round3(strongest.salience * strongest.confidence),
"source": "strongest_memory",
"updatedAt": now_iso(),
}));
}
themes.sort_by(|left, right| {
right["strength"]
.as_f64()
.unwrap_or_default()
.total_cmp(&left["strength"].as_f64().unwrap_or_default())
});
themes.dedup_by(|left, right| left.get("themeId") == right.get("themeId"));
themes.truncate(12);
serde_json::json!({
"schema_version": "epiphany.incubation.v0",
"updatedAt": now_iso(),
"themes": themes,
})
}
fn update_sleep_cycle(previous: Option<&Value>, incubation: &Value, allow_dream: bool) -> Value {
let cycle_hours = previous
.and_then(|value| value.get("cycleHours"))
.and_then(Value::as_i64)
.unwrap_or(4)
.clamp(2, 12);
let nap_duration_minutes = previous
.and_then(|value| value.get("napDurationMinutes"))
.and_then(Value::as_i64)
.unwrap_or(60)
.clamp(15, cycle_hours * 60 - 5);
let phase_offset_minutes = previous
.and_then(|value| value.get("phaseOffsetMinutesLocal"))
.and_then(Value::as_i64)
.unwrap_or(120)
.clamp(0, cycle_hours * 60 - 1);
let now = chrono::Utc::now();
let cycle_minutes = cycle_hours * 60;
let shifted = now.timestamp().div_euclid(60) - phase_offset_minutes;
let minute_in_cycle = shifted.rem_euclid(cycle_minutes);
let is_napping = minute_in_cycle < nap_duration_minutes;
let cycle_start_minute = shifted - minute_in_cycle + phase_offset_minutes;
let nap_start =
chrono::DateTime::<chrono::Utc>::from_timestamp(cycle_start_minute * 60, 0).unwrap_or(now);
let nap_end = nap_start + Duration::minutes(nap_duration_minutes);
let next_nap_start = if is_napping {
nap_start + Duration::minutes(cycle_minutes)
} else if minute_in_cycle >= nap_duration_minutes {
nap_start + Duration::minutes(cycle_minutes)
} else {
nap_start
};
let active_themes = incubation
.get("themes")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|theme| theme.get("themeId").and_then(Value::as_str))
.take(4)
.map(str::to_string)
.collect::<Vec<_>>();
let previous_dream_count = previous
.and_then(|value| value.get("dreamCountInCurrentNap"))
.and_then(Value::as_u64)
.unwrap_or(0);
let dream_count = if is_napping && allow_dream {
previous_dream_count.saturating_add(1)
} else if is_napping {
previous_dream_count
} else {
0
};
serde_json::json!({
"schema_version": "epiphany.sleep_cycle.v0",
"enabled": true,
"cycleHours": cycle_hours,
"napDurationMinutes": nap_duration_minutes,
"phaseOffsetMinutesLocal": phase_offset_minutes,
"replyMode": "sleep_rumination",
"isNapping": is_napping,
"currentNapStartedAt": if is_napping { Some(nap_start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) } else { None },
"currentNapEndsAt": if is_napping { Some(nap_end.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) } else { None },
"nextNapStartsAt": next_nap_start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"lastDreamAt": if is_napping && allow_dream { Some(now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) } else { previous.and_then(|value| value.get("lastDreamAt")).cloned().and_then(|value| value.as_str().map(str::to_string)) },
"dreamCountInCurrentNap": dream_count,
"activeDreamThemes": active_themes,
"lastDistillationSummary": if is_napping {
"Sleep pass prefers memory compression, resonance cooling, and dream residue over active work."
} else {
"Awake pass keeps resonance/incubation fresh without speaking unless Face has a real surface reason."
},
})
}
fn summary_tokens(summary: &str) -> BTreeSet<String> {
summary
.split(|ch: char| !ch.is_ascii_alphanumeric())
.map(|part| part.trim().to_ascii_lowercase())
.filter(|part| part.len() >= 4 && !STOP_WORDS.contains(&part.as_str()))
.collect()
}
fn token_overlap(left: &BTreeSet<String>, right: &BTreeSet<String>) -> f64 {
let shared = left.intersection(right).count() as f64;
if shared == 0.0 {
return 0.0;
}
let union = left.union(right).count() as f64;
if union <= 0.0 { 0.0 } else { shared / union }
}
fn shared_tokens(left: &BTreeSet<String>, right: &BTreeSet<String>) -> Vec<String> {
left.intersection(right).take(8).cloned().collect()
}
fn stable_theme_suffix(value: &str) -> String {
let mut hash = 5381_u64;
for byte in value.as_bytes() {
hash = ((hash << 5).wrapping_add(hash)).wrapping_add(*byte as u64);
}
format!("{:x}", hash & 0xffff)
}
fn now_iso() -> String {
chrono::Utc::now()
.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
.replace('Z', "+00:00")
}
fn now_stamp() -> String {
chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string()
}
fn round6(value: f64) -> f64 {
(value * 1_000_000.0).round() / 1_000_000.0
}
fn round3(value: f64) -> f64 {
(value * 1_000.0).round() / 1_000.0
}
const STOP_WORDS: &[&str] = &[
"about", "after", "agent", "before", "being", "between", "could", "from", "have", "into",
"lane", "memory", "more", "must", "should", "state", "than", "that", "their", "there", "this",
"through", "when", "with", "work",
];
fn agent_id_for_role(role_id: &str) -> &'static str {
match role_id {
"coordinator" => "epiphany.self",

View file

@ -41,6 +41,7 @@ pub use heartbeat_state::HeartbeatParticipant;
pub use heartbeat_state::HeartbeatPendingTurn;
pub use heartbeat_state::HeartbeatSelectionPolicy;
pub use heartbeat_state::HeartbeatTickOptions;
pub use heartbeat_state::VoidRoutineOptions;
pub use heartbeat_state::complete_heartbeat_store;
pub use heartbeat_state::default_heartbeat_state;
pub use heartbeat_state::ghostlight_scene_heartbeat_state;
@ -49,6 +50,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::run_void_routine_store;
pub use heartbeat_state::tick_heartbeat_store;
pub use heartbeat_state::validate_heartbeat_state;
pub use heartbeat_state::write_heartbeat_state_entry;

View file

@ -45,6 +45,7 @@ remembers doctrine; the branch remembers the blade.
- 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, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Tick/status/complete and idle rumination selfPatch application now run through Rust/CultCache directly. 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, 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.
- The Face public-surface pass is landed as a bounded lane. Face's role dossier lives in `state/agents.msgpack`; `epiphany_specialists.toml` gives it a VoidBot-heartbeat-derived prompt stripped of moderation authority; `state/face-discord.toml` and the native `epiphany-face-discord` binary enforce that Face may interact only through #aquarium. Missing channel id or token writes candidate chat artifacts instead of posting elsewhere.
- The first Epiphany-native character-loop pass is landed. `epiphany-character-loop turn --role face --stimulus <text>` loads Face's typed dossier from `state/agents.msgpack` and emits an auditable `epiphany.character_turn_packet.v0` packet with projected local context, visible stimulus, allowed outputs, and guardrails. The same packet builder works for other role ids, because every organ already has a Ghostlight-shaped dossier.
- The heartbeat/Face operator API pass is landed. Native `epiphany-heartbeat-store status` returns machine-readable initiative state plus CultCache store presence, native `epiphany-face-discord bubble` writes Discord-independent `epiphany.face_bubble.v0` artifacts, and native `epiphany-mvp-status` includes `heartbeat` and `face` blocks. Aquarium should call native/backend surfaces rather than resurrecting deleted Python action shims.
@ -63,7 +64,7 @@ remembers doctrine; the branch remembers the blade.
- The planning loop is now runtime-backed and operator-visible. Typed captures, backlog items, roadmap streams, Objective Drafts, and GitHub issue source refs live in `EpiphanyThreadState`, validate through revision-gated `thread/epiphany/update`, render into prompts when present, project read-only through `thread/epiphany/planning`, render in the GUI Planning panel, can be synthesized by the fixed Imagination/planning lane, and can be explicitly adopted through the artifacted `adoptObjectiveDraft` GUI action. Chat is deliberation, not an objective pipe; ideas and GitHub Issues remain planning state until explicit human adoption.
- The bridge/dashboard slice is now landed far enough to test locally. Native `epiphany-rider-bridge` inspects Rider installation, solution, VCS, changed files, and captures file/selection/symbol context into `.epiphany-gui/rider` artifacts; Aquarium has Inspect Rider and renders Rider audit artifacts in Environment; Aquarium also embeds the adjacent EpiphanyGraph viewer over typed `graphs.architecture`, `graphs.dataflow`, and `graphs.links`.
- A first Rider plugin scaffold lives under `integrations/rider`: tool window, Refresh status, and Send Context to Epiphany action. It shells through native `epiphany-rider-bridge` and does not own state. `gradle` is not installed on this machine, so the scaffold is not build-verified yet.
- Next real product move: continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, bridge tools, heartbeat scheduling, Face guardrails, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultNet organs. Then build-verify/package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity `6000.1.10f1` editor, and run the next Aetheria dogfood pass through Aquarium/Rider/Unity bridge artifacts. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
- Next real product move: continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; surface native heartbeat `sleepCycle`, `memoryResonance`, and `incubation`; keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, bridge tools, heartbeat scheduling, Face guardrails, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultNet organs. Then build-verify/package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity `6000.1.10f1` editor, and run the next Aetheria dogfood pass through Aquarium/Rider/Unity bridge artifacts. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
- The repo is an Epiphany fork of Codex, not a Codex preset.
- `vendor/codex` is tracked directly, not a submodule.
- `epiphany-core` owns the heavy Epiphany organs where practical.
@ -138,6 +139,7 @@ The current spine, blessed but not yet finished:
- 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, idle rumination routed through normal `selfPatch` review/application, 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, 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
- Ghostlight-shaped role dossiers in `state/agents.msgpack`, plus `selfPatch` review projection through `roleResult`/`roleAccept` and accepted memory application through the native `epiphany-agent-memory-store` binary
@ -277,6 +279,12 @@ For Epiphany-native character-loop packet projection, run:
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-character-loop -- smoke
```
For the native Void-derived routine pass, run:
```powershell
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-heartbeat-store -- routine --store .\state\agent-heartbeats.msgpack --artifact-dir .\.epiphany-heartbeats --agent-store .\state\agents.msgpack
```
For GUI Objective Draft adoption behavior, run:
```powershell

Binary file not shown.

Binary file not shown.

View file

@ -66,6 +66,7 @@ active_subgoals:
- Keep the landed Phase 6 coordinator, limited safe-boundary CRRC automation, and pre-compaction checkpoint intervention ready for the next bridge-equipped dogfood run.
- Preserve the Ghostlight-derived heartbeat initiative seam: every lane, including coordinator/Self and Face, has a role dossier and competes for heartbeat slots through readiness/reaction timing; idle winners ruminate and distill self-memory instead of inventing project work.
- Treat Ghostlight as reference lineage, not a sibling runtime to preserve. The useful organ is Epiphany-native character-loop protocol: any role can project its typed dossier plus visible stimulus into a local turn packet, with Face as the first public-surface actor.
- Treat VoidBot as reference lineage, not a runtime dependency. The useful organ is Epiphany-native Void routine physiology: heartbeat state owns sleep cycle, memory resonance, incubation, dream residue, and Aquarium-visible routine receipts over typed role dossiers.
- Preserve review gates for semantic findings; automatic CRRC may compact or launch the fixed reorient-worker, but not accept or promote.
- Keep persistent project state ruthlessly distilled so rehydration strengthens the next awakening instead of drowning it in vellum.
- Quarantine the Aetheria dogfood contamination: future target-repo work must be driven through Epiphany's GUI/coordinator/fixed lanes with auditable artifacts, not direct supervisor implementation.
@ -132,10 +133,10 @@ 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, and constraints; pending coordinator work can pull its owning lane through a reaction window; otherwise the earliest ready active participant wins the slot. If the winner has no routeable maintenance work, it ruminates on role quality and can apply a bounded selfPatch directly through the normal coordinator-reviewed memory rules when `--agent-store` and `--apply-rumination` are provided. 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.
- `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, and constraints; pending coordinator work can pull its owning lane through a reaction window; otherwise the earliest ready active participant wins the slot. If the winner has no routeable maintenance work, it ruminates on role quality and can apply a bounded selfPatch directly through the normal coordinator-reviewed memory rules when `--agent-store` and `--apply-rumination` are provided. 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`, and `incubation` 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 the reference for the next Epiphany heartbeat/state substrate: work, rumination, and sleep turns; completion-gated cooldown; typed CultCache state; Qdrant-backed memory vectors; resonance/incubation/dream state; and Aquarium-visible internals. The first MessagePack canonical state slice is now the heartbeat store through CultCache-compatible typed Rust state. See `notes/voidbot-routine-adoption-plan.md`.
- 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, 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.
- 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.
@ -194,6 +195,7 @@ landed_baseline:
- 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 memory rumination instead of fake project work, and the live state surface now ticks/completes/applies rumination through typed CultCache MessagePack instead of JSON or Python wrapper state. The same store can now initialize a `ghostlight-scene` profile for character scene turns.
- Phase 6 Void routine slice: native `epiphany-heartbeat-store routine` rebuilds VoidBot-style sleep, resonance, incubation, and dream maintenance inside Epiphany's heartbeat store. It reads typed role dossiers from `state/agents.msgpack`, updates `state/agent-heartbeats.msgpack`, and emits an auditable `epiphany.void_routine.v0` receipt without importing VoidBot or granting speech/project authority.
- 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:
- Use focused Rust app-server mapper/handler tests plus matching native `epiphany-core` smoke binaries before modifying update, distill, propose, promote, read-projection, or notification behavior. The old Python smoke shims have been cut.
@ -238,5 +240,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state through Phase 6 coordinator/specialist/CRRC/backend organs are landed. The Aquarium operator shrine has been split into sibling repo `E:\Projects\EpiphanyAquarium` with its own distilled interface doctrine and persistent memory. EpiphanyAgent now also has a Ghostlight-derived heartbeat initiative seam, Epiphany-native character-loop packets over its own role dossiers, a Face lane for public agent thought-weather, and a Discord-independent Face bubble API for Aquarium. Runtime Unity execution remains blocked until the exact project-pinned editor exists.
next_action: Continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; configure the #aquarium channel id before allowing Face to post; wire Aquarium Face interactions to `epiphany-character-loop` packets before choosing bubble/draft/silence; keep EpiphanyAgent changes to backend contracts, typed state, coordinator policy, bridge tools, heartbeat scheduling, and Face guardrails. Keep purging Python/JSON impurity only when the replacement is a typed Rust, CultCache, or CultNet surface with receipts.
summary: Phase 1 durable state through Phase 6 coordinator/specialist/CRRC/backend organs are landed. The Aquarium operator shrine has been split into sibling repo `E:\Projects\EpiphanyAquarium` with its own distilled interface doctrine and persistent memory. EpiphanyAgent now also has a Ghostlight-derived heartbeat initiative seam, Epiphany-native character-loop packets over its own role dossiers, VoidBot-derived native routine physiology in the heartbeat store, a Face lane for public agent thought-weather, and a Discord-independent Face bubble API for Aquarium. Runtime Unity execution remains blocked until the exact project-pinned editor exists.
next_action: Continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; configure the #aquarium channel id before allowing Face to post; wire Aquarium Face interactions to `epiphany-character-loop` packets before choosing bubble/draft/silence; surface heartbeat `sleepCycle`, `memoryResonance`, and `incubation` from native status in Aquarium; keep EpiphanyAgent changes to backend contracts, typed state, coordinator policy, bridge tools, heartbeat scheduling, and Face guardrails. Keep purging Python/JSON impurity only when the replacement is a typed Rust, CultCache, or CultNet surface with receipts.