From bb0dc639b77585217f9ea1097affd7fa60d919fb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 04:55:09 +0100 Subject: [PATCH] Make semantic projection admission causal --- epiphany-core/src/agent_memory.rs | 440 +++++++++++++++++- .../src/bin/epiphany-memory-semantic.rs | 62 ++- .../src/bin/epiphany-repo-model-bootstrap.rs | 19 +- epiphany-core/src/coordinator_launch.rs | 4 + .../src/coordinator_launch_context.rs | 33 ++ epiphany-core/src/heartbeat_state.rs | 4 +- epiphany-core/src/lib.rs | 27 +- .../src/memory_graph/semantic_index.rs | 82 ++++ epiphany-core/src/runtime_spine.rs | 436 ++++++++++++++++- notes/fresh-workspace-handoff.md | 20 + ...graph-semantic-projection-authority-map.md | 31 ++ state/ledgers.msgpack | Bin 495178 -> 495945 bytes state/map.yaml | 2 +- 13 files changed, 1112 insertions(+), 48 deletions(-) diff --git a/epiphany-core/src/agent_memory.rs b/epiphany-core/src/agent_memory.rs index beeb87a4..6a46520e 100644 --- a/epiphany-core/src/agent_memory.rs +++ b/epiphany-core/src/agent_memory.rs @@ -11,6 +11,7 @@ use cultcache_rs::SoaDocument; use serde::Deserialize; use serde::Serialize; use serde_json::Value; +use sha2::{Digest, Sha256}; use std::collections::BTreeMap; use std::fs; use std::path::Path; @@ -21,6 +22,67 @@ pub const AGENT_MEMORY_SWARM_IDENTITY_TYPE: &str = "epiphany.agent_memory_swarm_ pub const AGENT_MEMORY_SWARM_IDENTITY_SCHEMA_VERSION: &str = "epiphany.agent_memory_swarm_identity.v0"; pub const AGENT_MEMORY_SWARM_IDENTITY_KEY: &str = "swarm-identity"; +pub const AGENT_MEMORY_GENERATION_WITNESS_TYPE: &str = "epiphany.agent_memory_generation_witness"; +pub const AGENT_MEMORY_GENERATION_WITNESS_SCHEMA_VERSION: &str = + "epiphany.agent_memory_generation_witness.v0"; +pub const AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY: &str = "mind-generation/latest"; +pub const AGENT_MEMORY_MIND_ADMISSION_TYPE: &str = "epiphany.agent_memory_mind_admission"; +pub const AGENT_MEMORY_MIND_ADMISSION_SCHEMA_VERSION: &str = + "epiphany.agent_memory_mind_admission.v0"; + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.agent_memory_generation_witness", + schema = "AgentMemoryGenerationWitness" +)] +pub struct AgentMemoryGenerationWitness { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub witness_id: String, + #[cultcache(key = 2)] + pub swarm_id: String, + #[cultcache(key = 3)] + pub generation: u64, + #[cultcache(key = 4)] + pub previous_generation: u64, + #[cultcache(key = 5)] + pub previous_source_hash: String, + #[cultcache(key = 6)] + pub source_hash: String, + #[cultcache(key = 7)] + pub authority_receipt_id: String, + #[cultcache(key = 8)] + pub mutation_kind: String, + #[cultcache(key = 9)] + pub changed_role_ids: Vec, + #[cultcache(key = 10)] + pub committed_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.agent_memory_mind_admission", + schema = "AgentMemoryMindAdmissionReceipt" +)] +pub struct AgentMemoryMindAdmissionReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub receipt_id: String, + #[cultcache(key = 2)] + pub swarm_id: String, + #[cultcache(key = 3)] + pub role_id: String, + #[cultcache(key = 4)] + pub mutation_kind: String, + #[cultcache(key = 5)] + pub reason: String, + #[cultcache(key = 6)] + pub status: String, + #[cultcache(key = 7)] + pub resulting_source_hash: String, +} pub const AGENT_STATE_SOA_TYPE: &str = "epiphany.agent_state_soa"; pub const AGENT_STATE_SOA_SCHEMA_VERSION: &str = "epiphany.agent_state_soa.v0"; pub const AGENT_STATE_SOA_KEY: &str = "swarm"; @@ -623,6 +685,7 @@ pub fn migrate_agent_memory_json_dir_to_cultcache( let agent_dir = agent_dir.as_ref(); let store_path = store_path.as_ref(); let mut cache = agent_memory_cache(store_path)?; + require_agent_memory_migration_open(&mut cache, "JSON agent-memory import")?; let mut migrated = Vec::new(); for (role_id, expected_agent_id, filename) in ROLE_TARGETS { let path = agent_dir.join(filename); @@ -664,7 +727,7 @@ pub fn validate_agent_memory_store(store_path: impl AsRef) -> Result) -> Result { let store_path = store_path.as_ref(); let mut cache = agent_memory_cache(store_path)?; - cache.pull_all_backing_stores()?; + require_agent_memory_migration_open(&mut cache, "agent-memory repair")?; let mut repaired = Vec::new(); if let Some(mut modeling) = cache.get::("modeling")? @@ -793,6 +856,245 @@ pub fn repair_agent_memory_store(store_path: impl AsRef) -> Result })) } +pub fn load_agent_memory_generation_witness( + store_path: impl AsRef, +) -> Result> { + let store_path = store_path.as_ref(); + if !store_path.exists() { + return Ok(None); + } + let mut cache = agent_memory_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get(AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY) +} + +fn canonical_agent_memory_source_hash( + cache: &mut CultCache, + identity: &AgentMemorySwarmIdentity, + replacements: &BTreeMap, +) -> Result { + let mut hasher = Sha256::new(); + hasher.update(AGENT_MEMORY_SWARM_IDENTITY_SCHEMA_VERSION.as_bytes()); + hasher.update([0]); + hasher.update(identity.swarm_id.as_bytes()); + for (role_id, _, _) in ROLE_TARGETS { + let entry = replacements + .get(*role_id) + .cloned() + .or_else(|| { + cache + .get::(role_id) + .ok() + .flatten() + }) + .ok_or_else(|| anyhow!("canonical Mind generation is missing role {role_id:?}"))?; + let envelope = cache.prepare_entry(*role_id, &entry)?.0; + hasher.update((*role_id).as_bytes()); + hasher.update((envelope.payload.len() as u64).to_le_bytes()); + hasher.update(&envelope.payload); + } + Ok(format!("{:x}", hasher.finalize())) +} + +fn commit_agent_memory_generation( + store_path: &Path, + expected_generation: u64, + expected_source_hash: &str, + role_id: &str, + next_entry: EpiphanyAgentMemoryEntry, + mutation_kind: &str, + reason: &str, +) -> Result { + let expected_agent_id = agent_id_for_role(role_id).map_err(|message| anyhow!(message))?; + let validation = validate_agent_entry(&next_entry, expected_agent_id); + if !validation.is_empty() { + return Err(anyhow!("Mind commit candidate is invalid: {validation:?}")); + } + let mut cache = agent_memory_cache(store_path)?; + cache.pull_all_backing_stores()?; + let identity = cache + .get::(AGENT_MEMORY_SWARM_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("Mind commit requires immutable agent memory swarm identity"))?; + let current = + cache.get::(AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY)?; + let current_generation = current.as_ref().map_or(0, |witness| witness.generation); + let empty = BTreeMap::new(); + let current_source_hash = canonical_agent_memory_source_hash(&mut cache, &identity, &empty)?; + if let Some(witness) = ¤t + && (witness.schema_version != AGENT_MEMORY_GENERATION_WITNESS_SCHEMA_VERSION + || witness.swarm_id != identity.swarm_id + || witness.source_hash != current_source_hash + || witness.generation == 0) + { + return Err(anyhow!( + "stored Mind generation witness does not authenticate current canonical memory" + )); + } + if current_generation != expected_generation || current_source_hash != expected_source_hash { + return Err(anyhow!( + "agent memory generation changed before Mind commit" + )); + } + let mut replacements = BTreeMap::new(); + replacements.insert(role_id.to_string(), next_entry); + let source_hash = canonical_agent_memory_source_hash(&mut cache, &identity, &replacements)?; + if source_hash == current_source_hash { + return Err(anyhow!( + "Mind commit would mint a generation without changing canonical memory" + )); + } + let generation = current_generation + 1; + let fingerprint = format!( + "{}|{}|{}|{}|{}", + identity.swarm_id, generation, mutation_kind, role_id, source_hash + ); + let witness_id = format!( + "mind-generation-{}", + uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, fingerprint.as_bytes()) + ); + let receipt_id = format!( + "mind-admission-{}", + uuid::Uuid::new_v5( + &uuid::Uuid::NAMESPACE_OID, + format!("{fingerprint}|{reason}").as_bytes() + ) + ); + let receipt = AgentMemoryMindAdmissionReceipt { + schema_version: AGENT_MEMORY_MIND_ADMISSION_SCHEMA_VERSION.to_string(), + receipt_id: receipt_id.clone(), + swarm_id: identity.swarm_id.clone(), + role_id: role_id.to_string(), + mutation_kind: mutation_kind.to_string(), + reason: reason.to_string(), + status: "admitted".to_string(), + resulting_source_hash: source_hash.clone(), + }; + let committed_at = now_rfc3339(); + let witness = AgentMemoryGenerationWitness { + schema_version: AGENT_MEMORY_GENERATION_WITNESS_SCHEMA_VERSION.to_string(), + witness_id: witness_id.clone(), + swarm_id: identity.swarm_id.clone(), + generation, + previous_generation: current_generation, + previous_source_hash: current_source_hash, + source_hash: source_hash.clone(), + authority_receipt_id: receipt_id.clone(), + mutation_kind: mutation_kind.to_string(), + changed_role_ids: vec![role_id.to_string()], + committed_at: committed_at.clone(), + }; + let mut projected_entries = Vec::with_capacity(ROLE_TARGETS.len()); + for (canonical_role, _, _) in ROLE_TARGETS { + projected_entries.push( + replacements + .get(*canonical_role) + .cloned() + .or_else(|| { + cache + .get::(canonical_role) + .ok() + .flatten() + }) + .ok_or_else(|| anyhow!("canonical Mind generation lost role {canonical_role:?}"))?, + ); + } + let mut projection_snapshot = crate::memory_graph_from_agent_memories( + format!("{}-mind", identity.swarm_id), + &projected_entries, + ); + projection_snapshot.model_revision = generation; + let obligation = crate::derive_memory_semantic_projection_obligation( + &projection_snapshot, + &identity.swarm_id, + crate::SemanticPartition::Mind, + &format!("epiphany.agent-memory/{}/mind", identity.swarm_id), + &witness_id, + &committed_at, + )?; + + let opening = cache.snapshot_envelopes(); + let mut expected = Vec::new(); + let mut batch = Vec::new(); + for (canonical_role, _, _) in ROLE_TARGETS { + if let Some(existing) = opening.iter().find(|envelope| { + envelope.r#type == AGENT_MEMORY_TYPE && envelope.key == *canonical_role + }) { + expected.push(existing.clone()); + } + let entry = replacements + .get(*canonical_role) + .cloned() + .or_else(|| { + cache + .get::(canonical_role) + .ok() + .flatten() + }) + .ok_or_else(|| anyhow!("canonical Mind generation lost role {canonical_role:?}"))?; + batch.push(cache.prepare_entry(*canonical_role, &entry)?.0); + } + if let Some(existing) = opening.iter().find(|envelope| { + envelope.r#type == AGENT_MEMORY_GENERATION_WITNESS_TYPE + && envelope.key == AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY + }) { + expected.push(existing.clone()); + } + batch.push( + cache + .prepare_entry(AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY, &witness)? + .0, + ); + batch.push(cache.prepare_entry(&witness_id, &witness)?.0); + batch.push(cache.prepare_entry(&receipt_id, &receipt)?.0); + batch.push( + cache + .prepare_entry(&obligation.obligation_id, &obligation)? + .0, + ); + let backing = SingleFileMessagePackBackingStore::new(store_path); + if !backing.compare_and_swap_batch(&expected, batch)? { + return Err(anyhow!( + "agent memory Mind commit lost exact atomic compare-and-swap" + )); + } + Ok(witness) +} + +fn agent_memory_source_head(cache: &mut CultCache) -> Result<(u64, String)> { + let identity = cache + .get::(AGENT_MEMORY_SWARM_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("Mind commit requires immutable agent memory swarm identity"))?; + let canonical_hash = canonical_agent_memory_source_hash(cache, &identity, &BTreeMap::new())?; + if let Some(witness) = + cache.get::(AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY)? + { + if witness.schema_version != AGENT_MEMORY_GENERATION_WITNESS_SCHEMA_VERSION + || witness.swarm_id != identity.swarm_id + || witness.source_hash != canonical_hash + || witness.generation == 0 + { + return Err(anyhow!( + "stored Mind generation witness does not authenticate current canonical memory" + )); + } + return Ok((witness.generation, canonical_hash)); + } + Ok((0, canonical_hash)) +} + +fn require_agent_memory_migration_open(cache: &mut CultCache, operation: &str) -> Result<()> { + cache.pull_all_backing_stores()?; + if cache + .get::(AGENT_MEMORY_GENERATION_WITNESS_LATEST_KEY)? + .is_some() + { + return Err(anyhow!( + "{operation} is bootstrap/migration-only and cannot mutate canonical Mind after generation admission" + )); + } + Ok(()) +} + fn replace_deprecated_faculty_name(value: &mut String) -> usize { let count = value.matches("Proprioception").count(); if count > 0 { @@ -823,7 +1125,7 @@ pub fn load_agent_memory_entry_for_role( Ok(entry) } -pub fn write_agent_memory_entry_for_role( +pub fn write_agent_memory_entry_for_role_migration( store_path: impl AsRef, entry: &EpiphanyAgentMemoryEntry, ) -> Result<()> { @@ -839,6 +1141,7 @@ pub fn write_agent_memory_entry_for_role( )); } let mut cache = agent_memory_cache(store_path)?; + require_agent_memory_migration_open(&mut cache, "raw agent-memory replacement")?; cache.put(entry.role_id.as_str(), entry)?; Ok(()) } @@ -1008,6 +1311,11 @@ pub fn apply_agent_self_patch_document( let mut entry = cache .get::(role_id)? .ok_or_else(|| anyhow!("CultCache has no role memory entry for {role_id:?}"))?; + let (expected_generation, expected_source_hash) = agent_memory_source_head(&mut cache)?; + let reason = patch + .reason + .clone() + .unwrap_or_else(|| "admitted bounded self patch".to_string()); if let Some(incoming) = patch.semantic_memories { upsert_memories(&mut entry.agent.memories.semantic, incoming); @@ -1034,7 +1342,15 @@ pub fn apply_agent_self_patch_document( entry.agent.identity.private_notes = entry.agent.identity.private_notes[keep_from..].to_vec(); } - cache.put(role_id.to_string(), &entry)?; + commit_agent_memory_generation( + store_path, + expected_generation, + &expected_source_hash, + role_id, + entry, + "self_patch", + &reason, + )?; review.applied = Some(true); Ok(review) } @@ -1077,12 +1393,22 @@ pub fn apply_agent_memory_lifecycle_operation( let mut entry = cache .get::(role_id)? .ok_or_else(|| anyhow!("CultCache has no role memory entry for {role_id:?}"))?; + let (expected_generation, expected_source_hash) = agent_memory_source_head(&mut cache)?; + + let reason = operation.reason.clone(); for action in operation.actions { apply_memory_lifecycle_action(&mut entry, action)?; } - cache.put(role_id.to_string(), &entry)?; - refresh_agent_state_soa(store_path)?; + commit_agent_memory_generation( + store_path, + expected_generation, + &expected_source_hash, + role_id, + entry, + "lifecycle", + &reason, + )?; review.applied = Some(true); Ok(review) } @@ -1451,7 +1777,7 @@ pub fn apply_agent_canonical_trait_seeds( ) -> Result { let store_path = store_path.as_ref(); let mut cache = agent_memory_cache(store_path)?; - cache.pull_all_backing_stores()?; + require_agent_memory_migration_open(&mut cache, "canonical trait seeding")?; let mut applied = Vec::new(); for seed in seeds { if seed.trait_name.trim().is_empty() { @@ -1975,6 +2301,9 @@ pub fn project_agent_memory_to_json_dir( fn agent_memory_cache(store_path: &Path) -> Result { let mut cache = CultCache::new(); cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path)); @@ -2616,6 +2945,101 @@ mod tests { use super::*; use tempfile::tempdir; + #[test] + fn concurrent_mind_commits_have_one_generation_and_no_partial_companions() -> Result<()> { + let temp = tempdir()?; + let agent_dir = temp.path().join("agents-concurrent"); + fs::create_dir_all(&agent_dir)?; + for (role_id, agent_id, filename) in ROLE_TARGETS { + fs::write( + agent_dir.join(filename), + sample_agent_json(agent_id, &format!("Agent {role_id}")), + )?; + } + let store = temp.path().join("concurrent.msgpack"); + migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; + ensure_agent_memory_swarm_identity(&store, "concurrent-swarm")?; + let mut cache = agent_memory_cache(&store)?; + cache.pull_all_backing_stores()?; + let (generation, source_hash) = agent_memory_source_head(&mut cache)?; + let base = cache.get_required::("modeling")?; + let barrier = std::sync::Arc::new(std::sync::Barrier::new(3)); + let mut joins = Vec::new(); + for suffix in ["alpha", "beta"] { + let mut next = base.clone(); + next.agent.identity.public_description = format!( + "Modeling generation {suffix} preserves a distinct concurrent candidate truth." + ); + let barrier = barrier.clone(); + let store = store.clone(); + let source_hash = source_hash.clone(); + joins.push(std::thread::spawn(move || { + barrier.wait(); + commit_agent_memory_generation( + &store, + generation, + &source_hash, + "modeling", + next, + "self_patch", + &format!("Concurrent {suffix} admission candidate"), + ) + })); + } + barrier.wait(); + let results = joins + .into_iter() + .map(|join| join.join().expect("Mind commit worker panicked")) + .collect::>(); + assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); + assert_eq!(results.iter().filter(|result| result.is_err()).count(), 1); + let witness = load_agent_memory_generation_witness(&store)?.expect("generation witness"); + assert_eq!(witness.generation, 1); + let mut cache = agent_memory_cache(&store)?; + cache.pull_all_backing_stores()?; + let receipt = + cache.get::(&witness.authority_receipt_id)?; + assert!(receipt.is_some()); + let obligations = cache + .get_all::()? + .into_iter() + .filter(|obligation| obligation.source_commit_id == witness.witness_id) + .collect::>(); + assert_eq!(obligations.len(), 1); + let obligation = &obligations[0]; + assert_eq!(obligation.source_generation, witness.generation); + assert_eq!(obligation.partition, "mind"); + assert_eq!(obligation.swarm_id, witness.swarm_id); + let admitted = cache.get_required::("modeling")?; + assert!(write_agent_memory_entry_for_role_migration(&store, &admitted).is_err()); + assert!(repair_agent_memory_store(&store).is_err()); + assert!( + apply_agent_canonical_trait_seeds( + &[AgentCanonicalTraitSeed { + role_id: "coordinator".to_string(), + group_name: "underlying_organization".to_string(), + trait_name: "forbidden_after_admission".to_string(), + mean: 0.5, + plasticity: 0.5, + current_activation: 0.5, + source: Some("hostile bypass test".to_string()), + }], + &store, + ) + .is_err() + ); + + let mut corrupted = admitted; + corrupted + .agent + .identity + .public_description + .push_str(" corrupt"); + cache.put("modeling", &corrupted)?; + assert!(agent_memory_source_head(&mut cache).is_err()); + Ok(()) + } + #[test] fn role_memory_migrates_reviews_and_applies_native_patch() -> Result<()> { let temp = tempdir()?; @@ -2636,6 +3060,7 @@ mod tests { } let store = temp.path().join("agents.msgpack"); migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; + ensure_agent_memory_swarm_identity(&store, "test-swarm")?; assert!(validate_agent_memory_store(&store)?.is_empty()); let patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({ @@ -2681,6 +3106,7 @@ mod tests { } let store = temp.path().join("agents.msgpack"); migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; + ensure_agent_memory_swarm_identity(&store, "test-swarm")?; let patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({ "agentId": "epiphany.modeling", @@ -2797,6 +3223,7 @@ mod tests { } let store = temp.path().join("agents.msgpack"); migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; + ensure_agent_memory_swarm_identity(&store, "test-swarm")?; let applied = apply_agent_canonical_trait_seeds( &[AgentCanonicalTraitSeed { @@ -2862,6 +3289,7 @@ mod tests { } let store = temp.path().join("agents.msgpack"); migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; + ensure_agent_memory_swarm_identity(&store, "test-swarm")?; let refreshed = refresh_agent_state_soa(&store)?; assert_eq!(refreshed["rowCount"], 7); diff --git a/epiphany-core/src/bin/epiphany-memory-semantic.rs b/epiphany-core/src/bin/epiphany-memory-semantic.rs index 87595bb9..4eb66c62 100644 --- a/epiphany-core/src/bin/epiphany-memory-semantic.rs +++ b/epiphany-core/src/bin/epiphany-memory-semantic.rs @@ -4,7 +4,8 @@ use epiphany_core::{ EpiphanyMemoryContextQuery, EpiphanyMemoryGraphSnapshot, MemorySemanticIndexConfig, SemanticPartition, agent_memory_role_ids, index_memory_semantic_partition, load_agent_memory_entry_for_role, load_memory_graph_snapshot, memory_graph_from_agent_memories, - persist_memory_semantic_index_receipt, runtime_current_repo_model, semantic_memory_context, + persist_memory_semantic_index_receipt, runtime_current_repo_model, runtime_swarm_binding, + semantic_memory_context, }; use std::env; use std::path::PathBuf; @@ -13,7 +14,7 @@ fn main() -> Result<()> { let mut args = env::args().skip(1); let command = args.next().ok_or_else(|| usage_error("missing command"))?; let options = Options::parse(args)?; - let snapshot = options.load_snapshot()?; + let (snapshot, swarm_id) = options.load_source()?; let config = MemorySemanticIndexConfig::from_env(); match command.as_str() { "index" => { @@ -23,7 +24,7 @@ fn main() -> Result<()> { .ok_or_else(|| usage_error("index requires --receipt-store "))?; let receipt = index_memory_semantic_partition( &snapshot, - &options.swarm_id, + &swarm_id, options.partition, &Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), &config, @@ -55,7 +56,7 @@ fn main() -> Result<()> { .ok_or_else(|| usage_error("context requires --text "))?; let packet = semantic_memory_context( &snapshot, - &options.swarm_id, + &swarm_id, options.partition, &EpiphanyMemoryContextQuery { id: options @@ -81,7 +82,7 @@ struct Options { runtime_store: Option, agent_store: Option, receipt_store: Option, - swarm_id: String, + swarm_id: Option, partition: SemanticPartition, text: Option, query_id: Option, @@ -96,7 +97,7 @@ impl Options { runtime_store: None, agent_store: None, receipt_store: None, - swarm_id: String::new(), + swarm_id: None, partition: SemanticPartition::Modeling, text: None, query_id: None, @@ -114,7 +115,7 @@ impl Options { "--runtime-store" => options.runtime_store = Some(PathBuf::from(value()?)), "--agent-store" => options.agent_store = Some(PathBuf::from(value()?)), "--receipt-store" => options.receipt_store = Some(PathBuf::from(value()?)), - "--swarm-id" => options.swarm_id = value()?, + "--swarm-id" => options.swarm_id = Some(value()?), "--partition" => options.partition = parse_partition(&value()?)?, "--text" => options.text = Some(value()?), "--query-id" => options.query_id = Some(value()?), @@ -123,8 +124,12 @@ impl Options { _ => return Err(usage_error(&format!("unexpected argument {flag:?}"))), } } - if options.swarm_id.trim().is_empty() { - return Err(usage_error("--swarm-id is required")); + if options + .swarm_id + .as_ref() + .is_some_and(|id| id.trim().is_empty()) + { + return Err(usage_error("--swarm-id must not be empty")); } let sources = [ options.graph_store.is_some(), @@ -149,13 +154,26 @@ impl Options { "Modeling projection requires --runtime-store or --graph-store", )); } + if options.graph_store.is_some() && options.swarm_id.is_none() { + return Err(usage_error("--graph-store requires --swarm-id")); + } Ok(options) } - fn load_snapshot(&self) -> Result { + fn load_source(&self) -> Result<(EpiphanyMemoryGraphSnapshot, String)> { if let Some(path) = &self.runtime_store { - return runtime_current_repo_model(path)? - .ok_or_else(|| anyhow!("runtime store has no admitted RepoModel")); + let binding = runtime_swarm_binding(path)? + .ok_or_else(|| anyhow!("runtime store has no immutable swarm binding"))?; + if self + .swarm_id + .as_ref() + .is_some_and(|claimed| claimed != &binding.swarm_id) + { + return Err(anyhow!("--swarm-id does not match runtime swarm binding")); + } + let snapshot = runtime_current_repo_model(path)? + .ok_or_else(|| anyhow!("runtime store has no admitted RepoModel"))?; + return Ok((snapshot, binding.swarm_id)); } if let Some(path) = &self.agent_store { let mut entries = Vec::new(); @@ -169,21 +187,27 @@ impl Options { } let identity = epiphany_core::load_agent_memory_swarm_identity(path)? .ok_or_else(|| anyhow!("agent store has no immutable swarm identity; run epiphany-agent-memory-store set-swarm-identity"))?; - if self.swarm_id != identity.swarm_id { + if self + .swarm_id + .as_ref() + .is_some_and(|claimed| claimed != &identity.swarm_id) + { return Err(anyhow!( "--swarm-id {:?} does not match the canonical agent-store swarm identity {:?}", self.swarm_id, identity.swarm_id )); } - return Ok(memory_graph_from_agent_memories( - format!("{}-mind", identity.swarm_id), - &entries, + let swarm_id = identity.swarm_id; + return Ok(( + memory_graph_from_agent_memories(format!("{swarm_id}-mind"), &entries), + swarm_id, )); } let path = self.graph_store.as_ref().expect("validated graph store"); - load_memory_graph_snapshot(path)? - .ok_or_else(|| anyhow!("memory graph store {} is missing", path.display())) + let snapshot = load_memory_graph_snapshot(path)? + .ok_or_else(|| anyhow!("memory graph store {} is missing", path.display()))?; + Ok((snapshot, self.swarm_id.clone().expect("validated swarm id"))) } } @@ -202,7 +226,7 @@ fn parse_profile(value: &str) -> Result { fn usage_error(message: &str) -> anyhow::Error { anyhow!( - "{message}\nusage: epiphany-memory-semantic (--runtime-store |--agent-store |--graph-store ) --swarm-id --partition [--receipt-store ] [--text ] [--query-id ] [--budget ] [--profile ]" + "{message}\nusage: epiphany-memory-semantic (--runtime-store |--agent-store |--graph-store ) [--swarm-id ] --partition [--receipt-store ] [--text ] [--query-id ] [--budget ] [--profile ]" ) } diff --git a/epiphany-core/src/bin/epiphany-repo-model-bootstrap.rs b/epiphany-core/src/bin/epiphany-repo-model-bootstrap.rs index 7c779189..1e3bb6b3 100644 --- a/epiphany-core/src/bin/epiphany-repo-model-bootstrap.rs +++ b/epiphany-core/src/bin/epiphany-repo-model-bootstrap.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result, anyhow}; use chrono::{SecondsFormat, Utc}; use epiphany_core::{ - ensure_runtime_repo_model, load_thread_state, memory_graph_from_epiphany_graphs, + bind_runtime_to_agent_memory_swarm, ensure_runtime_repo_model, load_thread_state, + memory_graph_from_epiphany_graphs, migrate_legacy_repo_model_projection_obligation, }; use std::env; use std::path::{Path, PathBuf}; @@ -10,6 +11,7 @@ fn main() -> Result<()> { let mut args = env::args().skip(1); let runtime_store = required_path(&mut args, "--runtime-store")?; let thread_state_store = required_path(&mut args, "--thread-state-store")?; + let agent_store = required_path(&mut args, "--agent-store")?; if args.next().is_some() { return Err(usage("unexpected trailing arguments")); } @@ -39,12 +41,11 @@ fn main() -> Result<()> { // migration receipt. A deliberately absent legacy path prevents a stale // sibling graph from impersonating the typed thread-state bootstrap. let absent_legacy = runtime_store.with_extension("no-legacy-memory-graph"); - let (snapshot, receipt) = ensure_runtime_repo_model( - &runtime_store, - absent_legacy, - &bootstrap, - &Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), - )?; + let at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + let binding = bind_runtime_to_agent_memory_swarm(&runtime_store, &agent_store, &at)?; + migrate_legacy_repo_model_projection_obligation(&runtime_store)?; + let (snapshot, receipt) = + ensure_runtime_repo_model(&runtime_store, absent_legacy, &bootstrap, &at)?; println!( "{}", serde_json::to_string_pretty(&serde_json::json!({ @@ -56,6 +57,8 @@ fn main() -> Result<()> { "modelHash": snapshot.model_hash, "migrationReceiptId": receipt.receipt_id, "migrationSource": receipt.source_store, + "swarmId": binding.swarm_id, + "runtimeSwarmBindingId": binding.binding_id, }))? ); Ok(()) @@ -75,6 +78,6 @@ fn required_path(args: &mut impl Iterator, flag: &str) -> Result< fn usage(message: &str) -> anyhow::Error { anyhow!( - "{message}\nusage: epiphany-repo-model-bootstrap --runtime-store --thread-state-store " + "{message}\nusage: epiphany-repo-model-bootstrap --runtime-store --thread-state-store --agent-store " ) } diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 258cff97..b3f5ce49 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -792,6 +792,10 @@ pub(crate) mod tests { created_at: "2026-07-13T05:00:00Z".into(), }, )?; + crate::runtime_spine::tests::bind_test_runtime_swarm( + &store, + &format!("proposal-launch-swarm-{suffix}"), + )?; let state = EpiphanyThreadState::default(); let mut cache = coordinator_acceptance_cache(&store)?; cache.put( diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index f1860875..f1a5d3ea 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -815,6 +815,12 @@ mod tests { intent: &HandsActionIntent, review: &crate::HandsActionReview, ) -> anyhow::Result<()> { + if crate::runtime_swarm_binding(store)?.is_none() { + crate::runtime_spine::tests::bind_test_runtime_swarm( + store, + "context-hands-test-swarm", + )?; + } let bootstrap = EpiphanyMemoryGraphSnapshot { schema_version: Some(MEMORY_GRAPH_SCHEMA_VERSION.to_string()), graph_id: "context-runtime-model".to_string(), @@ -1022,6 +1028,18 @@ mod tests { )); fs::create_dir(&temp)?; let runtime_store = temp.join("runtime-spine.msgpack"); + initialize_runtime_spine( + &runtime_store, + RuntimeSpineInitOptions { + runtime_id: "canonical-model-launch-test".to_string(), + display_name: "Canonical Model Launch Test".to_string(), + created_at: "2026-06-12T00:00:00Z".to_string(), + }, + )?; + crate::runtime_spine::tests::bind_test_runtime_swarm( + &runtime_store, + "canonical-model-launch-swarm", + )?; let graph_store = memory_graph_store_path(&runtime_store); let mut snapshot = EpiphanyMemoryGraphSnapshot { schema_version: Some(MEMORY_GRAPH_SCHEMA_VERSION.to_string()), @@ -1096,6 +1114,21 @@ mod tests { std::env::temp_dir().join(format!("epiphany-bridge-launch-context-{}", Uuid::new_v4())); fs::create_dir(&temp)?; let runtime_store = temp.join("runtime-spine.msgpack"); + crate::initialize_runtime_spine( + &runtime_store, + crate::RuntimeSpineInitOptions { + runtime_id: "launch-context-runtime".to_string(), + display_name: "Launch context test".to_string(), + created_at: "2026-07-12T00:00:00Z".to_string(), + }, + )?; + let agent_store = temp.join("agents.msgpack"); + crate::ensure_agent_memory_swarm_identity(&agent_store, "launch-context-swarm")?; + crate::bind_runtime_to_agent_memory_swarm( + &runtime_store, + &agent_store, + "2026-07-12T00:00:01Z", + )?; let state = EpiphanyThreadState { revision: 7, objective: Some("Test launch context.".to_string()), diff --git a/epiphany-core/src/heartbeat_state.rs b/epiphany-core/src/heartbeat_state.rs index 59abac29..3ab01286 100644 --- a/epiphany-core/src/heartbeat_state.rs +++ b/epiphany-core/src/heartbeat_state.rs @@ -3867,8 +3867,8 @@ mod tests { let agent_store = temp.path().join("agents.msgpack"); let artifact_dir = temp.path().join("artifacts"); initialize_heartbeat_store(&store_path, 1.0)?; - crate::ensure_agent_memory_swarm_identity(&agent_store, "swarm-persona-memory-test")?; - crate::write_agent_memory_entry_for_role(&agent_store, &persona_memory_entry())?; + crate::ensure_agent_memory_swarm_identity(&agent_store, "heartbeat-test-swarm")?; + crate::write_agent_memory_entry_for_role_migration(&agent_store, &persona_memory_entry())?; queue_heartbeat_pending_mention_store( &store_path, HeartbeatQueueMentionOptions { diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 83512435..708fc481 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -74,13 +74,16 @@ pub use agent_launch::epiphany_worker_prompt; pub use agent_launch::render_epiphany_coordinator_note; pub use agent_launch::render_epiphany_pre_compaction_checkpoint_intervention; pub use agent_launch::unique_strings; +pub use agent_memory::AGENT_MEMORY_MIND_ADMISSION_TYPE; pub use agent_memory::AGENT_MEMORY_SWARM_IDENTITY_KEY; pub use agent_memory::AGENT_MEMORY_SWARM_IDENTITY_SCHEMA_VERSION; pub use agent_memory::AGENT_MEMORY_SWARM_IDENTITY_TYPE; pub use agent_memory::AgentCanonicalTraitSeed; pub use agent_memory::AgentMemoryBundle; +pub use agent_memory::AgentMemoryGenerationWitness; pub use agent_memory::AgentMemoryLifecycleAction; pub use agent_memory::AgentMemoryLifecycleOperation; +pub use agent_memory::AgentMemoryMindAdmissionReceipt; pub use agent_memory::AgentMemoryReview; pub use agent_memory::AgentMemorySwarmIdentity; pub use agent_memory::AgentSelfPatch; @@ -110,6 +113,7 @@ pub use agent_memory::apply_agent_self_patch; pub use agent_memory::apply_agent_self_patch_document; pub use agent_memory::ensure_agent_memory_swarm_identity; pub use agent_memory::load_agent_memory_entry_for_role; +pub use agent_memory::load_agent_memory_generation_witness; pub use agent_memory::load_agent_memory_swarm_identity; pub use agent_memory::load_agent_state_soa_entry; pub use agent_memory::migrate_agent_memory_json_dir_to_cultcache; @@ -123,7 +127,7 @@ pub use agent_memory::review_agent_memory_lifecycle_operation; pub use agent_memory::review_agent_self_patch; pub use agent_memory::review_agent_self_patch_document; pub use agent_memory::validate_agent_memory_store; -pub use agent_memory::write_agent_memory_entry_for_role; +pub use agent_memory::write_agent_memory_entry_for_role_migration; pub use agent_utterance_state::AGENT_CHARACTER_STATE_VECTOR_SIZE; pub use agent_utterance_state::AGENT_UTTERANCE_STATE_SCHEMA_VERSION; pub use agent_utterance_state::AgentUtteranceActivation; @@ -625,6 +629,8 @@ pub use memory_graph::MEMORY_GRAPH_KEY; pub use memory_graph::MEMORY_GRAPH_SCHEMA_VERSION; pub use memory_graph::MEMORY_GRAPH_TYPE; pub use memory_graph::MEMORY_SEMANTIC_INDEX_RECEIPT_SCHEMA_VERSION; +pub use memory_graph::MEMORY_SEMANTIC_PROJECTION_ATTEMPT_SCHEMA_VERSION; +pub use memory_graph::MEMORY_SEMANTIC_PROJECTION_OBLIGATION_SCHEMA_VERSION; pub use memory_graph::MemorySemanticIndexConfig; pub use memory_graph::MemorySemanticIndexReceipt; pub use memory_graph::MemorySemanticProjectionAttempt; @@ -632,13 +638,6 @@ pub use memory_graph::MemorySemanticProjectionHealth; pub use memory_graph::MemorySemanticProjectionHealthStatus; pub use memory_graph::MemorySemanticProjectionObligation; pub use memory_graph::MemorySemanticProjectionSourceHead; -pub use memory_graph::MEMORY_SEMANTIC_PROJECTION_ATTEMPT_SCHEMA_VERSION; -pub use memory_graph::MEMORY_SEMANTIC_PROJECTION_OBLIGATION_SCHEMA_VERSION; -pub use memory_graph::bind_memory_semantic_index_receipt; -pub use memory_graph::derive_memory_semantic_projection_health; -pub use memory_graph::memory_semantic_projection_query_eligible; -pub use memory_graph::validate_memory_semantic_projection_attempt; -pub use memory_graph::validate_memory_semantic_projection_obligation; pub use memory_graph::RepoFrontierAdoptedPlan; pub use memory_graph::RepoFrontierItem; pub use memory_graph::RepoFrontierStatus; @@ -656,8 +655,11 @@ pub use memory_graph::SemanticProjectionCandidate; pub use memory_graph::SemanticProjectionDocument; pub use memory_graph::SemanticVectorIndex; pub use memory_graph::SemanticVisibility; +pub use memory_graph::bind_memory_semantic_index_receipt; pub use memory_graph::compose_memory_graph_snapshots; pub use memory_graph::derive_memory_graph_freshness; +pub use memory_graph::derive_memory_semantic_projection_health; +pub use memory_graph::derive_memory_semantic_projection_obligation; pub use memory_graph::derive_repo_model_patch; pub use memory_graph::derive_semantic_projection; pub use memory_graph::index_memory_semantic_partition; @@ -672,6 +674,7 @@ pub use memory_graph::memory_graph_from_epiphany_graphs; pub use memory_graph::memory_graph_from_heartbeat_cognition; pub use memory_graph::memory_graph_model_hash; pub use memory_graph::memory_graph_node_id; +pub use memory_graph::memory_semantic_projection_query_eligible; pub use memory_graph::persist_memory_semantic_index_receipt; pub use memory_graph::plan_memory_graph_context_cut; pub use memory_graph::plan_memory_graph_context_cut_with_ranked_ids; @@ -681,6 +684,8 @@ pub use memory_graph::semantic_memory_context; pub use memory_graph::semantic_point_id; pub use memory_graph::validate_memory_graph_entry; pub use memory_graph::validate_memory_graph_snapshot; +pub use memory_graph::validate_memory_semantic_projection_attempt; +pub use memory_graph::validate_memory_semantic_projection_obligation; pub use memory_graph::write_memory_graph_entry; pub use memory_graph::write_memory_graph_snapshot; pub use mind_gateway::MIND_GATEWAY_REVIEW_SCHEMA_VERSION; @@ -782,6 +787,7 @@ pub use runtime_spine::EpiphanyRuntimeRoleWorkerResult; pub use runtime_spine::EpiphanyRuntimeSession; pub use runtime_spine::EpiphanyRuntimeSessionStatus; pub use runtime_spine::EpiphanyRuntimeSpineStatus; +pub use runtime_spine::EpiphanyRuntimeSwarmBinding; pub use runtime_spine::EpiphanyRuntimeWorkerLaunchRequest; pub use runtime_spine::EpiphanyToolInvocationStatus; pub use runtime_spine::PreparedRuntimeSpineHeartbeatJob; @@ -796,6 +802,8 @@ pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION; pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_TYPE; pub use runtime_spine::RUNTIME_SESSION_TYPE; pub use runtime_spine::RUNTIME_SPINE_SCHEMA_VERSION; +pub use runtime_spine::RUNTIME_SWARM_BINDING_KEY; +pub use runtime_spine::RUNTIME_SWARM_BINDING_SCHEMA_VERSION; pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION; pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_TYPE; pub use runtime_spine::RuntimeHandsReceiptChainSummary; @@ -808,6 +816,7 @@ pub use runtime_spine::RuntimeSpineJobOptions; pub use runtime_spine::RuntimeSpineJobResultOptions; pub use runtime_spine::RuntimeSpineSessionOptions; pub use runtime_spine::append_runtime_event; +pub use runtime_spine::bind_runtime_to_agent_memory_swarm; pub use runtime_spine::canonical_repo_frontier_plan_candidate_id; pub use runtime_spine::clear_epiphany_job_binding_backend; pub use runtime_spine::commit_repo_frontier_modeling_request; @@ -824,6 +833,7 @@ pub use runtime_spine::ensure_runtime_repo_model; pub use runtime_spine::ensure_runtime_session; pub use runtime_spine::initialize_runtime_spine; pub use runtime_spine::intake_user_repo_frontier_proposal; +pub use runtime_spine::migrate_legacy_repo_model_projection_obligation; pub use runtime_spine::open_runtime_spine_heartbeat_job; pub use runtime_spine::plan_runtime_spine_heartbeat_launch; pub use runtime_spine::prepare_runtime_spine_heartbeat_job; @@ -875,6 +885,7 @@ pub use runtime_spine::runtime_soul_verdict_receipt; pub use runtime_spine::runtime_spine_cache; pub use runtime_spine::runtime_spine_status; pub use runtime_spine::runtime_substrate_gate_repo_access_grant_receipt; +pub use runtime_spine::runtime_swarm_binding; pub use runtime_spine::runtime_tool_invocation_statuses; pub use runtime_spine::runtime_worker_launch_request; pub use runtime_spine::select_and_commit_repo_frontier_planning_request; diff --git a/epiphany-core/src/memory_graph/semantic_index.rs b/epiphany-core/src/memory_graph/semantic_index.rs index e2fbac31..36b3fb17 100644 --- a/epiphany-core/src/memory_graph/semantic_index.rs +++ b/epiphany-core/src/memory_graph/semantic_index.rs @@ -279,6 +279,54 @@ pub fn validate_memory_semantic_projection_obligation( Ok(()) } +pub fn derive_memory_semantic_projection_obligation( + snapshot: &EpiphanyMemoryGraphSnapshot, + swarm_id: &str, + partition: SemanticPartition, + canonical_source_id: &str, + source_commit_id: &str, + created_at: &str, +) -> Result { + for (label, value) in [ + ("swarm_id", swarm_id), + ("canonical_source_id", canonical_source_id), + ("source_commit_id", source_commit_id), + ("created_at", created_at), + ] { + if value.trim().is_empty() { + return Err(anyhow!("semantic projection obligation missing {label}")); + } + } + chrono::DateTime::parse_from_rfc3339(created_at) + .map_err(|_| anyhow!("semantic projection obligation timestamp must be RFC3339"))?; + let documents = derive_semantic_projection(swarm_id, snapshot)? + .into_iter() + .filter(|document| document.partition == partition) + .collect::>(); + let canonical_content_set_hash = canonical_content_set_hash(&documents); + let partition = partition_name(partition).to_string(); + let identity = format!("{swarm_id}|{partition}|{canonical_source_id}|{source_commit_id}"); + let obligation = MemorySemanticProjectionObligation { + schema_version: MEMORY_SEMANTIC_PROJECTION_OBLIGATION_SCHEMA_VERSION.to_string(), + obligation_id: format!( + "memory-semantic-projection-{partition}-{:x}", + Sha256::digest(identity.as_bytes()) + ), + swarm_id: swarm_id.to_string(), + partition, + canonical_source_id: canonical_source_id.to_string(), + source_commit_id: source_commit_id.to_string(), + graph_id: snapshot.graph_id.clone(), + source_generation: snapshot.model_revision, + source_model_hash: crate::memory_graph_model_hash(snapshot)?, + canonical_content_set_hash, + projection_schema_version: SEMANTIC_PROJECTION_SCHEMA_VERSION.to_string(), + created_at: created_at.to_string(), + }; + validate_memory_semantic_projection_obligation(&obligation)?; + Ok(obligation) +} + pub fn validate_memory_semantic_projection_attempt( attempt: &MemorySemanticProjectionAttempt, ) -> Result<()> { @@ -761,6 +809,40 @@ mod tests { snapshot } + #[test] + fn projection_obligation_identity_is_commit_owned_and_deterministic() -> Result<()> { + let snapshot = snapshot(); + let first = derive_memory_semantic_projection_obligation( + &snapshot, + "swarm-a", + SemanticPartition::Modeling, + "epiphany.runtime/runtime-a/repo-model", + "repo-model-admission-1", + "2026-07-15T00:00:00Z", + )?; + assert_eq!( + derive_memory_semantic_projection_obligation( + &snapshot, + "swarm-a", + SemanticPartition::Modeling, + "epiphany.runtime/runtime-a/repo-model", + "repo-model-admission-1", + "2026-07-15T00:00:00Z", + )?, + first + ); + let second = derive_memory_semantic_projection_obligation( + &snapshot, + "swarm-a", + SemanticPartition::Modeling, + "epiphany.runtime/runtime-a/repo-model", + "repo-model-admission-2", + "2026-07-15T00:00:01Z", + )?; + assert_ne!(first.obligation_id, second.obligation_id); + Ok(()) + } + #[test] fn unavailable_vector_service_preserves_canonical_bm25_packet() { let snapshot = snapshot(); diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index afc3bce7..8643639d 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -5,7 +5,10 @@ use crate::agent_launch::{ EPIPHANY_MIND_OWNER_ROLE, EPIPHANY_MIND_ROLE_BINDING_ID, EPIPHANY_MODELING_OWNER_ROLE, EPIPHANY_MODELING_ROLE_BINDING_ID, }; -use crate::agent_memory::AGENT_MEMORY_TYPE; +use crate::agent_memory::{ + AGENT_MEMORY_SWARM_IDENTITY_KEY, AGENT_MEMORY_SWARM_IDENTITY_SCHEMA_VERSION, + AGENT_MEMORY_SWARM_IDENTITY_TYPE, AGENT_MEMORY_TYPE, load_agent_memory_swarm_identity, +}; use crate::continuity_gateway::ContinuityRecoveryReceipt; use crate::continuity_gateway::*; use crate::cultmesh_integration::EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION; @@ -180,6 +183,9 @@ pub const SURFACE_VOID_MEMORY_TYPE: &str = "epiphany.surface.void_memory"; pub const SURFACE_REPO_INITIALIZATION_TYPE: &str = "epiphany.surface.repo_initialization"; pub const SURFACE_REPO_BIRTH_RUNNER_TYPE: &str = "epiphany.surface.repo_birth_runner"; pub const RUNTIME_IDENTITY_KEY: &str = "self"; +pub const RUNTIME_SWARM_BINDING_TYPE: &str = "epiphany.runtime.swarm_binding"; +pub const RUNTIME_SWARM_BINDING_KEY: &str = "runtime-swarm-binding"; +pub const RUNTIME_SWARM_BINDING_SCHEMA_VERSION: &str = "epiphany.runtime.swarm_binding.v0"; pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0"; pub const RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION: &str = "epiphany.runtime.worker_launch_request.v1"; @@ -263,6 +269,30 @@ pub struct EpiphanyRuntimeIdentity { pub metadata: BTreeMap, } +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.runtime.swarm_binding", + schema = "EpiphanyRuntimeSwarmBinding" +)] +pub struct EpiphanyRuntimeSwarmBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub swarm_id: String, + #[cultcache(key = 4)] + pub source_identity_type: String, + #[cultcache(key = 5)] + pub source_identity_key: String, + #[cultcache(key = 6)] + pub source_identity_sha256: String, + #[cultcache(key = 7)] + pub bound_at: String, +} + #[derive(Clone, Debug, PartialEq, DatabaseEntry)] #[cultcache(type = "epiphany.runtime.session", schema = "EpiphanyRuntimeSession")] pub struct EpiphanyRuntimeSession { @@ -811,6 +841,8 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { let mut cache = CultCache::new(); cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -897,6 +929,148 @@ pub fn initialize_runtime_spine( Ok(identity) } +pub fn bind_runtime_to_agent_memory_swarm( + runtime_store: impl AsRef, + agent_store: impl AsRef, + bound_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(bound_at) + .map_err(|_| anyhow!("runtime swarm binding timestamp must be RFC3339"))?; + let source = load_agent_memory_swarm_identity(agent_store)? + .ok_or_else(|| anyhow!("agent memory store has no canonical swarm identity"))?; + if source.schema_version != AGENT_MEMORY_SWARM_IDENTITY_SCHEMA_VERSION { + return Err(anyhow!("unsupported canonical agent-memory swarm identity")); + } + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + let identity = cache + .get::(RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("runtime swarm binding requires runtime identity"))?; + let binding = EpiphanyRuntimeSwarmBinding { + schema_version: RUNTIME_SWARM_BINDING_SCHEMA_VERSION.to_string(), + binding_id: RUNTIME_SWARM_BINDING_KEY.to_string(), + runtime_id: identity.runtime_id.clone(), + swarm_id: source.swarm_id.clone(), + source_identity_type: AGENT_MEMORY_SWARM_IDENTITY_TYPE.to_string(), + source_identity_key: AGENT_MEMORY_SWARM_IDENTITY_KEY.to_string(), + source_identity_sha256: format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&source)?)), + bound_at: bound_at.to_string(), + }; + if let Some(existing) = cache.get::(RUNTIME_SWARM_BINDING_KEY)? { + return if existing.schema_version == binding.schema_version + && existing.binding_id == binding.binding_id + && existing.runtime_id == binding.runtime_id + && existing.swarm_id == binding.swarm_id + && existing.source_identity_type == binding.source_identity_type + && existing.source_identity_key == binding.source_identity_key + && existing.source_identity_sha256 == binding.source_identity_sha256 + { + Ok(existing) + } else { + Err(anyhow!("runtime swarm binding identity collision")) + }; + } + let backing = SingleFileMessagePackBackingStore::new(runtime_store); + let identity_envelope = backing + .pull_all()? + .into_iter() + .find(|entry| entry.r#type == RUNTIME_IDENTITY_TYPE && entry.key == RUNTIME_IDENTITY_KEY) + .ok_or_else(|| anyhow!("runtime swarm binding lost runtime identity envelope"))?; + let (binding_envelope, _) = cache.prepare_entry(RUNTIME_SWARM_BINDING_KEY, &binding)?; + if backing.compare_and_swap_batch( + &[identity_envelope.clone()], + vec![identity_envelope, binding_envelope], + )? { + return Ok(binding); + } + let mut reloaded = runtime_spine_cache(runtime_store)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(RUNTIME_SWARM_BINDING_KEY)? { + Some(existing) if existing == binding => Ok(existing), + _ => Err(anyhow!("runtime swarm binding lost immutable CAS")), + } +} + +pub fn runtime_swarm_binding( + runtime_store: impl AsRef, +) -> Result> { + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + cache.get(RUNTIME_SWARM_BINDING_KEY) +} + +fn require_runtime_swarm_binding(cache: &CultCache) -> Result { + let identity = require_identity(cache)?; + let binding = cache + .get::(RUNTIME_SWARM_BINDING_KEY)? + .ok_or_else(|| anyhow!("RepoModel admission requires immutable runtime swarm binding"))?; + if binding.schema_version != RUNTIME_SWARM_BINDING_SCHEMA_VERSION + || binding.binding_id != RUNTIME_SWARM_BINDING_KEY + || binding.runtime_id != identity.runtime_id + || binding.swarm_id.trim().is_empty() + || binding.source_identity_type != AGENT_MEMORY_SWARM_IDENTITY_TYPE + || binding.source_identity_key != AGENT_MEMORY_SWARM_IDENTITY_KEY + || binding.source_identity_sha256.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&binding.bound_at).is_err() + { + return Err(anyhow!("runtime swarm binding is invalid")); + } + Ok(binding) +} + +fn modeling_projection_obligation( + cache: &CultCache, + snapshot: &crate::EpiphanyMemoryGraphSnapshot, + source_commit_id: &str, + created_at: &str, +) -> Result { + let binding = require_runtime_swarm_binding(cache)?; + crate::derive_memory_semantic_projection_obligation( + snapshot, + &binding.swarm_id, + crate::SemanticPartition::Modeling, + &format!("epiphany.runtime/{}/repo-model", binding.runtime_id), + source_commit_id, + created_at, + ) +} + +fn require_modeling_projection_obligation_for_commit( + cache: &CultCache, + source_commit_id: &str, + source_generation: u64, + source_model_hash: &str, + created_at: &str, +) -> Result { + let binding = require_runtime_swarm_binding(cache)?; + let canonical_source_id = format!("epiphany.runtime/{}/repo-model", binding.runtime_id); + let matches = cache + .get_all::()? + .into_iter() + .filter(|obligation| obligation.source_commit_id == source_commit_id) + .collect::>(); + if matches.len() != 1 { + return Err(anyhow!( + "RepoModel commit requires exactly one semantic projection obligation" + )); + } + let obligation = matches.into_iter().next().expect("one obligation"); + crate::validate_memory_semantic_projection_obligation(&obligation)?; + if obligation.swarm_id != binding.swarm_id + || obligation.partition != "modeling" + || obligation.canonical_source_id != canonical_source_id + || obligation.source_generation != source_generation + || obligation.source_model_hash != source_model_hash + || obligation.created_at != created_at + { + return Err(anyhow!( + "RepoModel semantic projection obligation collision" + )); + } + Ok(obligation) +} + pub fn create_runtime_session( store_path: impl AsRef, options: RuntimeSpineSessionOptions, @@ -1827,7 +2001,15 @@ pub fn ensure_runtime_repo_model( let receipt = cache .get::("repo-model-migration")? .ok_or_else(|| anyhow!("runtime repo model exists without its migration receipt"))?; - return Ok((entry.snapshot()?, receipt)); + let snapshot = entry.snapshot()?; + require_modeling_projection_obligation_for_commit( + &cache, + &receipt.receipt_id, + receipt.imported_revision, + &receipt.imported_hash, + &receipt.imported_at, + )?; + return Ok((snapshot, receipt)); } let (snapshot, source_store) = @@ -1850,8 +2032,18 @@ pub fn ensure_runtime_repo_model( }; let (model_envelope, _) = cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &entry)?; let (receipt_envelope, _) = cache.prepare_entry(&receipt.receipt_id, &receipt)?; + let obligation = modeling_projection_obligation( + &cache, + &snapshot, + &receipt.receipt_id, + &receipt.imported_at, + )?; + let (obligation_envelope, _) = cache.prepare_entry(&obligation.obligation_id, &obligation)?; let backing = SingleFileMessagePackBackingStore::new(runtime_store); - if backing.compare_and_swap_batch(&[], vec![model_envelope, receipt_envelope])? { + if backing.compare_and_swap_batch( + &[], + vec![model_envelope, receipt_envelope, obligation_envelope], + )? { return Ok((snapshot, receipt)); } let mut reloaded = runtime_spine_cache(runtime_store)?; @@ -1871,6 +2063,13 @@ pub fn ensure_runtime_repo_model( { return Err(anyhow!("runtime repo model migration companion collision")); } + require_modeling_projection_obligation_for_commit( + &reloaded, + &existing_receipt.receipt_id, + existing_receipt.imported_revision, + &existing_receipt.imported_hash, + &existing_receipt.imported_at, + )?; Ok((snapshot, existing_receipt)) } _ => Err(anyhow!( @@ -1879,6 +2078,84 @@ pub fn ensure_runtime_repo_model( } } +pub fn migrate_legacy_repo_model_projection_obligation( + runtime_store: impl AsRef, +) -> Result> { + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + let Some(entry) = cache.get::(crate::MEMORY_GRAPH_KEY)? else { + return Ok(None); + }; + crate::validate_memory_graph_entry(&entry)?; + let snapshot = entry.snapshot()?; + let receipt = cache + .get::("repo-model-migration")? + .ok_or_else(|| anyhow!("legacy RepoModel has no migration receipt"))?; + if receipt.imported_revision != snapshot.model_revision + || receipt.imported_hash != crate::memory_graph_model_hash(&snapshot)? + { + return Err(anyhow!( + "legacy projection-obligation migration only accepts an unchanged bootstrap RepoModel" + )); + } + let expected = modeling_projection_obligation( + &cache, + &snapshot, + &receipt.receipt_id, + &receipt.imported_at, + )?; + let matches = cache + .get_all::()? + .into_iter() + .filter(|candidate| candidate.source_commit_id == receipt.receipt_id) + .collect::>(); + if matches.len() == 1 && matches[0] == expected { + return Ok(Some(expected)); + } + if !matches.is_empty() { + return Err(anyhow!( + "legacy RepoModel projection obligation identity collision" + )); + } + let backing = SingleFileMessagePackBackingStore::new(runtime_store); + let opening = backing.pull_all()?; + let preserve_types = [ + (crate::MEMORY_GRAPH_TYPE, crate::MEMORY_GRAPH_KEY), + ( + REPO_MODEL_MIGRATION_RECEIPT_TYPE, + receipt.receipt_id.as_str(), + ), + (RUNTIME_SWARM_BINDING_TYPE, RUNTIME_SWARM_BINDING_KEY), + ]; + let mut preserved = Vec::new(); + for (document_type, key) in preserve_types { + preserved.push( + opening + .iter() + .find(|envelope| envelope.r#type == document_type && envelope.key == key) + .cloned() + .ok_or_else(|| anyhow!("legacy RepoModel migration companion disappeared"))?, + ); + } + let obligation_envelope = cache.prepare_entry(&expected.obligation_id, &expected)?.0; + let mut replacement = preserved.clone(); + replacement.push(obligation_envelope); + if backing.compare_and_swap_batch(&preserved, replacement)? { + return Ok(Some(expected)); + } + let mut reloaded = runtime_spine_cache(runtime_store)?; + reloaded.pull_all_backing_stores()?; + require_modeling_projection_obligation_for_commit( + &reloaded, + &receipt.receipt_id, + receipt.imported_revision, + &receipt.imported_hash, + &receipt.imported_at, + ) + .map(Some) +} + pub fn commit_repo_model_admission( runtime_store: impl AsRef, result_id: &str, @@ -2640,6 +2917,13 @@ pub fn commit_repo_model_admission( { return Err(anyhow!("repo model admission receipt identity collision")); } + require_modeling_projection_obligation_for_commit( + &cache, + &existing_receipt.receipt_id, + existing_receipt.admitted_revision, + &existing_receipt.admitted_hash, + &existing_receipt.admitted_at, + )?; return Ok(existing_receipt); } (None, None) => {} @@ -2761,9 +3045,17 @@ pub fn commit_repo_model_admission( let (next_model_envelope, _) = cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &next_entry)?; let (review_envelope, _) = cache.prepare_entry(&review.review_id, review)?; let (receipt_envelope, _) = cache.prepare_entry(&receipt_id, &receipt)?; + let obligation = + modeling_projection_obligation(&cache, &next, &receipt.receipt_id, &receipt.admitted_at)?; + let (obligation_envelope, _) = cache.prepare_entry(&obligation.obligation_id, &obligation)?; if !backing.compare_and_swap_batch( &[current_envelope], - vec![next_model_envelope, review_envelope, receipt_envelope], + vec![ + next_model_envelope, + review_envelope, + receipt_envelope, + obligation_envelope, + ], )? { return Err(anyhow!( "repo model admission stale model or companion collision" @@ -4079,6 +4371,13 @@ fn commit_repo_frontier_plan_decision_inner( { return Err(anyhow!("Adopt retry model admission chain mismatch")); } + require_modeling_projection_obligation_for_commit( + &cache, + &admission.receipt_id, + admission.admitted_revision, + &admission.admitted_hash, + &admission.admitted_at, + )?; } return Ok(existing.clone()); } @@ -4205,9 +4504,20 @@ fn commit_repo_frontier_plan_decision_inner( claim_repair_request_id: String::new(), frontier_plan_decision_id: decision_id.clone(), }; + let obligation = modeling_projection_obligation( + &cache, + &next, + &admission.receipt_id, + &admission.admitted_at, + )?; receipt.model_admission_receipt_id = admission_id; companions.push(cache.prepare_entry(&review.review_id, &review)?.0); companions.push(cache.prepare_entry(&admission.receipt_id, &admission)?.0); + companions.push( + cache + .prepare_entry(&obligation.obligation_id, &obligation)? + .0, + ); cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &next_entry)?.0 } else { current_envelope.clone() @@ -7509,6 +7819,56 @@ pub(crate) mod tests { } } + pub(crate) fn bind_test_runtime_swarm(store: &Path, swarm_id: &str) -> Result<()> { + let agent_store = store.with_extension("test-agent-memory.cc"); + crate::ensure_agent_memory_swarm_identity(&agent_store, swarm_id)?; + bind_runtime_to_agent_memory_swarm(store, &agent_store, "2026-07-13T00:00:01Z")?; + Ok(()) + } + + #[test] + fn legacy_repo_model_migration_adds_only_exact_projection_pressure() -> Result<()> { + let root = tempdir()?; + let store = root.path().join("legacy-repo-model.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "legacy-runtime".into(), + display_name: "Legacy runtime".into(), + created_at: "2026-07-15T00:00:00Z".into(), + }, + )?; + bind_test_runtime_swarm(&store, "legacy-swarm")?; + let snapshot = repo_model_bootstrap(); + let entry = crate::EpiphanyMemoryGraphEntry::from_snapshot(&snapshot)?; + let model_hash = crate::memory_graph_model_hash(&snapshot)?; + let receipt = RepoModelMigrationReceipt { + schema_version: REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "repo-model-migration".into(), + source_store: "legacy-test".into(), + source_graph_id: snapshot.graph_id.clone(), + imported_revision: snapshot.model_revision, + imported_hash: model_hash, + imported_at: "2026-07-15T00:00:01Z".into(), + contract: REPO_MODEL_MIGRATION_CONTRACT.to_string(), + }; + let mut cache = runtime_spine_cache(&store)?; + cache.put(crate::MEMORY_GRAPH_KEY, &entry)?; + cache.put(&receipt.receipt_id, &receipt)?; + + let obligation = migrate_legacy_repo_model_projection_obligation(&store)? + .expect("legacy projection obligation"); + assert_eq!(obligation.source_commit_id, receipt.receipt_id); + assert_eq!(obligation.source_generation, snapshot.model_revision); + assert_eq!(obligation.partition, "modeling"); + assert_eq!( + migrate_legacy_repo_model_projection_obligation(&store)?, + Some(obligation.clone()) + ); + assert_eq!(runtime_current_repo_model(&store)?, Some(snapshot)); + Ok(()) + } + fn repo_model_result_and_review( store: &Path, result_id: &str, @@ -7663,6 +8023,7 @@ pub(crate) mod tests { created_at: "2026-07-13T03:00:00Z".into(), }, )?; + bind_test_runtime_swarm(&store, &format!("proposal-swarm-{suffix}"))?; let bootstrap = repo_model_bootstrap(); let legacy = root.join(format!("proposal-{suffix}.legacy")); let (current, _) = @@ -8011,6 +8372,7 @@ pub(crate) mod tests { created_at: "2026-07-13T02:00:00Z".into(), }, )?; + bind_test_runtime_swarm(&store, "proposal-intake-swarm")?; let mut cache = runtime_spine_cache(&store)?; cache.put( crate::THREAD_STATE_KEY, @@ -8126,6 +8488,7 @@ pub(crate) mod tests { created_at: "2026-07-13T01:00:00Z".into(), }, )?; + bind_test_runtime_swarm(&store, "proposal-intake-swarm")?; let mut cache = runtime_spine_cache(&store)?; cache.put( crate::THREAD_STATE_KEY, @@ -8667,6 +9030,9 @@ pub(crate) mod tests { review: &HandsActionReview, suffix: &str, ) -> Result<(RepoFrontierRoute, RepoFrontierHandsAuthority)> { + if runtime_swarm_binding(store)?.is_none() { + bind_test_runtime_swarm(store, &format!("route-swarm-{suffix}"))?; + } let bootstrap = repo_model_bootstrap(); let legacy = store.with_extension(format!("{suffix}.legacy.msgpack")); let (current, _) = @@ -8995,6 +9361,7 @@ pub(crate) mod tests { created_at: "2026-07-13T00:00:00Z".to_string(), }, )?; + bind_test_runtime_swarm(&store, "repo-model-test-swarm")?; let bootstrap = repo_model_bootstrap(); let (current, migration) = ensure_runtime_repo_model( &store, @@ -9003,6 +9370,14 @@ pub(crate) mod tests { "2026-07-13T03:00:00Z", )?; assert_eq!(migration.imported_revision, 0); + let mut projection_cache = runtime_spine_cache(&store)?; + projection_cache.pull_all_backing_stores()?; + assert_eq!( + projection_cache + .get_all::()? + .len(), + 1 + ); let (result, review) = repo_model_result_and_review( &store, "model-result-1", @@ -9013,6 +9388,14 @@ pub(crate) mod tests { put_runtime_role_worker_result(&store, &result)?; let receipt = commit_repo_model_admission(&store, &result.result_id, &review)?; assert_eq!(receipt.admitted_revision, 1); + let mut projection_cache = runtime_spine_cache(&store)?; + projection_cache.pull_all_backing_stores()?; + assert_eq!( + projection_cache + .get_all::()? + .len(), + 2 + ); assert_eq!( commit_repo_model_admission(&store, &result.result_id, &review)?, receipt @@ -9079,6 +9462,46 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn repo_model_bootstrap_requires_exact_immutable_runtime_swarm_binding() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime-binding.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "runtime-binding-test".into(), + display_name: "Runtime binding test".into(), + created_at: "2026-07-13T00:00:00Z".into(), + }, + )?; + let before = fs::read(&store)?; + assert!( + ensure_runtime_repo_model( + &store, + temp.path().join("absent.cc"), + &repo_model_bootstrap(), + "2026-07-13T00:00:02Z" + ) + .is_err() + ); + assert_eq!(fs::read(&store)?, before); + let agent_store = temp.path().join("agents.cc"); + crate::ensure_agent_memory_swarm_identity(&agent_store, "swarm-binding-test")?; + let binding = + bind_runtime_to_agent_memory_swarm(&store, &agent_store, "2026-07-13T00:00:01Z")?; + assert_eq!( + bind_runtime_to_agent_memory_swarm(&store, &agent_store, "2026-07-13T00:00:01Z")?, + binding + ); + let other_agents = temp.path().join("other-agents.cc"); + crate::ensure_agent_memory_swarm_identity(&other_agents, "other-swarm")?; + assert!( + bind_runtime_to_agent_memory_swarm(&store, &other_agents, "2026-07-13T00:00:01Z") + .is_err() + ); + Ok(()) + } + #[test] fn evolution_cannot_bypass_routes_or_own_frontier_verdict_lifecycle() -> Result<()> { let routed_temp = tempdir()?; @@ -9108,6 +9531,7 @@ pub(crate) mod tests { created_at: "2026-07-13T08:00:00Z".to_string(), }, )?; + bind_test_runtime_swarm(&store, "evolution-lifecycle-swarm")?; let (base, _) = ensure_runtime_repo_model( &store, temp.path().join("legacy.cc"), @@ -9401,6 +9825,9 @@ pub(crate) mod tests { suffix: &str, items: Vec, ) -> Result<()> { + if runtime_swarm_binding(store)?.is_none() { + bind_test_runtime_swarm(store, &format!("eligibility-swarm-{suffix}"))?; + } let bootstrap = repo_model_bootstrap(); let legacy = store.with_extension(format!("{suffix}.legacy.msgpack")); let (mut current, _) = @@ -9440,6 +9867,7 @@ pub(crate) mod tests { created_at: "2026-07-13T05:00:00Z".to_string(), }, )?; + bind_test_runtime_swarm(&unadmitted_store, "route-unadmitted-swarm")?; ensure_runtime_repo_model( &unadmitted_store, unadmitted.path().join("legacy.msgpack"), diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index c965dee2..76585924 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -1089,3 +1089,23 @@ admission does not yet atomically emit obligations, indexing does not yet claim and discharge them, queries do not yet require the newest exact success, and CultMesh/Idunn wiring deliberately remains absent. The next cut is canonical admission ownership, not a polling loop. + +## Atomic semantic admission (2026-07-15) + +The causal cut is now implemented. Runtime-spine is immutably bound to the +canonical agent-Mind swarm identity. All three RepoModel writers atomically +companion canonical admission with one exact Modeling projection obligation; +the live legacy runtime was explicitly migrated without rewriting its model or +migration receipt. Mind self-patch and lifecycle admission now use a fixed-role +generation CAS that writes canonical rows, generation witness, persisted Mind +admission receipt, and Mind obligation together. Every extension re-hashes the +canonical rows and authenticates the prior witness. Bootstrap/import/repair/raw +replacement/trait seeding cannot write after a generation exists. + +Core proof after fixture repair: 342 tests passed, 0 failed, 1 intentional +ignore; focused legacy migration and hostile concurrent Mind CAS tests pass; +all binaries compile. The current `state/agents.msgpack` predates the generation +witness and still needs one explicit legacy Mind-generation migration before +query gating can regard its Qdrant projection as ready. Next implement +restart-safe projector claim/attempt/terminal receipt execution and exact query +eligibility, then CultMesh/Idunn physiology. diff --git a/notes/memory-graph-semantic-projection-authority-map.md b/notes/memory-graph-semantic-projection-authority-map.md index e3d12da9..57a9be02 100644 --- a/notes/memory-graph-semantic-projection-authority-map.md +++ b/notes/memory-graph-semantic-projection-authority-map.md @@ -248,3 +248,34 @@ the command from a timer would make elapsed time a substitute for causality. then add the Idunn-hosted consumer and CultMesh health projection. Do not add polling flags, file-mtime inference, or a reconciliation cache around the current unwitnessed writes. + +## Atomic Admission Landed (2026-07-15) + +- Runtime-spine now carries one immutable binding to the canonical Mind-store + swarm identity. Runtime id, graph id, paths, metadata, and CLI labels cannot + substitute for it. +- RepoModel bootstrap, generic Modeling admission, and frontier-plan Adopt each + commit their canonical model witness and one deterministically derived + Modeling projection obligation in the same CAS. Exact retries require the + obligation; missing or colliding companions fail closed. +- A one-time legacy runtime migration preserves the unchanged bootstrap model, + migration receipt, and swarm binding envelopes while adding only the missing + obligation. The live runtime store passed this migration under + `gamecult.epiphany.main`. +- Reviewed Mind self-patches and lifecycle operations now commit the complete + canonical role generation, immutable/latest generation witness, persisted + Mind admission receipt, and exact Mind projection obligation in one CAS. + The canonical source hash is recomputed in fixed role order on every + admission and must authenticate the previous witness. +- JSON import, legacy repair, raw migration replacement, and canonical trait + seeding are explicitly bootstrap-only and structurally refuse after the + first admitted generation. SoA remains derived and cannot advance Mind. +- Empty canonical partitions still produce obligations. Empty is a demand to + remove stale scoped points, not permission to preserve a previous + projection. + +Remaining: migrate the current legacy Mind rows into generation zero pressure; +add projector claims/attempt terminal CAS and bind index receipts to exact +obligations; make queries require the newest exact success before Qdrant +ranking; then publish derived health through provider-owned CultMesh state and +attach Idunn process survival. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index c21b9d671b7a254d8148c537c20745423be4717c..879ef32b69c329a0059bc2849e341dfd1f1d6c71 100644 GIT binary patch delta 698 zcmZ{iy^GX97{<-I8bKRD{DkA>1RG7retEE49gF1^o>qs=o8;Z>U}h#aGubuGEn*>@ z78WjPtSrSs5E1_YK^y-i-%SutIE$D0o_U_%Ge6&L{dm9i>AH7uG#rn52j9>5Z|;1) z?HzjFhZpP4%XP=&+O%i6x?JbD&VOBZ{4cMry#3aX`uBrqC+LkHM}slnF|42e@@|PI zyG~1yfF+5_aR*Z=tQA^;(o2YiN-f0N&A~3wNZ8{E$Vibv$D1r5kN2t2dpf23pB1Np zo?+s^RRyK>!`J?`6$k}xPGC_IB&f6!3Fa^@b2r6=7pA+9Lf9`^3U9E9E(lUGVnG&W zMoSPDzKw-#|Sx^dw7NUtgY9ftw$-%Z{GY%`^gu*{kLW!K?a?Y1;UUicf9HT6UQ delta 61 zcmX@PNbb}exeXGG+=ixBM#fRc7}$H&H!Cw1GBU1cp32ldm5C9EK{PWEvj8zG5VLKc J%EZ348vtGX6D$A# diff --git a/state/map.yaml b/state/map.yaml index 93b222fb..4c4d025c 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -714,4 +714,4 @@ current_status: - Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass. - `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure. last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync. - next_action: Wire completion-gated semantic projection into canonical admission before adding its daemon. Swarm identity is now an explicit immutable typed document in the Mind store (`gamecult.epiphany.main` locally); fixed organ agent ids are no longer misused as swarm identity. Pure typed projection obligations, attempts, exact receipt causality, and derived pending/failed/stale/ready health law exist, but are not yet runtime authority. Next make RepoModel bootstrap/admission/frontier adoption and every reviewed Mind mutation atomically commit canonical generation plus one obligation; cut remaining raw Mind writers or demote them to migration-only. Then make query eligibility require the newest exact obligation/success receipt, add restart-safe claim/attempt execution, and only afterward publish provider-owned CultMesh sight and attach Idunn survival policy. Canonical BM25 remains partition-constrained and correct throughout. Refresh stale Modeling anchors after the completion nerve is causal. Do not let timers, Qdrant compatibility, point counts, Idunn liveness, CultMesh mirrors, Eve/Gjallar, fixed agent ids, or payload prose impersonate projection readiness or swarm identity. + next_action: Build restart-safe projector execution on the landed atomic admission nerve. Runtime-spine is immutably bound to canonical Mind swarm identity; RepoModel bootstrap/generic admission/frontier Adopt atomically emit Modeling obligations; reviewed Mind self-patch/lifecycle commits atomically emit fixed-role generation witness, persisted admission receipt, and Mind obligation; bootstrap-only writers refuse after generation and witness hashes are authenticated against current canonical rows. The live legacy runtime has its exact Modeling obligation. Next add an explicit legacy Mind generation-zero migration for current `state/agents.msgpack`, then claim/attempt/terminal receipt CAS, zero-document scoped deletion, and query gating that permits Qdrant only for the newest exact obligation/success receipt. Only after that publish derived provider-owned CultMesh health and attach Idunn survival. Canonical partitioned BM25 remains correct throughout. Do not let timers, Qdrant compatibility, point counts, runtime ids, paths, CLI labels, Idunn liveness, CultMesh mirrors, Eve/Gjallar, or payload prose impersonate swarm identity or readiness.