Add memory graph heartbeat profile import
This commit is contained in:
parent
17542726e6
commit
4ea3f4e49d
7 changed files with 436 additions and 2 deletions
|
|
@ -104,6 +104,7 @@ pub use memory_graph::memory_graph_domain_id;
|
|||
pub use memory_graph::memory_graph_edge_id;
|
||||
pub use memory_graph::memory_graph_from_agent_memories;
|
||||
pub use memory_graph::memory_graph_from_epiphany_graphs;
|
||||
pub use memory_graph::memory_graph_from_heartbeat_cognition;
|
||||
pub use memory_graph::memory_graph_node_id;
|
||||
pub use memory_graph::plan_memory_graph_context_cut;
|
||||
pub use memory_graph::validate_memory_graph_entry;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ pub use ids::memory_graph_edge_id;
|
|||
pub use ids::memory_graph_node_id;
|
||||
pub use profiles::memory_graph_from_agent_memories;
|
||||
pub use profiles::memory_graph_from_epiphany_graphs;
|
||||
pub use profiles::memory_graph_from_heartbeat_cognition;
|
||||
pub use store::EpiphanyMemoryGraphEntry;
|
||||
pub use store::MEMORY_GRAPH_KEY;
|
||||
pub use store::MEMORY_GRAPH_SCHEMA_VERSION;
|
||||
|
|
|
|||
430
epiphany-core/src/memory_graph/profiles/heartbeat.rs
Normal file
430
epiphany-core/src/memory_graph/profiles/heartbeat.rs
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
use crate::heartbeat_state::EpiphanyHeartbeatCognitionEntry;
|
||||
use crate::memory_graph::EpiphanyMemoryDomain;
|
||||
use crate::memory_graph::EpiphanyMemoryGraphSnapshot;
|
||||
use crate::memory_graph::EpiphanyMemoryLifecycle;
|
||||
use crate::memory_graph::EpiphanyMemoryNode;
|
||||
use crate::memory_graph::EpiphanyMemoryNodeKind;
|
||||
use crate::memory_graph::EpiphanyMemoryProfile;
|
||||
use crate::memory_graph::EpiphanyMemorySummary;
|
||||
use crate::memory_graph::memory_graph_domain_id;
|
||||
use crate::memory_graph::memory_graph_node_id;
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn memory_graph_from_heartbeat_cognition(
|
||||
graph_id: impl Into<String>,
|
||||
cognition: &EpiphanyHeartbeatCognitionEntry,
|
||||
) -> EpiphanyMemoryGraphSnapshot {
|
||||
let mut domains = Vec::new();
|
||||
let mut nodes = Vec::new();
|
||||
let mut summaries = Vec::new();
|
||||
|
||||
import_resonance(cognition, &mut domains, &mut nodes, &mut summaries);
|
||||
import_incubation(cognition, &mut domains, &mut nodes, &mut summaries);
|
||||
import_candidate_interventions(cognition, &mut domains, &mut nodes, &mut summaries);
|
||||
import_bridge_pressure(cognition, &mut domains, &mut nodes, &mut summaries);
|
||||
|
||||
EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some("epiphany.memory_graph.v0".to_string()),
|
||||
graph_id: graph_id.into(),
|
||||
domains,
|
||||
nodes,
|
||||
summaries,
|
||||
freshness: Some(crate::memory_graph::EpiphanyMemoryFreshness {
|
||||
status: crate::memory_graph::EpiphanyMemoryFreshnessStatus::Ready,
|
||||
note: Some(
|
||||
"Imported from heartbeat cognition as provisional memory pressure.".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn import_resonance(
|
||||
cognition: &EpiphanyHeartbeatCognitionEntry,
|
||||
domains: &mut Vec<EpiphanyMemoryDomain>,
|
||||
nodes: &mut Vec<EpiphanyMemoryNode>,
|
||||
summaries: &mut Vec<EpiphanyMemorySummary>,
|
||||
) {
|
||||
let Some(resonance) = cognition.memory_resonance.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(pairs) = resonance.get("pairs").and_then(Value::as_array) else {
|
||||
return;
|
||||
};
|
||||
let domain_id = push_domain(
|
||||
domains,
|
||||
EpiphanyMemoryProfile::ShortTerm,
|
||||
"heartbeat resonance",
|
||||
);
|
||||
let before = nodes.len();
|
||||
for pair in pairs.iter().take(8) {
|
||||
let left = string_at(pair, "leftSummary");
|
||||
let right = string_at(pair, "rightSummary");
|
||||
let left_role = string_at(pair, "leftRole");
|
||||
let right_role = string_at(pair, "rightRole");
|
||||
let id_seed = format!(
|
||||
"{}:{}:{}",
|
||||
left_role,
|
||||
right_role,
|
||||
string_at(pair, "leftMemoryId")
|
||||
);
|
||||
nodes.push(EpiphanyMemoryNode {
|
||||
id: memory_graph_node_id(&domain_id, "resonance", id_seed, None),
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::ShortTerm,
|
||||
kind: EpiphanyMemoryNodeKind::ShortTermThought,
|
||||
title: format!("{left_role}/{right_role} resonance"),
|
||||
claim: format!("{left} / {right}"),
|
||||
question: "Is this resonance signal, stale echo, or a branch worth following?"
|
||||
.to_string(),
|
||||
tension: "Shared vocabulary is not proof of shared truth.".to_string(),
|
||||
action_implication:
|
||||
"Treat as provisional attention pressure until sleep or review accounts for it."
|
||||
.to_string(),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: EpiphanyMemoryLifecycle::Active,
|
||||
salience: score_to_u32(number_at(pair, "strength")),
|
||||
confidence: 55,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
push_summary(
|
||||
summaries,
|
||||
&domain_id,
|
||||
"summary-heartbeat-resonance",
|
||||
"heartbeat resonance",
|
||||
"Heartbeat resonance produced provisional short-term thought nodes.",
|
||||
&nodes[before..],
|
||||
EpiphanyMemoryProfile::ShortTerm,
|
||||
);
|
||||
}
|
||||
|
||||
fn import_incubation(
|
||||
cognition: &EpiphanyHeartbeatCognitionEntry,
|
||||
domains: &mut Vec<EpiphanyMemoryDomain>,
|
||||
nodes: &mut Vec<EpiphanyMemoryNode>,
|
||||
summaries: &mut Vec<EpiphanyMemorySummary>,
|
||||
) {
|
||||
let Some(incubation) = cognition.incubation.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(themes) = incubation.get("themes").and_then(Value::as_array) else {
|
||||
return;
|
||||
};
|
||||
let domain_id = push_domain(
|
||||
domains,
|
||||
EpiphanyMemoryProfile::Incubation,
|
||||
"heartbeat incubation",
|
||||
);
|
||||
let before = nodes.len();
|
||||
for theme in themes.iter().take(12) {
|
||||
let theme_id = string_at(theme, "themeId");
|
||||
let status = string_at(theme, "status");
|
||||
nodes.push(EpiphanyMemoryNode {
|
||||
id: memory_graph_node_id(&domain_id, "incubation", &theme_id, None),
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::Incubation,
|
||||
kind: EpiphanyMemoryNodeKind::IncubationThread,
|
||||
title: theme_id.clone(),
|
||||
claim: string_at(theme, "summary"),
|
||||
question: string_at(theme, "latentQuestion"),
|
||||
tension: string_at(theme, "holdingCloseBecause"),
|
||||
action_implication: string_at(theme, "whyItPulls"),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: incubation_lifecycle(&status),
|
||||
salience: score_to_u32(number_at(theme, "priorityScore")),
|
||||
confidence: score_to_u32(number_at(theme, "maturation")),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
push_summary(
|
||||
summaries,
|
||||
&domain_id,
|
||||
"summary-heartbeat-incubation",
|
||||
"heartbeat incubation",
|
||||
"Heartbeat incubation produced provisional incubation threads.",
|
||||
&nodes[before..],
|
||||
EpiphanyMemoryProfile::Incubation,
|
||||
);
|
||||
}
|
||||
|
||||
fn import_candidate_interventions(
|
||||
cognition: &EpiphanyHeartbeatCognitionEntry,
|
||||
domains: &mut Vec<EpiphanyMemoryDomain>,
|
||||
nodes: &mut Vec<EpiphanyMemoryNode>,
|
||||
summaries: &mut Vec<EpiphanyMemorySummary>,
|
||||
) {
|
||||
let Some(candidates) = cognition.candidate_interventions.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(items) = candidates.get("items").and_then(Value::as_array) else {
|
||||
return;
|
||||
};
|
||||
let domain_id = push_domain(
|
||||
domains,
|
||||
EpiphanyMemoryProfile::CandidateIntervention,
|
||||
"heartbeat candidate interventions",
|
||||
);
|
||||
let before = nodes.len();
|
||||
for item in items.iter().take(8) {
|
||||
let intervention_id = string_at(item, "interventionId");
|
||||
nodes.push(EpiphanyMemoryNode {
|
||||
id: memory_graph_node_id(&domain_id, "candidate", &intervention_id, None),
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::CandidateIntervention,
|
||||
kind: EpiphanyMemoryNodeKind::CandidateIntervention,
|
||||
title: string_at(item, "summary"),
|
||||
claim: string_at(item, "draft"),
|
||||
question: "Should this candidate intervention be spoken, deferred, or retired?"
|
||||
.to_string(),
|
||||
tension: "Candidate speech is not authority and still requires review.".to_string(),
|
||||
action_implication: "Route through Face/coordinator review before public surfacing."
|
||||
.to_string(),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: EpiphanyMemoryLifecycle::Queued,
|
||||
salience: score_to_u32(number_at(item, "noveltyToRoom")),
|
||||
confidence: 55,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
push_summary(
|
||||
summaries,
|
||||
&domain_id,
|
||||
"summary-heartbeat-candidates",
|
||||
"heartbeat candidate interventions",
|
||||
"Heartbeat produced review-gated candidate interventions.",
|
||||
&nodes[before..],
|
||||
EpiphanyMemoryProfile::CandidateIntervention,
|
||||
);
|
||||
}
|
||||
|
||||
fn import_bridge_pressure(
|
||||
cognition: &EpiphanyHeartbeatCognitionEntry,
|
||||
domains: &mut Vec<EpiphanyMemoryDomain>,
|
||||
nodes: &mut Vec<EpiphanyMemoryNode>,
|
||||
summaries: &mut Vec<EpiphanyMemorySummary>,
|
||||
) {
|
||||
let Some(bridge) = cognition.bridge.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let decision = bridge
|
||||
.pointer("/decision/speakDecision")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("silence");
|
||||
if decision == "silence" {
|
||||
return;
|
||||
}
|
||||
let domain_id = push_domain(
|
||||
domains,
|
||||
EpiphanyMemoryProfile::AgencyPressure,
|
||||
"heartbeat bridge pressure",
|
||||
);
|
||||
let reason = bridge
|
||||
.pointer("/decision/reason")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("Bridge pressure has no explicit reason.");
|
||||
let node = EpiphanyMemoryNode {
|
||||
id: memory_graph_node_id(&domain_id, "bridge-pressure", decision, None),
|
||||
domain_id: domain_id.clone(),
|
||||
profile: EpiphanyMemoryProfile::AgencyPressure,
|
||||
kind: EpiphanyMemoryNodeKind::AgencyPressure,
|
||||
title: format!("bridge decision: {decision}"),
|
||||
claim: reason.to_string(),
|
||||
question: "Does this bridge pressure require action, cooling, or silence?".to_string(),
|
||||
tension: "Bridge pressure can steer attention but cannot mutate durable truth.".to_string(),
|
||||
action_implication: "Coordinator or Face may review this pressure; no automatic speech or state write follows.".to_string(),
|
||||
source_hashes: vec!["anchor:missing".to_string()],
|
||||
lifecycle: if decision == "draft" {
|
||||
EpiphanyMemoryLifecycle::Obligated
|
||||
} else {
|
||||
EpiphanyMemoryLifecycle::Active
|
||||
},
|
||||
salience: 65,
|
||||
confidence: 55,
|
||||
..Default::default()
|
||||
};
|
||||
nodes.push(node);
|
||||
push_summary(
|
||||
summaries,
|
||||
&domain_id,
|
||||
"summary-heartbeat-bridge-pressure",
|
||||
"heartbeat bridge pressure",
|
||||
"Heartbeat bridge produced provisional agency pressure.",
|
||||
nodes.last().map(std::slice::from_ref).unwrap_or(&[]),
|
||||
EpiphanyMemoryProfile::AgencyPressure,
|
||||
);
|
||||
}
|
||||
|
||||
fn push_domain(
|
||||
domains: &mut Vec<EpiphanyMemoryDomain>,
|
||||
profile: EpiphanyMemoryProfile,
|
||||
name: &str,
|
||||
) -> String {
|
||||
let domain_id = memory_graph_domain_id(profile, "heartbeat", name);
|
||||
if !domains.iter().any(|domain| domain.id == domain_id) {
|
||||
domains.push(EpiphanyMemoryDomain {
|
||||
id: domain_id.clone(),
|
||||
profile,
|
||||
title: name.to_string(),
|
||||
description: Some(
|
||||
"Heartbeat cognition imported as provisional memory graph pressure.".to_string(),
|
||||
),
|
||||
lifecycle: domain_lifecycle(profile),
|
||||
});
|
||||
}
|
||||
domain_id
|
||||
}
|
||||
|
||||
fn domain_lifecycle(profile: EpiphanyMemoryProfile) -> EpiphanyMemoryLifecycle {
|
||||
match profile {
|
||||
EpiphanyMemoryProfile::CandidateIntervention => EpiphanyMemoryLifecycle::Queued,
|
||||
_ => EpiphanyMemoryLifecycle::Active,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_summary(
|
||||
summaries: &mut Vec<EpiphanyMemorySummary>,
|
||||
domain_id: &str,
|
||||
id: &str,
|
||||
target: &str,
|
||||
claim: &str,
|
||||
nodes: &[EpiphanyMemoryNode],
|
||||
profile: EpiphanyMemoryProfile,
|
||||
) {
|
||||
if nodes.is_empty() {
|
||||
return;
|
||||
}
|
||||
summaries.push(EpiphanyMemorySummary {
|
||||
id: id.to_string(),
|
||||
domain_id: domain_id.to_string(),
|
||||
covers_node_ids: nodes.iter().map(|node| node.id.clone()).collect(),
|
||||
target: target.to_string(),
|
||||
claim: claim.to_string(),
|
||||
question: "What should sleep or review do with this provisional pressure?".to_string(),
|
||||
tension: "Heartbeat cognition is provisional physiology, not durable doctrine.".to_string(),
|
||||
action_implication: "Do not promote without a later sleep/review operation.".to_string(),
|
||||
anchor_count: 0,
|
||||
freshness: crate::memory_graph::EpiphanyMemoryFreshnessStatus::Ready,
|
||||
confidence: if profile == EpiphanyMemoryProfile::CandidateIntervention {
|
||||
60
|
||||
} else {
|
||||
55
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn string_at(value: &Value, key: &str) -> String {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn number_at(value: &Value, key: &str) -> f64 {
|
||||
value.get(key).and_then(Value::as_f64).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn score_to_u32(value: f64) -> u32 {
|
||||
if value.is_finite() {
|
||||
(value.clamp(0.0, 1.0) * 100.0).round() as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn incubation_lifecycle(status: &str) -> EpiphanyMemoryLifecycle {
|
||||
match status {
|
||||
"ripe" | "deepening" => EpiphanyMemoryLifecycle::Deepening,
|
||||
"refractory" | "stalled" => EpiphanyMemoryLifecycle::Cooling,
|
||||
_ => EpiphanyMemoryLifecycle::Active,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::heartbeat_state::HEARTBEAT_COGNITION_SCHEMA_VERSION;
|
||||
use crate::memory_graph::validate_memory_graph_snapshot;
|
||||
|
||||
#[test]
|
||||
fn heartbeat_profile_imports_provisional_pressure_without_promoting_it() {
|
||||
let cognition = EpiphanyHeartbeatCognitionEntry {
|
||||
schema_version: HEARTBEAT_COGNITION_SCHEMA_VERSION.to_string(),
|
||||
updated_at: "2026-05-18T00:00:00Z".to_string(),
|
||||
latest_run_id: Some("run-1".to_string()),
|
||||
latest_artifact_ref: None,
|
||||
source: Some("unit-test".to_string()),
|
||||
sleep_cycle: None,
|
||||
memory_resonance: Some(serde_json::json!({
|
||||
"pairs": [{
|
||||
"leftRole": "body",
|
||||
"rightRole": "soul",
|
||||
"leftMemoryId": "mem-a",
|
||||
"leftSummary": "Architecture needs anchors.",
|
||||
"rightSummary": "Verification needs receipts.",
|
||||
"strength": 0.8
|
||||
}]
|
||||
})),
|
||||
incubation: Some(serde_json::json!({
|
||||
"themes": [{
|
||||
"themeId": "theme-body-soul",
|
||||
"summary": "Body and Soul keep touching evidence boundaries.",
|
||||
"latentQuestion": "Should this become doctrine?",
|
||||
"holdingCloseBecause": "It is live but not settled.",
|
||||
"whyItPulls": "It affects future review gates.",
|
||||
"status": "deepening",
|
||||
"priorityScore": 0.72,
|
||||
"maturation": 0.6
|
||||
}]
|
||||
})),
|
||||
thought_lanes: None,
|
||||
bridge: Some(serde_json::json!({
|
||||
"decision": {
|
||||
"speakDecision": "hold",
|
||||
"reason": "The thought is live but not ready for speech."
|
||||
}
|
||||
})),
|
||||
candidate_interventions: Some(serde_json::json!({
|
||||
"items": [{
|
||||
"interventionId": "candidate-theme-body-soul",
|
||||
"summary": "Possible note",
|
||||
"draft": "Evidence boundaries are lighting up.",
|
||||
"noveltyToRoom": 0.7
|
||||
}]
|
||||
})),
|
||||
appraisals: None,
|
||||
reactions: None,
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let snapshot = memory_graph_from_heartbeat_cognition("heartbeat-profile", &cognition);
|
||||
let errors = validate_memory_graph_snapshot(&snapshot);
|
||||
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
assert!(snapshot.nodes.iter().any(|node| {
|
||||
node.profile == EpiphanyMemoryProfile::ShortTerm
|
||||
&& node.lifecycle == EpiphanyMemoryLifecycle::Active
|
||||
}));
|
||||
assert!(snapshot.nodes.iter().any(|node| {
|
||||
node.profile == EpiphanyMemoryProfile::Incubation
|
||||
&& node.lifecycle == EpiphanyMemoryLifecycle::Deepening
|
||||
}));
|
||||
assert!(snapshot.nodes.iter().any(|node| {
|
||||
node.profile == EpiphanyMemoryProfile::CandidateIntervention
|
||||
&& node.lifecycle == EpiphanyMemoryLifecycle::Queued
|
||||
}));
|
||||
assert!(snapshot.nodes.iter().any(|node| {
|
||||
node.profile == EpiphanyMemoryProfile::AgencyPressure
|
||||
&& node.lifecycle == EpiphanyMemoryLifecycle::Active
|
||||
}));
|
||||
assert!(
|
||||
snapshot
|
||||
.nodes
|
||||
.iter()
|
||||
.all(|node| node.lifecycle != EpiphanyMemoryLifecycle::Promoted)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod agent;
|
||||
pub mod heartbeat;
|
||||
pub mod repo;
|
||||
|
||||
pub use agent::memory_graph_from_agent_memories;
|
||||
pub use heartbeat::memory_graph_from_heartbeat_cognition;
|
||||
pub use repo::memory_graph_from_epiphany_graphs;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ remembers doctrine; the branch remembers the blade.
|
|||
- 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. The routine now also carries anti-loop physiology imported from Void's calmer brain: `noveltyToSelf`, `noveltyToRoom`, source coverage, saturation pressure, refractory cooling, and explicit permission to let a live unsaturated thought deepen without forcing novelty theater every pass. This mutates only heartbeat physiology fields: project truth and role memory mutation remain on their reviewed surfaces.
|
||||
- The next memory foundation must not build separate engines for repo graph and agent mind. Heartbeat cognition, role self-memory, repo architecture/dataflow, evidence scars, incubation, agency pressure, and context packets should converge through `EpiphanyMemoryGraph` profiles so one typed graph owns anchors, lifecycle, summaries, freshness, context cuts, and Qdrant embedding manifests. The repo profile producer maps accepted architecture/dataflow graph state into memory graph documents without scanner/Qdrant/model work and keeps bad topology visible as validation failure. The agent profile producer maps reviewed role dossier memories/goals/values into role_self/identity graph nodes without treating heartbeat rumination as durable memory. The next clean implementation slice is heartbeat cognition pressure import as provisional short-term/incubation/agency/candidate profile documents, still before sleep runner, Qdrant, CultNet, or prompt integration.
|
||||
- The next memory foundation must not build separate engines for repo graph and agent mind. Heartbeat cognition, role self-memory, repo architecture/dataflow, evidence scars, incubation, agency pressure, and context packets should converge through `EpiphanyMemoryGraph` profiles so one typed graph owns anchors, lifecycle, summaries, freshness, context cuts, and Qdrant embedding manifests. Repo, agent, and heartbeat profile producers now map accepted graph truth, reviewed role dossier memories/goals/values, and provisional heartbeat short-term/incubation/agency/candidate pressure into memory graph documents without scanner/Qdrant/model work. Heartbeat import deliberately does not mark durable promotion. The next clean slice is a merge/compose law plus store command so the profile producers converge into one validated memory graph snapshot instead of becoming rival truth stores.
|
||||
- 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.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -297,4 +297,4 @@ current_status:
|
|||
freshness propagation, summary-versus-child context cuts, and a validated
|
||||
CultCache store entry; the native `epiphany-memory-graph` binary can
|
||||
status/validate/query/smoke the store without Qdrant.
|
||||
next_action: Continue the memory graph build unless a more urgent Codex starvation regression appears. Repo and agent profile producers now map existing accepted architecture/dataflow graph state plus reviewed role dossier memories/goals/values into `EpiphanyMemoryGraph` documents without scanner/Qdrant/model work. The next clean slice is heartbeat cognition pressure import as provisional short-term/incubation/agency/candidate profile documents, still without sleep runner, Qdrant, CultNet advertisement, or prompt integration.
|
||||
next_action: Continue the memory graph build unless a more urgent Codex starvation regression appears. Repo, agent, and heartbeat profile producers now map accepted architecture/dataflow graph truth, reviewed role dossier memory/goals/values, and heartbeat cognition pressure into `EpiphanyMemoryGraph` documents. Heartbeat imports are provisional short-term/incubation/agency/candidate pressure only; they must not mark durable promotion. The next clean slice is a merge/compose law plus store command so these profile producers write one validated memory graph snapshot instead of becoming rival truth stores.
|
||||
|
|
|
|||
Reference in a new issue