Type heartbeat cognition loop

This commit is contained in:
Meta Vens 2026-05-19 12:43:01 +01:00
commit 587bc2b9a5
7 changed files with 742 additions and 729 deletions

File diff suppressed because it is too large Load diff

View file

@ -63,11 +63,11 @@ pub struct EpiphanyHeartbeatCognitionEntry {
#[cultcache(key = 6, default)]
pub memory_resonance: Option<HeartbeatMemoryResonance>,
#[cultcache(key = 7, default)]
pub incubation: Option<Value>,
pub incubation: Option<HeartbeatIncubation>,
#[cultcache(key = 8, default)]
pub thought_lanes: Option<Value>,
pub thought_lanes: Option<HeartbeatThoughtLanes>,
#[cultcache(key = 9, default)]
pub bridge: Option<Value>,
pub bridge: Option<HeartbeatCognitionBridge>,
#[cultcache(key = 10, default)]
pub candidate_interventions: Option<HeartbeatCandidateInterventions>,
#[cultcache(key = 11, default)]
@ -198,6 +198,203 @@ pub struct HeartbeatMemoryResonancePair {
pub evidence_refs: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverage {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub roles: Vec<HeartbeatSourceCoverageRole>,
pub memory_kinds: Vec<HeartbeatSourceCoverageKind>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverageRole {
pub role_id: String,
pub count: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverageKind {
pub kind: String,
pub count: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatIncubation {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub source_coverage: HeartbeatSourceCoverage,
pub last_incubation_summary: String,
pub themes: Vec<HeartbeatIncubationTheme>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatIncubationTheme {
pub theme_id: String,
pub summary: String,
pub strength: f64,
pub source: String,
pub source_roles: Vec<String>,
pub source_kinds: Vec<String>,
pub source_memory_ids: Vec<String>,
pub support_count: usize,
pub evidence_diversity: f64,
pub exploration_bonus: f64,
pub novelty: f64,
pub novelty_to_self: f64,
pub novelty_to_room: f64,
pub maturation: f64,
pub desire_to_speak: f64,
pub saturation_score: f64,
pub recent_match_count: usize,
pub refractory_penalty: f64,
pub priority_score: f64,
pub status: String,
pub latent_question: String,
pub why_it_pulls: String,
pub holding_close_because: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatThoughtLanes {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub analytic: HeartbeatAnalyticLane,
pub associative: HeartbeatAssociativeLane,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAnalyticLane {
pub description: String,
pub active_threads: Vec<HeartbeatAnalyticThread>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAssociativeLane {
pub description: String,
pub active_threads: Vec<HeartbeatAssociativeThread>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAnalyticThread {
pub thread_id: String,
pub topic: String,
pub claim: String,
pub evidence_refs: Vec<String>,
pub salience: f64,
pub confidence: f64,
pub desire_to_act: f64,
pub counterweight: String,
pub last_touched_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAssociativeThread {
pub thread_id: String,
pub topic: String,
pub claim: String,
pub source_theme_id: String,
pub novelty: f64,
pub room_relevance: f64,
pub desire_to_speak: f64,
pub status: String,
pub counterweight: String,
pub last_touched_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatCognitionBridge {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub recent_syntheses: Vec<HeartbeatBridgeSynthesis>,
pub source_coverage: HeartbeatSourceCoverage,
pub topic_saturation: Vec<HeartbeatTopicSaturation>,
pub refractory_topics: Vec<HeartbeatRefractoryTopic>,
pub unresolved_tensions: Vec<HeartbeatBridgeTension>,
pub decision: HeartbeatBridgeDecision,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeSynthesis {
pub timestamp: String,
pub summary: String,
pub dominant_topics: Vec<String>,
pub lane_balance: String,
pub speak_decision: String,
pub theme_status: String,
pub novelty_to_self: f64,
pub novelty_to_room: f64,
pub saturation_score: f64,
pub saturation_note: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatTopicSaturation {
pub topic: String,
pub dominance: f64,
pub recent_mentions: usize,
pub cooling_advice: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatRefractoryTopic {
pub topic: String,
pub penalty: f64,
pub cools_until: String,
pub reason: String,
pub last_triggered_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeTension {
pub topic: String,
pub summary: String,
pub opened_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeDecision {
pub lane_balance: String,
pub speak_decision: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]

View file

@ -127,9 +127,9 @@ fn legacy_heartbeat_cognition_entry(
source,
sleep_cycle: decode_legacy_document(legacy.sleep_cycle)?,
memory_resonance: decode_legacy_document(legacy.memory_resonance)?,
incubation: legacy.incubation,
thought_lanes: legacy.thought_lanes,
bridge: legacy.bridge,
incubation: decode_legacy_document(legacy.incubation)?,
thought_lanes: decode_legacy_document(legacy.thought_lanes)?,
bridge: decode_legacy_document(legacy.bridge)?,
candidate_interventions: decode_legacy_document(legacy.candidate_interventions)?,
appraisals: decode_legacy_document(legacy.appraisals)?,
reactions: decode_legacy_document(legacy.reactions)?,

View file

@ -1,12 +1,26 @@
use crate::heartbeat_state::EpiphanyHeartbeatCognitionEntry;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeDecision;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeSynthesis;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeTension;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCandidateIntervention;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCandidateInterventions;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCognitionBridge;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatIncubation;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatIncubationTheme;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatMemoryResonance;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatMemoryResonancePair;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatSourceCoverage;
use crate::memory_graph::EpiphanyMemoryDomain;
use crate::memory_graph::EpiphanyMemoryGraphSnapshot;
use crate::memory_graph::EpiphanyMemoryLifecycle;
@ -16,7 +30,6 @@ 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>,
@ -109,44 +122,41 @@ fn import_incubation(
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");
for theme in incubation.themes.iter().take(12) {
let theme_id = theme.theme_id.clone();
let status = theme.status.clone();
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_any(
theme,
&["latentQuestion", "question"],
"What is this incubation theme trying to become?",
),
tension: string_at_any(
theme,
&["holdingCloseBecause", "tension"],
"Heartbeat marked this theme but did not preserve a tension.",
),
action_implication: string_at_any(
theme,
&["whyItPulls", "actionImplication"],
"Keep in incubation until sleep or review accounts for it.",
),
claim: theme.summary.clone(),
question: if theme.latent_question.is_empty() {
"What is this incubation theme trying to become?".to_string()
} else {
theme.latent_question.clone()
},
tension: if theme.holding_close_because.is_empty() {
"Heartbeat marked this theme but did not preserve a tension.".to_string()
} else {
theme.holding_close_because.clone()
},
action_implication: if theme.why_it_pulls.is_empty() {
"Keep in incubation until sleep or review accounts for it.".to_string()
} else {
theme.why_it_pulls.clone()
},
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")),
salience: score_to_u32(theme.priority_score),
confidence: score_to_u32(theme.maturation),
..Default::default()
});
}
@ -217,10 +227,7 @@ fn import_bridge_pressure(
let Some(bridge) = cognition.bridge.as_ref() else {
return;
};
let decision = bridge
.pointer("/decision/speakDecision")
.and_then(Value::as_str)
.unwrap_or("silence");
let decision = bridge.decision.speak_decision.as_str();
if decision == "silence" {
return;
}
@ -229,10 +236,11 @@ fn import_bridge_pressure(
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 reason = if bridge.decision.reason.is_empty() {
"Bridge pressure has no explicit reason."
} else {
bridge.decision.reason.as_str()
};
let node = EpiphanyMemoryNode {
id: memory_graph_node_id(&domain_id, "bridge-pressure", decision, None),
domain_id: domain_id.clone(),
@ -324,31 +332,6 @@ fn push_summary(
});
}
fn string_at(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
fn string_at_any(value: &Value, keys: &[&str], fallback: &str) -> String {
keys.iter()
.find_map(|key| {
let value = value.get(key).and_then(Value::as_str)?;
if value.trim().is_empty() {
None
} else {
Some(value.to_string())
}
})
.unwrap_or_else(|| fallback.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
@ -401,29 +384,52 @@ mod tests {
evidence_refs: vec!["mem-a".to_string(), "mem-b".to_string()],
}],
}),
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
}, {
"themeId": "theme-live-store-shape",
"summary": "Live heartbeat stores may omit optional explanatory fields.",
"status": "deepening"
}]
})),
incubation: Some(HeartbeatIncubation {
schema_version: "epiphany.incubation.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
source_coverage: HeartbeatSourceCoverage::default(),
last_incubation_summary: "Body/Soul evidence seam is live.".to_string(),
themes: vec![
HeartbeatIncubationTheme {
theme_id: "theme-body-soul".to_string(),
summary: "Body and Soul keep touching evidence boundaries.".to_string(),
latent_question: "Should this become doctrine?".to_string(),
holding_close_because: "It is live but not settled.".to_string(),
why_it_pulls: "It affects future review gates.".to_string(),
status: "deepening".to_string(),
priority_score: 0.72,
maturation: 0.6,
..Default::default()
},
HeartbeatIncubationTheme {
theme_id: "theme-live-store-shape".to_string(),
summary: "Live heartbeat stores may omit optional explanatory fields."
.to_string(),
status: "deepening".to_string(),
..Default::default()
},
],
}),
thought_lanes: None,
bridge: Some(serde_json::json!({
"decision": {
"speakDecision": "hold",
"reason": "The thought is live but not ready for speech."
}
})),
bridge: Some(HeartbeatCognitionBridge {
schema_version: "epiphany.cognition_bridge.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
decision: HeartbeatBridgeDecision {
speak_decision: "hold".to_string(),
reason: "The thought is live but not ready for speech.".to_string(),
..Default::default()
},
recent_syntheses: vec![HeartbeatBridgeSynthesis {
summary: "The thought is live but not ready for speech.".to_string(),
..Default::default()
}],
unresolved_tensions: vec![HeartbeatBridgeTension {
topic: "thought authority boundary".to_string(),
summary: "Bridge pressure is not authority.".to_string(),
opened_at: "2026-05-18T00:00:00Z".to_string(),
}],
..Default::default()
}),
candidate_interventions: Some(HeartbeatCandidateInterventions {
schema_version: "epiphany.candidate_interventions.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),

View file

@ -61,7 +61,7 @@ remembers doctrine; the branch remembers the blade.
- Thread start route handling and its async lifecycle task now live in `codex_message_processor/thread_start_routes.rs`; root no longer owns `thread_start_task`.
- Thread config helpers now live in `codex_message_processor/thread_config.rs`: instruction-source loading, thread config override construction, config-load error shaping, and dynamic-tool validation. The pure dynamic-tool validation contract has module-local unit tests.
- Turn control routes now live in `codex_message_processor/turn_routes.rs`: turn start, thread item injection, app-server client info setting, turn steer, turn interrupt, mode normalization, and input-limit validation with module-local unit tests.
- Heartbeat durable document/schema types now live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`, role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`, persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`, and status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`. Adaptive pacing, scene protocol, participant personality/mood timing, initiative heat projection, birth personality seed receipts, pending-turn freeze/multiplier facts, Void routine sleep-cycle physiology, Void routine memory-resonance pairs, candidate interventions, role thought appraisals, and derived reactions are typed fields rather than `extra` maps or generic JSON cargo; only the sealed legacy combined-state reader still carries `extra` for old `voidRoutine` migration. The root `heartbeat_state.rs` keeps scheduling and the remaining Void cognition orchestration for the next cuts.
- Heartbeat durable document/schema types now live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`, role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`, persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`, and status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`. Adaptive pacing, scene protocol, participant personality/mood timing, initiative heat projection, birth personality seed receipts, pending-turn freeze/multiplier facts, Void routine sleep-cycle physiology, Void routine memory-resonance pairs, incubation themes/source coverage, analytic/associative thought lanes, bridge syntheses/saturation/refractory cooling/decision, candidate interventions, role thought appraisals, and derived reactions are typed fields rather than `extra` maps or generic JSON cargo; only the sealed legacy combined-state reader still carries `extra` for old `voidRoutine` migration. The root `heartbeat_state.rs` keeps scheduling and cognition orchestration; do not reintroduce generic JSON there merely because operator artifacts are JSON.
- Heartbeat role/default and store seams now have module-local unit tests: fixed lane catalog, Ghostlight scene role projection, and typed state/cognition CultCache round-trip. Keep future heartbeat cuts aligned to those seam tests instead of growing root integration-only coverage.
- Heartbeat adaptive pacing now lives in `epiphany-core/src/heartbeat_state/heartbeat_pacing.rs`, including running-turn count and effective cooldown multipliers. It has a module-local unit test for pending-turn counting; root scheduling calls the pacing seam instead of owning pressure/concurrency math directly.
- Thread projection now lives in `codex_message_processor/thread_projection.rs`: state-db/thread-store/rollout summary projection, `ConversationSummary` to API `Thread` mapping, thread-title attachment helpers, turn pagination, and rollout turn reconstruction. `codex_message_processor.rs` is about 1,009 lines after this extraction and mostly owns JSON-RPC dispatch, processor construction, shared thread lookup/submit/error helpers, and lifecycle shutdown.
@ -101,7 +101,7 @@ remembers doctrine; the branch remembers the blade.
- Active heartbeat turns now freeze initiative explicitly. A running pending turn carries `initiativeFrozen` and `initiativeFreezeReason`; schedule/status/readiness projections expose `initiative_frozen`; the scheduler still rejects attempts to wake that lane again; and completion receipts mark `cooldownStartedAfterCompletion: true`. A focused high-heat test proves a 25x heated implementation lane cannot be queued again while it is still thinking.
- 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 typed bounded memory resonance, maintains incubation themes, runs analytic and associative cognition lanes, writes bridge syntheses/saturation/tension, produces typed candidate interventions, projects the active thought cluster through each role's Ghostlight-shaped personality vectors, derives typed participant-local reactions from typed appraisals, applies personality/mood timing, advances typed sleep/dream-cycle state, 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 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 typed bounded memory resonance, maintains typed incubation themes/source coverage, runs typed analytic and associative cognition lanes, writes typed bridge syntheses/saturation/refractory cooling/decision state, produces typed candidate interventions, projects the active thought cluster through each role's Ghostlight-shaped personality vectors, derives typed participant-local reactions from typed appraisals, applies personality/mood timing, advances typed sleep/dream-cycle state, 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. 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. Compose now merges profile snapshots into one validated graph and rejects duplicate authority; `epiphany-memory-graph compose` writes a composed typed CultCache graph from typed source graph stores; `epiphany-memory-graph refresh` can invoke live agent-memory plus heartbeat-cognition producers from their typed stores into one graph store; `epiphany-core` has a native typed `EpiphanyThreadState` CultCache store seam; the bridge mirrors loaded Codex-thread Epiphany state into `state/thread-state.msgpack`; and CultNet now advertises logical `EpiphanyThreadState` plus `EpiphanyMemoryGraph` snapshot contracts. The next clean slice is local refresh/dogfood once a mirrored thread-state store exists, then a Qdrant embedding cache writer that treats embeddings as rebuildable manifests, not canonical memory.
- 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.
@ -198,7 +198,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, 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, incubation, analytic/associative cognition lanes, bridge judgment, typed candidate interventions, typed Ghostlight-style personality appraisals, mood/anxiety timing, typed derived reactions, and dream maintenance stored in the typed heartbeat store
- 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
- 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

Binary file not shown.

View file

@ -101,7 +101,7 @@ accepted_design:
- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. The old JSON-value selfPatch functions remain only as edge compatibility wrappers for CLI/artifact ingress; `epiphany-agent-memory-store`, heartbeat rumination application, and repo initialization selfPatch processing parse once at the edge and then operate on typed documents. Role-result self-persistence no longer exports a JSON-value reviewer; native review uses `review_role_self_patch_document`.
- Durable Ghostlight role dossiers are typed through their relationship/event/scene/perceived-overlay surfaces now. `EpiphanyAgentMemoryEntry` no longer stores those arrays as `serde_json::Value`, and durable Ghostlight structs no longer carry flattened unknown `extra` bags. `AgentSelfPatch.extra` remains only as an ingress guard so unknown patch keys can be rejected with reasons.
- The state ledger CultCache document is now a closed typed ledger shape. Branch and evidence records no longer carry flattened `extra` maps; evidence additions remain explicit records with `ts`, `type`, `status`, `note`, and optional branch.
- Heartbeat durable document/schema types live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`; role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`; heartbeat persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`; heartbeat status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`; the root heartbeat module remains the scheduling/cognition orchestration surface until those responsibilities are split into their own tested seams. Heartbeat timing authority is now typed state: adaptive pacing, scene protocol, participant personality/mood timing, initiative heat projection, birth personality seed receipts, pending-turn freeze/multiplier facts, Void routine sleep-cycle physiology, Void routine memory-resonance pairs, candidate interventions, role thought appraisals, and derived reactions are named fields rather than `extra` maps or generic JSON cargo. The only remaining heartbeat `extra` bag is the sealed legacy combined-state reader for old `voidRoutine` migration.
- Heartbeat durable document/schema types live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`; role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`; heartbeat persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`; heartbeat status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`; the root heartbeat module remains the scheduling/cognition orchestration surface until those responsibilities are split into their own tested seams. Heartbeat timing authority is now typed state: adaptive pacing, scene protocol, participant personality/mood timing, initiative heat projection, birth personality seed receipts, pending-turn freeze/multiplier facts, Void routine sleep-cycle physiology, Void routine memory-resonance pairs, incubation themes/source coverage, analytic/associative thought lanes, bridge syntheses/saturation/refractory cooling/decision, candidate interventions, role thought appraisals, and derived reactions are named fields rather than `extra` maps or generic JSON cargo. The only remaining heartbeat `extra` bag is the sealed legacy combined-state reader for old `voidRoutine` migration.
- Heartbeat role/default and store seams are pinned by module-local unit tests for fixed lane catalog, Ghostlight scene role projection, and typed state/cognition CultCache round-trip.
- Heartbeat adaptive pacing lives in `epiphany-core/src/heartbeat_state/heartbeat_pacing.rs`; it owns pressure/concurrency derivation, running-turn count, initiative heat projection, and effective cooldown multipliers for scheduler/projection callers.
- `epiphany-openai-auth-spine` is now a thin Epiphany-named boundary over retained vendored `codex-login`, not a clean-room credential store or token-refresh clone. Keep this impurity sealed as the Codex-compatible subscription auth organ while Epiphany owns typed request/status/event/runtime documents around it.
@ -293,10 +293,12 @@ rejected_paths:
current_status:
summary: The Codex starvation pass has five typing receipts, fifty-six processor/foundation cuts, a real physical extraction in `epiphany-codex-bridge`, a root `epiphany-state-model` crate for durable Epiphany state plus prompt-state rendering, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, native state-update contract/validation/application in `epiphany-core`, native exact retrieval path matching in `epiphany-core`, and the route-facing mutation/view/coordinator projection services in `epiphany-codex-bridge`. `codex-protocol` no longer defines durable `Epiphany*` state structs; it re-exports the native model and renderer for legacy rollout/API compatibility. `epiphany-core` has no direct vendored Codex dependency, and `codex-core` no longer depends on `epiphany-core`: pressure derives from native `EpiphanyTokenUsageSnapshot`, Codex rollout replay stays in the real Codex rollout adapter, prompt rendering lives in `epiphany-state-model`, and `CodexThread` exposes only host state/path/persistence facts. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing execution and projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, state update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, watcher invalidation adaptation, full `thread/epiphany/view` response construction, freshness/context/graph-query response construction, role/reorient result response construction, distill/propose patch-response construction, coordinator status/view construction, and acceptance/evidence signal helpers. Vendored app-server no longer retains root Epiphany modules; `codex_message_processor.rs` still contains route dispatch, JSON-RPC response shaping, watcher registration/snapshotting calls, and notification emission. Codex product surfaces for apps, skills, plugins, marketplace, external agent config import, SDK scaffolding, connectors, ChatGPT app discovery, plugin provenance, and skill mention plumbing have been cut from the live path. `codex_thread.rs` is now about 449 lines and owns Codex thread hosting, not Epiphany policy. `epiphany-openai-codex-spine` owns the native OpenAI model transport path while intentionally reaching retained vendored `codex-login` only through `epiphany-openai-auth-spine`; auth identity, credential storage, and ChatGPT token refresh remain owned by the Codex-compatible auth organ. `epiphany-openai-runtime` writes typed OpenAI/runtime documents, loads durable worker launch requests, persists typed role/reorient worker result documents, and completes heartbeat/specialist runtime jobs without Codex worker execution. MCP itself is correctly JSON at its protocol edge; the target remains typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON.
latest_cut: >
The heartbeat memory-resonance, sleep-cycle, candidate-intervention, role
thought appraisal, and derived reaction routine documents are typed. Native
routine construction, cognition persistence, legacy decode, memory-graph
import, novelty checks, mood timing, and sleep/dream-cycle update now
operate on named CultCache documents instead of generic JSON; status still
projects JSON only as operator glass.
next_action: Continue internal CultCache purification before outward feature growth. The remaining high-signal internal JSON is the rest of `EpiphanyHeartbeatCognitionEntry`: `incubation`, `thought_lanes`, and `bridge` still persist as `serde_json::Value`. Type those cognition documents one bounded organ at a time, keeping JSON only at artifact/status/wire projections and legacy decode seams; then return to app-server JSON-RPC route starvation toward CultNet contracts.
The heartbeat cognition document is now typed through the full Void routine
loop: sleep cycle, memory resonance, incubation, thought lanes, bridge,
candidate interventions, role thought appraisals, and derived reactions are
named CultCache documents. Native routine construction, cognition
persistence, legacy decode, memory-graph import, novelty/saturation checks,
mood timing, and sleep/dream-cycle update operate on typed state; JSON
survives there only as operator/status/artifact projection and sealed legacy
migration glass.
next_action: Continue Codex starvation and CultNet liberation outside the now-typed heartbeat cognition document. Classify the next high-signal generic JSON surfaces by ownership before cutting: route-facing app-server JSON-RPC response shaping, Qdrant payload/cache edges, OpenAI/MCP protocol edges, and role-result/raw ingress compatibility. Keep JSON only at true wire/hostile-ingress/forensic boundaries; move Epiphany-owned runtime data into typed CultCache documents and CultNet contracts.