From e17676984e77c59ef74cd65c3cbae131848d0c6d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 19 May 2026 12:49:44 +0100 Subject: [PATCH] Type character-loop cognition seeds --- .../src/bin/epiphany-character-loop.rs | 259 ++++++++++-------- epiphany-core/src/lib.rs | 7 + notes/fresh-workspace-handoff.md | 4 +- state/ledgers.msgpack | Bin 130936 -> 131312 bytes state/map.yaml | 4 +- 5 files changed, 156 insertions(+), 118 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-character-loop.rs b/epiphany-core/src/bin/epiphany-character-loop.rs index 0d5fed0f..a2965f8a 100644 --- a/epiphany-core/src/bin/epiphany-character-loop.rs +++ b/epiphany-core/src/bin/epiphany-character-loop.rs @@ -6,8 +6,16 @@ use epiphany_core::EpiphanyAgentMemoryEntry; use epiphany_core::GhostlightMemory; use epiphany_core::GhostlightPerceivedStateOverlay; use epiphany_core::GhostlightTraitVector; +use epiphany_core::HeartbeatAgentReaction; +use epiphany_core::HeartbeatAgentThoughtAppraisal; +use epiphany_core::HeartbeatAppraisalReview; +use epiphany_core::HeartbeatCandidateImplications; +use epiphany_core::HeartbeatEmotionalAppraisal; +use epiphany_core::HeartbeatParticipantLocalContext; +use epiphany_core::HeartbeatPersonalityProjection; use epiphany_core::dossier_profile_for_role; use epiphany_core::load_agent_memory_entry_for_role; +use serde::Serialize; use serde_json::Value; use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -262,6 +270,38 @@ struct TraitSignal { weight: f64, } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CharacterProjectionSeed { + #[serde(rename = "schema_version")] + schema_version: String, + participant_agent_id: String, + role_id: String, + thought_cluster_ref: String, + thought_tokens: Vec, + dominant_trait_matches: Vec, + relationship_pressure: CharacterPressureSeed, + perceived_overlay_pressure: CharacterPressureSeed, + contract: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CharacterPressureSeed { + strength: f64, + summary: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct CharacterReactionSeed { + #[serde(rename = "schema_version")] + schema_version: String, + #[serde(flatten)] + reaction: HeartbeatAgentReaction, + contract: String, +} + fn deterministic_projection_seed( entry: &EpiphanyAgentMemoryEntry, stimulus: &str, @@ -269,7 +309,7 @@ fn deterministic_projection_seed( mode: &str, status: &str, mood: &str, -) -> Value { +) -> CharacterProjectionSeed { let thought_tokens = stimulus_token_set(stimulus, source, mode, status, mood); let traits = collect_trait_signals(entry); let mut scored = traits @@ -278,53 +318,50 @@ fn deterministic_projection_seed( let trait_tokens = summary_tokens(&format!("{} {}", item.group, item.name)); let overlap = token_overlap(&trait_tokens, &thought_tokens); let projection = round3((item.weight * (0.55 + overlap * 1.8)).clamp(0.0, 1.0)); - serde_json::json!({ - "group": item.group, - "name": item.name, - "activation": round3(item.activation), - "plasticity": round3(item.plasticity), - "tokenOverlap": round3(overlap), - "projection": projection, - }) + HeartbeatPersonalityProjection { + group: item.group.to_string(), + name: item.name.clone(), + activation: round3(item.activation), + plasticity: round3(item.plasticity), + token_overlap: round3(overlap), + projection, + } }) .collect::>(); - scored.sort_by(|left, right| { - right["projection"] - .as_f64() - .unwrap_or_default() - .total_cmp(&left["projection"].as_f64().unwrap_or_default()) - }); + scored.sort_by(|left, right| right.projection.total_cmp(&left.projection)); scored.truncate(6); let (relationship_pressure, relationship_summary) = strongest_relationship_pressure(entry, &thought_tokens); let (overlay_pressure, overlay_summary) = strongest_overlay_pressure(entry, &thought_tokens); - serde_json::json!({ - "schema_version": "epiphany.personality_projection.v0", - "participantAgentId": entry.agent.agent_id, - "roleId": entry.role_id, - "thoughtClusterRef": format!("{}#stimulus", source), - "thoughtTokens": thought_tokens.into_iter().collect::>(), - "dominantTraitMatches": scored, - "relationshipPressure": { - "strength": round3(relationship_pressure), - "summary": relationship_summary, + CharacterProjectionSeed { + schema_version: "epiphany.personality_projection.v0".to_string(), + participant_agent_id: entry.agent.agent_id.clone(), + role_id: entry.role_id.clone(), + thought_cluster_ref: format!("{}#stimulus", source), + thought_tokens: thought_tokens.into_iter().collect::>(), + dominant_trait_matches: scored, + relationship_pressure: CharacterPressureSeed { + strength: round3(relationship_pressure), + summary: relationship_summary, }, - "perceivedOverlayPressure": { - "strength": round3(overlay_pressure), - "summary": overlay_summary, + perceived_overlay_pressure: CharacterPressureSeed { + strength: round3(overlay_pressure), + summary: overlay_summary, }, - "contract": "Projection is participant-local and fallible. It is a seed for embodied response, not reviewed project truth.", - }) + contract: + "Projection is participant-local and fallible. It is a seed for embodied response, not reviewed project truth." + .to_string(), + } } fn deterministic_appraisal_seed( entry: &EpiphanyAgentMemoryEntry, - projection_seed: &Value, + projection_seed: &CharacterProjectionSeed, stimulus: &str, source: &str, -) -> Value { +) -> HeartbeatAgentThoughtAppraisal { let traits = collect_trait_signals(entry); let reactivity = average(traits.iter().take(8).map(|item| item.activation)).unwrap_or(0.5); let plasticity = average(traits.iter().take(8).map(|item| item.plasticity)).unwrap_or(0.5); @@ -352,17 +389,12 @@ fn deterministic_appraisal_seed( ) .unwrap_or((1.0 - expressiveness * 0.45).clamp(0.0, 1.0)); let alignment = projection_seed - .pointer("/dominantTraitMatches/0/projection") - .and_then(Value::as_f64) + .dominant_trait_matches + .first() + .map(|item| item.projection) .unwrap_or(reactivity); - let relationship_pressure = projection_seed - .pointer("/relationshipPressure/strength") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let overlay_pressure = projection_seed - .pointer("/perceivedOverlayPressure/strength") - .and_then(Value::as_f64) - .unwrap_or(0.0); + let relationship_pressure = projection_seed.relationship_pressure.strength; + let overlay_pressure = projection_seed.perceived_overlay_pressure.strength; let thought_pressure = (0.18 + (summary_tokens(stimulus).len() as f64 / 18.0).min(0.42) + relationship_pressure * 0.22 @@ -383,18 +415,17 @@ fn deterministic_appraisal_seed( round3((arousal * 0.58 + guardedness * 0.22 + thought_pressure * 0.20).clamp(0.0, 1.0)); let valence = round3((0.52 + curiosity * 0.22 - guardedness * 0.20).clamp(0.0, 1.0)); let label = interpretation_label("draft", arousal, guardedness, curiosity); - serde_json::json!({ - "schema_version": "epiphany.agent_thought_appraisal.v0", - "appraisalId": format!("face-appraisal-{}", short_id()), - "reviewStatus": "generated_unreviewed", - "participantAgentId": entry.agent.agent_id, - "roleId": entry.role_id, - "currentCharacterStateRef": format!("state/agents.msgpack#{}", entry.role_id), - "thoughtClusterRef": format!("{}#stimulus", source), - "observableThoughtSummary": stimulus, - "participantLocalContext": { - "displayName": entry.agent.identity.name, - "valueLabels": entry + HeartbeatAgentThoughtAppraisal { + schema_version: "epiphany.agent_thought_appraisal.v0".to_string(), + appraisal_id: format!("face-appraisal-{}", short_id()), + review_status: "generated_unreviewed".to_string(), + participant_agent_id: entry.agent.agent_id.clone(), + role_id: entry.role_id.clone(), + current_character_state_ref: format!("state/agents.msgpack#{}", entry.role_id), + thought_cluster_ref: format!("{}#stimulus", source), + participant_local_context: HeartbeatParticipantLocalContext { + display_name: entry.agent.identity.name.clone(), + values: entry .agent .canonical_state .values @@ -402,74 +433,74 @@ fn deterministic_appraisal_seed( .take(5) .map(|value| value.label.clone()) .collect::>(), - "reactivity": round3(reactivity), - "plasticity": round3(plasticity), - "expressiveness": round3(expressiveness), - "guardedness": round3(guarded_baseline), - "relationshipMemoryCount": entry.agent.memories.relationship_summaries.len(), - "perceivedOverlayCount": entry.agent.perceived_state_overlays.len(), + reactivity: round3(reactivity), + plasticity: round3(plasticity), + expressiveness: round3(expressiveness), + guardedness: round3(guarded_baseline), }, - "personalityProjection": projection_seed, - "interpretation": format!( + observable_thought_summary: stimulus.to_string(), + personality_projection: projection_seed.dominant_trait_matches.clone(), + interpretation: format!( "{} appraises the visible stimulus through its local personality, relationship memory, and perceived overlays; this is embodied response guidance, not omniscient truth.", entry.agent.identity.name ), - "emotionalAppraisal": { - "valence": valence, - "arousal": arousal, - "urgency": urgency, - "curiosity": curiosity, - "guardedness": guardedness, - "thoughtPressure": round3(thought_pressure), + emotional_appraisal: HeartbeatEmotionalAppraisal { + valence, + arousal, + urgency, + curiosity, + guardedness, + thought_pressure: round3(thought_pressure), }, - "interpretationLabel": label, - "candidateImplications": { - "reactionMode": reaction_mode(&label, "draft"), - "reactionIntensity": round3((urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0)), - "shouldSpeak": entry.role_id == "face" && guardedness < 0.78, - "shouldIncubate": guardedness >= 0.55, + interpretation_label: label.clone(), + confidence_notes: "Deterministic Face-local appraisal rebuilt from the same Ghostlight-style personality projection logic used by heartbeat. Useful for reaction shape, still reviewable and fallible.".to_string(), + candidate_implications: HeartbeatCandidateImplications { + reaction_mode: reaction_mode(&label, "draft").to_string(), + reaction_intensity: round3( + (urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0), + ), + should_speak: entry.role_id == "face" && guardedness < 0.78, + should_incubate: guardedness >= 0.55, }, - "confidenceNotes": "Deterministic Face-local appraisal rebuilt from the same Ghostlight-style personality projection logic used by heartbeat. Useful for reaction shape, still reviewable and fallible.", - "review": { - "acceptedForMutation": false, - "rationale": "Appraisal may steer expression and reaction; state mutation still requires explicit review.", - } - }) + review: HeartbeatAppraisalReview { + accepted_for_mutation: false, + rationale: + "Appraisal may steer expression and reaction; state mutation still requires explicit review." + .to_string(), + }, + } } -fn deterministic_reaction_seed(role_id: &str, appraisal_seed: &Value) -> Value { - let arousal = appraisal_seed - .pointer("/emotionalAppraisal/arousal") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let guardedness = appraisal_seed - .pointer("/emotionalAppraisal/guardedness") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let curiosity = appraisal_seed - .pointer("/emotionalAppraisal/curiosity") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let mode = appraisal_seed - .pointer("/candidateImplications/reactionMode") - .and_then(Value::as_str) - .unwrap_or("incubate"); - let intensity = appraisal_seed - .pointer("/candidateImplications/reactionIntensity") - .and_then(Value::as_f64) - .unwrap_or(0.0); - serde_json::json!({ - "schema_version": "epiphany.agent_reaction.v0", - "reactionId": format!("reaction-{}-{}", role_id, short_id()), - "roleId": role_id, - "appraisalId": appraisal_seed.get("appraisalId"), - "mode": mode, - "moodLabel": mood_label(arousal, guardedness, curiosity), - "intensity": round3(intensity), - "surface": if role_id == "face" { "aquarium" } else { "internal" }, - "recommendedUse": reaction_recommended_use(role_id, mode), - "contract": "Reaction is derived from local appraisal and may color speech or silence; it still does not mutate durable state by itself.", - }) +fn deterministic_reaction_seed( + role_id: &str, + appraisal_seed: &HeartbeatAgentThoughtAppraisal, +) -> CharacterReactionSeed { + let arousal = appraisal_seed.emotional_appraisal.arousal; + let guardedness = appraisal_seed.emotional_appraisal.guardedness; + let curiosity = appraisal_seed.emotional_appraisal.curiosity; + let mode = appraisal_seed.candidate_implications.reaction_mode.as_str(); + let intensity = appraisal_seed.candidate_implications.reaction_intensity; + CharacterReactionSeed { + schema_version: "epiphany.agent_reaction.v0".to_string(), + reaction: HeartbeatAgentReaction { + reaction_id: format!("reaction-{}-{}", role_id, short_id()), + role_id: role_id.to_string(), + participant_agent_id: appraisal_seed.participant_agent_id.clone(), + appraisal_id: appraisal_seed.appraisal_id.clone(), + mode: mode.to_string(), + mood_label: mood_label(arousal, guardedness, curiosity).to_string(), + intensity: round3(intensity), + bridge_decision: "draft".to_string(), + surface: if role_id == "face" { + "aquarium" + } else { + "internal" + } + .to_string(), + recommended_use: reaction_recommended_use(role_id, mode).to_string(), + }, + contract: "Reaction is derived from local appraisal and may color speech or silence; it still does not mutate durable state by itself.".to_string(), + } } fn collect_trait_signals(entry: &EpiphanyAgentMemoryEntry) -> Vec { diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 97186019..1fd09166 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -66,15 +66,22 @@ pub use heartbeat_state::HEARTBEAT_COGNITION_TYPE; pub use heartbeat_state::HEARTBEAT_STATE_KEY; pub use heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION; pub use heartbeat_state::HEARTBEAT_STATE_TYPE; +pub use heartbeat_state::HeartbeatAgentReaction; +pub use heartbeat_state::HeartbeatAgentThoughtAppraisal; +pub use heartbeat_state::HeartbeatAppraisalReview; pub use heartbeat_state::HeartbeatBirthPersonalitySeed; +pub use heartbeat_state::HeartbeatCandidateImplications; pub use heartbeat_state::HeartbeatCompleteOptions; +pub use heartbeat_state::HeartbeatEmotionalAppraisal; pub use heartbeat_state::HeartbeatHeatUpdateOptions; pub use heartbeat_state::HeartbeatHistoryEvent; pub use heartbeat_state::HeartbeatInitiativeHeatPolicy; pub use heartbeat_state::HeartbeatInitiativeMultiplier; pub use heartbeat_state::HeartbeatPacingPolicy; pub use heartbeat_state::HeartbeatParticipant; +pub use heartbeat_state::HeartbeatParticipantLocalContext; pub use heartbeat_state::HeartbeatPendingTurn; +pub use heartbeat_state::HeartbeatPersonalityProjection; pub use heartbeat_state::HeartbeatPumpOptions; pub use heartbeat_state::HeartbeatSelectionPolicy; pub use heartbeat_state::HeartbeatTickOptions; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b1bd1a6d..5357ec2a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -106,7 +106,7 @@ remembers doctrine; the branch remembers the blade. - 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. - 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. If a Face has a persona name/avatar, `post` uses the shared Discord webhook pipe so each Epiphany can speak with its own nickname and avatar without a separate bot identity. - The Void memory bridge is now native enough to keep the useful organs attached. `state/void-memory.toml` and `epiphany-void-memory` can check Void's Docker Postgres state spine, query Qdrant Discord-history and source/lore collections with the configured Ollama embedding model, and fetch raw archive context windows from Void's file-backed Discord archive. Native `epiphany-mvp-status` includes a `voidMemory` block so Aquarium/backend clients can inspect whether the mouth and memory are actually wired. -- The first Epiphany-native character-loop pass is landed. `epiphany-character-loop turn --role face --stimulus ` 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 first Epiphany-native character-loop pass is landed. `epiphany-character-loop turn --role face --stimulus ` loads Face's typed dossier from `state/agents.msgpack`, builds typed projection/appraisal/reaction seeds from the shared heartbeat cognition documents, 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, thought appraisals, and derived reactions; 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. - The native runtime-spine pass is landed as a first vertebra, not a full daemon. `state/runtime-spine.msgpack` is the default store, `.epiphany-dogfood/runtime-spine-job/runtime.msgpack` and `hello.cultnet` are the latest job/result smoke artifacts, and specialist launch/result flows now route through typed heartbeat/runtime-spine documents. Its CultNet hello advertises mutation contracts for runtime/session/job/memory/heartbeat/ledger documents, with coordinator-owned intent/receipt paths for writes. - The Aetheria dogfood run has a contamination scar. The supervising Codex session directly edited and committed target-repo work on `E:\Projects\Aetheria-Economy` instead of only driving Epiphany lanes. Treat those Aetheria commits as supervisor-seeded implementation, not clean evidence that Epiphany coordinated the work. Future dogfood must run through the GUI/coordinator/fixed role lanes with auditable artifacts unless the user explicitly authorizes an operator intervention. Remember the sunburn: do not stare into the worker's objective until you become it. @@ -199,7 +199,7 @@ The current spine, blessed but not yet finished: - 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, 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 typed sleep cycle, typed memory resonance, typed incubation, typed analytic/associative cognition lanes, typed bridge judgment/cooling, typed candidate interventions, typed Ghostlight-style personality appraisals, mood/anxiety timing, typed 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 +- Epiphany-native character-loop packet projection through `epiphany-character-loop`, with Face as the first public-surface actor over its typed role dossier and typed heartbeat-derived projection/appraisal/reaction seeds before JSON artifact serialization - Face as the public #aquarium-only surface for translating agent thought-weather into short chats or candidate drafts, with persona webhook presentation and Void memory/search access, 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 - first Unity runtime bridge through native `epiphany-unity-bridge`, native `epiphany-unity-bridge-smoke`, GUI Inspect Unity, runtime artifact listing, and implementation prompt guardrails diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 96aa975c92f73f2e5551c023f924283ef5f16d8c..ce8dfe6cc27b5f9a0a8ccbecfcecea08f6014d17 100644 GIT binary patch delta 328 zcmZXQu}T9$5QbeX6g$B})M5})$%R}nYVrhPp`cgUZFVPj3!B|@XZM0OBK9gMT;)Ru zq6j{Mui!ga+8DeG5d_N_=KsE*`5MoEUM}2j%+1a`KQ>MoE3QiRQZn}by?p=LZnd|g zRws(PNgVgKc6*(2>4B{^_L8$4C5VhWEI@79#^AGhG8yT|>(dLqjVQBP%0= FC;-^I5M%%V diff --git a/state/map.yaml b/state/map.yaml index 4427895a..fa425b3c 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -178,7 +178,7 @@ accepted_design: - `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 typed memory-resonance pair documents, maintains incubation themes, runs analytic and associative cognition lanes, writes bridge syntheses/saturation/tension, produces reviewed candidate interventions, advances typed sleep/dream-cycle state, 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. + - `epiphany-character-loop` now advertises `epiphany.cognition_lanes.v0` and `epiphany.cognition_bridge.v0` in every character-turn packet, and its projection/appraisal/reaction seeds reuse the typed heartbeat cognition documents before artifact serialization. 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 Face Discord boundary. Face may interact only through #aquarium; missing or mismatched channel configuration produces candidate chat artifacts instead of posting elsewhere. When a Face has a configured persona name/avatar, posting uses the shared Discord webhook pipe so each Epiphany can speak with its own nickname and avatar without needing a separate bot identity. - `state/void-memory.toml` and the native `epiphany-void-memory` binary are the first Void memory bridge: Epiphany can check Void's Docker Postgres state spine, query Qdrant Discord-history and source/lore vector collections through the configured Ollama embedding model, and fetch raw archive context windows from Void's Discord archive. Raw archive rows remain evidence for Face and Eyes, not public speech. @@ -236,7 +236,7 @@ landed_baseline: - Phase 6 Aquarium graph and Rider bridge slice: the Tauri/React/WebGL Aquarium embeds the adjacent EpiphanyGraph viewer over typed graph state, native `epiphany-rider-bridge` writes Rider/source-context artifacts, Aquarium Inspect Rider surfaces those artifacts, and `integrations/rider` scaffolds the first frontend-only Rider tool-window/context-action plugin. - 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`, the native `epiphany-face-discord` persona/webhook pipe, `state/void-memory.toml`, and the native `epiphany-void-memory` bridge establish Face as the public #aquarium-only translator of Epiphany agent thought-weather, with Void-derived Discord memory/search/presentation organs but 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 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, builds typed projection/appraisal/reaction seeds from the shared heartbeat cognition structs, 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/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 now also carries Void's anti-loop organs: split `noveltyToSelf`/`noveltyToRoom`, source coverage, saturation pressure, refractory cooling, and explicit permission to let a live unsaturated thought deepen without forced novelty theater. 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.