Add memory graph compose store command

This commit is contained in:
Meta Vens 2026-05-18 02:00:35 +01:00
commit 6a4918ee67
4 changed files with 83 additions and 3 deletions

View file

@ -13,6 +13,7 @@ use epiphany_core::EpiphanyMemoryNode;
use epiphany_core::EpiphanyMemoryNodeKind;
use epiphany_core::EpiphanyMemoryProfile;
use epiphany_core::EpiphanyMemorySummary;
use epiphany_core::compose_memory_graph_snapshots;
use epiphany_core::derive_memory_graph_freshness;
use epiphany_core::load_memory_graph_snapshot;
use epiphany_core::memory_graph_domain_id;
@ -63,6 +64,11 @@ fn main() -> Result<()> {
let packet = plan_memory_graph_context_cut(&snapshot, &query);
print_json(&packet)?;
}
"compose" => {
let compose = read_compose_args(args)?;
let output = compose_memory_graph_store(compose)?;
print_json(&output)?;
}
"smoke" => {
let store = optional_path_arg(&mut args, "--store")?
.unwrap_or_else(|| scoped_temp_store("epiphany-memory-graph-smoke"));
@ -140,6 +146,78 @@ fn read_context_query(args: impl Iterator<Item = String>) -> Result<EpiphanyMemo
Ok(query)
}
struct ComposeArgs {
store: PathBuf,
graph_id: String,
sources: Vec<PathBuf>,
}
fn read_compose_args(args: impl Iterator<Item = String>) -> Result<ComposeArgs> {
let mut store = None;
let mut graph_id = "epiphany-memory-graph".to_string();
let mut sources = Vec::new();
let mut args = args.peekable();
while let Some(flag) = args.next() {
match flag.as_str() {
"--store" => store = Some(PathBuf::from(next_value(&mut args, "--store")?)),
"--graph-id" => graph_id = next_value(&mut args, "--graph-id")?,
"--source" => sources.push(PathBuf::from(next_value(&mut args, "--source")?)),
_ => return Err(anyhow!("unexpected compose argument {flag:?}")),
}
}
if sources.is_empty() {
return Err(anyhow!(
"compose requires at least one --source memory graph store"
));
}
Ok(ComposeArgs {
store: store.ok_or_else(|| anyhow!("compose requires --store <path>"))?,
graph_id,
sources,
})
}
fn compose_memory_graph_store(args: ComposeArgs) -> Result<serde_json::Value> {
let mut snapshots = Vec::new();
let mut source_status = Vec::new();
for source in &args.sources {
let snapshot = load_memory_graph_snapshot(source)?
.ok_or_else(|| anyhow!("memory graph source {} is missing", source.display()))?;
source_status.push(serde_json::json!({
"source": source,
"graphId": snapshot.graph_id,
"domains": snapshot.domains.len(),
"nodes": snapshot.nodes.len(),
"edges": snapshot.edges.len(),
"summaries": snapshot.summaries.len(),
}));
snapshots.push(snapshot);
}
let snapshot = compose_memory_graph_snapshots(args.graph_id, snapshots).map_err(|errors| {
anyhow!(
"composed memory graph is invalid: {}",
errors
.iter()
.map(|error| format!("{}: {}", error.path, error.message))
.collect::<Vec<_>>()
.join("; ")
)
})?;
write_memory_graph_snapshot(&args.store, &snapshot)?;
Ok(serde_json::json!({
"ok": true,
"store": args.store,
"graphId": snapshot.graph_id,
"sources": source_status,
"domains": snapshot.domains.len(),
"nodes": snapshot.nodes.len(),
"edges": snapshot.edges.len(),
"summaries": snapshot.summaries.len(),
"freshness": snapshot.freshness,
}))
}
fn require_path_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(require_string_arg(args, name)?))
}
@ -195,7 +273,9 @@ fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
}
fn print_usage() {
eprintln!("usage: epiphany-memory-graph <status|validate|context|smoke> --store <path> ...");
eprintln!(
"usage: epiphany-memory-graph <status|validate|context|compose|smoke> --store <path> ..."
);
}
fn scoped_temp_store(prefix: &str) -> PathBuf {

View file

@ -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. 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; the next clean slice is a native store command that writes the composed snapshot, still before Qdrant, CultNet, prompt integration, or sleep promotion.
- 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. The next clean slice is a refresh path that invokes live profile producers from existing typed stores instead of requiring prebuilt source graph stores, still before Qdrant, CultNet, prompt integration, or sleep promotion.
- 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.

View file

@ -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, 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; compose now merges profile snapshots into one validated graph and rejects duplicate authority. The next clean slice is a native store command that writes the composed snapshot, still without Qdrant, CultNet advertisement, prompt integration, or sleep promotion.
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; compose now merges profile snapshots into one validated graph and the native `epiphany-memory-graph compose` command writes that graph to a typed CultCache store from typed source graph stores. The next clean slice is a refresh path that invokes the live profile producers from their existing typed stores instead of requiring prebuilt source graph stores, still without Qdrant, CultNet advertisement, prompt integration, or sleep promotion.