From 86d702002acee8321438f32fb9cf69a29a667d70 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 19 Jul 2026 00:45:18 +0100 Subject: [PATCH] Keep resident brakes on consequence surfaces --- .../admitted_model_direction_consideration.rs | 38 ++++- .../src/bin/epiphany-daemon-supervisor.rs | 43 +++++- .../src/heartbeat_state/heartbeat_store.rs | 144 ++++++++++++++++-- notes/fresh-workspace-handoff.md | 27 +++- state/map.yaml | 4 +- vendor/cultcache-rs | 2 +- 6 files changed, 232 insertions(+), 26 deletions(-) diff --git a/epiphany-core/src/admitted_model_direction_consideration.rs b/epiphany-core/src/admitted_model_direction_consideration.rs index 402c47b2..47c6dd71 100644 --- a/epiphany-core/src/admitted_model_direction_consideration.rs +++ b/epiphany-core/src/admitted_model_direction_consideration.rs @@ -101,9 +101,10 @@ pub fn commit_request( let identity = cache .get::(crate::RUNTIME_IDENTITY_KEY)? .ok_or_else(|| anyhow!("model direction consideration requires runtime identity"))?; - let thread = cache - .get::(crate::THREAD_STATE_KEY)? - .ok_or_else(|| anyhow!("model direction consideration requires thread state"))?; + let Some(thread) = cache.get::(crate::THREAD_STATE_KEY)? + else { + return Ok(None); + }; let model = crate::runtime_current_repo_model(runtime_store)? .ok_or_else(|| anyhow!("model direction consideration requires admitted Modeling map"))?; let model_hash = crate::memory_graph_model_hash(&model)?; @@ -251,6 +252,7 @@ pub fn render_prompt(request: &AdmittedModelDirectionConsiderationRequest) -> St #[cfg(test)] mod tests { use super::*; + use std::collections::BTreeMap; fn request() -> AdmittedModelDirectionConsiderationRequest { AdmittedModelDirectionConsiderationRequest { @@ -322,4 +324,34 @@ mod tests { }); assert!(validate_result(&request, &proposed).is_ok()); } + + #[test] + fn cold_runtime_without_thread_has_no_direction_request_yet() -> Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("runtime.ccmp"); + let mut cache = crate::runtime_spine_cache(&store)?; + cache.put( + crate::RUNTIME_IDENTITY_KEY, + &crate::EpiphanyRuntimeIdentity { + schema_version: crate::RUNTIME_SPINE_SCHEMA_VERSION.into(), + runtime_id: "runtime-cold".into(), + display_name: "Cold runtime".into(), + runtime_kind: "resident".into(), + created_at: "2026-07-18T00:00:00Z".into(), + updated_at: "2026-07-18T00:00:00Z".into(), + supported_document_types: Vec::new(), + metadata: BTreeMap::new(), + }, + )?; + + assert!(commit_request(&store, "2026-07-18T00:01:00Z")?.is_none()); + let mut reloaded = crate::runtime_spine_cache(&store)?; + reloaded.pull_all_backing_stores()?; + assert!( + reloaded + .get_all::()? + .is_empty() + ); + Ok(()) + } } diff --git a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs index 00aa13a1..e7fec5ed 100644 --- a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs +++ b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs @@ -4020,10 +4020,7 @@ fn assert_swarm_brake_allows_service_lifecycle_entry( let scope_matches = matches!(brake.scope.as_str(), "swarm" | "all"); let surface_matches = brake.protected_surfaces.is_empty() || brake.protected_surfaces.iter().any(|surface| { - surface == "heartbeat.scheduler" - || surface == "daemon.lifecycle_poke" - || surface == "daemon.*" - || surface == "*" + surface == "daemon.lifecycle_poke" || surface == "daemon.*" || surface == "*" }); if scope_matches && surface_matches { anyhow::bail!( @@ -4099,6 +4096,44 @@ mod provider_status_ownership_tests { } } +#[cfg(test)] +mod service_lifecycle_brake_authority_tests { + use super::*; + + fn brake(protected_surfaces: &[&str]) -> EpiphanyCultMeshSwarmBrakeEntry { + EpiphanyCultMeshSwarmBrakeEntry { + schema_version: "epiphany.cultmesh.swarm_brake.v0".into(), + brake_id: "brake-test".into(), + status: "engaged".into(), + scope: "all".into(), + reason: "sleep".into(), + operator_agent_id: "operator".into(), + affected_clusters: vec!["local".into()], + protected_surfaces: protected_surfaces.iter().map(|v| (*v).into()).collect(), + created_at_utc: "2026-07-18T00:00:00Z".into(), + expires_at_utc: None, + private_state_exposed: false, + notes: Vec::new(), + runtime_id: "runtime".into(), + } + } + + #[test] + fn cognitive_scheduler_brake_does_not_claim_service_physiology() { + let cognitive = brake(&[ + "heartbeat.scheduler", + "coordinator.run", + "persona.public_speech", + "daemon.tool_invocation", + ]); + assert!(assert_swarm_brake_allows_service_lifecycle_entry(Some(&cognitive)).is_ok()); + for surface in ["daemon.lifecycle_poke", "daemon.*", "*"] { + let lifecycle = brake(&[surface]); + assert!(assert_swarm_brake_allows_service_lifecycle_entry(Some(&lifecycle)).is_err()); + } + } +} + #[cfg(test)] mod semantic_projector_authority_tests { use super::*; diff --git a/epiphany-core/src/heartbeat_state/heartbeat_store.rs b/epiphany-core/src/heartbeat_state/heartbeat_store.rs index e9f81ae4..5e2cd53a 100644 --- a/epiphany-core/src/heartbeat_state/heartbeat_store.rs +++ b/epiphany-core/src/heartbeat_state/heartbeat_store.rs @@ -18,8 +18,9 @@ use super::participant_kind; use anyhow::Result; use anyhow::anyhow; use cultcache_rs::SingleFileMessagePackBackingStore; -use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry}; +use cultcache_rs::{CacheBackingStore, CultCache, CultCacheEnvelope, DatabaseEntry}; use serde_json::Value; +use std::collections::HashSet; use std::path::Path; pub fn heartbeat_state_cache(store_path: impl AsRef) -> Result { @@ -27,8 +28,7 @@ pub fn heartbeat_state_cache(store_path: impl AsRef) -> Result cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref())); - cache.pull_all_backing_stores()?; + import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref(), false)?; Ok(cache) } @@ -37,11 +37,77 @@ fn legacy_heartbeat_state_cache(store_path: impl AsRef) -> Result()?; cache.register_entry_type::()?; cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref())); - cache.pull_all_backing_stores()?; + import_owned_heartbeat_envelopes(&mut cache, store_path.as_ref(), true)?; Ok(cache) } +fn import_owned_heartbeat_envelopes( + cache: &mut CultCache, + store_path: &Path, + legacy: bool, +) -> Result<()> { + let state_type = if legacy { + ::TYPE + } else { + ::TYPE + }; + let mut identities = HashSet::new(); + for envelope in SingleFileMessagePackBackingStore::new(store_path).pull_all()? { + let owned = (envelope.r#type == state_type && envelope.key == HEARTBEAT_STATE_KEY) + || (envelope.r#type == ::TYPE + && envelope.key == HEARTBEAT_COGNITION_KEY) + || envelope.r#type == ::TYPE; + if !owned { + continue; + } + if !identities.insert((envelope.r#type.clone(), envelope.key.clone())) { + return Err(anyhow!( + "heartbeat store contains duplicate owner entry type {:?} key {:?}", + envelope.r#type, + envelope.key + )); + } + if envelope.r#type == state_type { + if legacy { + // The legacy and current state documents intentionally share a + // polymorphic identity. A current payload is not legacy cargo. + let _ = cache.load_envelope::(envelope); + } else { + cache.load_envelope::(envelope)?; + } + } else if envelope.r#type == ::TYPE { + cache.load_envelope::(envelope)?; + } else { + cache.load_envelope::(envelope)?; + } + } + Ok(()) +} + +fn commit_owned_entry( + store_path: &Path, + cache: &CultCache, + key: &str, + value: &T, +) -> Result { + let expected = cache + .snapshot_envelopes() + .into_iter() + .find(|entry| entry.r#type == T::TYPE && entry.key == key); + let (replacement, written) = cache.prepare_entry(key, value)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + let committed = match expected { + Some(expected) => backing.compare_and_swap_entry(&expected, replacement)?, + None => backing.insert_entry_if_absent(replacement)?, + }; + if !committed { + return Err(anyhow!( + "heartbeat owner entry lost exact atomic compare-and-swap" + )); + } + Ok(written) +} + pub fn load_heartbeat_state_entry( store_path: impl AsRef, ) -> Result> { @@ -54,8 +120,9 @@ pub fn write_heartbeat_state_entry( state: &EpiphanyHeartbeatStateEntry, ) -> Result { validate_heartbeat_state(state)?; - let mut cache = heartbeat_state_cache(store_path)?; - cache.put(HEARTBEAT_STATE_KEY, state) + let store_path = store_path.as_ref(); + let cache = heartbeat_state_cache(store_path)?; + commit_owned_entry(store_path, &cache, HEARTBEAT_STATE_KEY, state) } pub fn load_heartbeat_state_transaction( @@ -119,8 +186,9 @@ pub fn write_heartbeat_cognition_entry( cognition: &EpiphanyHeartbeatCognitionEntry, ) -> Result { validate_heartbeat_cognition(cognition)?; - let mut cache = heartbeat_state_cache(store_path)?; - cache.put(HEARTBEAT_COGNITION_KEY, cognition) + let store_path = store_path.as_ref(); + let cache = heartbeat_state_cache(store_path)?; + commit_owned_entry(store_path, &cache, HEARTBEAT_COGNITION_KEY, cognition) } pub fn write_heartbeat_stale_turn_repair_receipt( @@ -128,9 +196,27 @@ pub fn write_heartbeat_stale_turn_repair_receipt( receipt: &EpiphanyHeartbeatStaleTurnRepairReceipt, ) -> Result { validate_heartbeat_stale_turn_repair_receipt(receipt)?; - let mut cache = heartbeat_state_cache(store_path)?; - let written = cache.put(receipt.receipt_id.clone(), receipt)?; - cache.put(HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY, &written)?; + let store_path = store_path.as_ref(); + let cache = heartbeat_state_cache(store_path)?; + let written = receipt.clone(); + let (receipt_entry, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + let (latest_entry, _) = cache.prepare_entry(HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY, receipt)?; + let expected = cache + .snapshot_envelopes() + .into_iter() + .filter(|entry| { + entry.r#type == ::TYPE + && (entry.key == receipt.receipt_id + || entry.key == HEARTBEAT_STALE_TURN_REPAIR_LATEST_KEY) + }) + .collect::>(); + if !SingleFileMessagePackBackingStore::new(store_path) + .compare_and_swap_batch(&expected, vec![receipt_entry, latest_entry])? + { + return Err(anyhow!( + "heartbeat stale-turn receipt lost exact atomic compare-and-swap" + )); + } Ok(written) } @@ -333,6 +419,13 @@ mod tests { use crate::heartbeat_state::default_heartbeat_state; use pretty_assertions::assert_eq; + #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] + #[cultcache(type = "test.foreign.readiness", schema = "ForeignReadiness")] + struct ForeignReadiness { + #[cultcache(key = 0)] + status: String, + } + #[test] fn round_trips_state_and_cognition_documents() -> Result<()> { let temp = tempfile::tempdir()?; @@ -373,4 +466,31 @@ mod tests { assert_eq!(loaded_cognition.latest_run_id.as_deref(), Some("run-1")); Ok(()) } + + #[test] + fn heartbeat_owner_reads_and_writes_preserve_foreign_shared_store_rows() -> Result<()> { + let temp = tempfile::tempdir()?; + let store_path = temp.path().join("heartbeats.msgpack"); + let mut foreign_cache = CultCache::new(); + foreign_cache.register_entry_type::()?; + let (foreign, _) = foreign_cache.prepare_entry( + "provider-readiness", + &ForeignReadiness { + status: "ready".into(), + }, + )?; + let mut backing = SingleFileMessagePackBackingStore::new(&store_path); + backing.push(&foreign)?; + + assert!(load_heartbeat_state_entry(&store_path)?.is_none()); + write_heartbeat_state_entry(&store_path, &default_heartbeat_state(1.0))?; + + let rows = backing.pull_all()?; + assert!(rows.contains(&foreign)); + assert!(rows.iter().any(|row| { + row.r#type == ::TYPE + && row.key == HEARTBEAT_STATE_KEY + })); + Ok(()) + } } diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 2c3a39ad..9e5ff150 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -2736,12 +2736,29 @@ permits daemon supervision, leases, health, liveness, and read-only sight while blocking cognition launches, cognitive-work scheduling, Hands consequences, publication, and deployment actuation. -The d4 deployment authority is revoked. It produced no final witness and must -not be described as deployed. Any successor begins braked and earns deployment -truth only through exact signed aggregate health and Idunn's final witness. +Exact candidate `9165862cce09739a62b20c0c7728ccceb71cfc2f` crossed Bifrost and +built immutable release +`sha256-39eed3817a10325b39b7f223fb2919c5959c88443ef82d9cc1497f6af631f365` +with witness +`sha256-9ad9cfe17fcabffb84fb392cbd5ecc1e8823482a338b38ced52e1020532e80c6`. +It was not deployed. Startup exposed three faults, so all candidate services +and the Idunn transaction were terminated through rollback. There is no +deployment witness or active-release pointer. The immutable package and stable +compiler cache remain reusable evidence only. -Next action: select and package a successor, cross Bifrost and Idunn while -braked, and verify the typed brake, exact +The successor source cuts those faults at their owners. Heartbeat reads only +its typed identities from the mixed heartbeat/readiness store and writes exact +owner entries with CAS, preserving foreign readiness. CultCache now provides a +typed in-memory envelope load primitive for this shared-store view. Cold Self +without thread state emits no direction request yet instead of failing. A +`heartbeat.scheduler` brake blocks cognitive scheduling but does not block +daemon lifecycle physiology; explicit daemon lifecycle surfaces still do. +Focused proofs pass and the full core library reports 570 passed, zero failed, +one ignored. + +Next action: commit and push the successor and CultCache dependency, revoke the +failed exact candidate authority, then cross the successor through Bifrost and +Idunn while braked. Verify the typed brake, exact release/witness bindings, all three service owners, signed Idunn health, local Qdrant/Ollama routes, and final deployment witness. Then run the Starfire-offline rehearsal before 2026-07-22: organization feedback -> persistent Modeling map -> diff --git a/state/map.yaml b/state/map.yaml index d874dcaa..116b04fe 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -783,4 +783,6 @@ current_status: - Release `sha256-c94109f94ee42c1257089830f399cb87cd5ad672772c274194371995ce4df923` is the first deployed immutable sibling generation. Native packaging builds from detached exact source commit `dd80f4a51c18425a6665710698a357be61154abf`, initializes pinned submodules, uses the tracked lockfile under `--locked`, and publishes four exact binaries plus a typed CultCache witness/head only after atomic directory publication and byte verification. Task Scheduler pins release id/digest and the witnessed supervisor path; Idunn reauthenticates the complete set and reserved policy role paths. Current-boot proof: reconciler PID 21228 launched direct child 22736; witnessed Mind/Modeling queries ranked semantically; killing that exact-path child caused Idunn to launch PID 19748 with receipt `86a42d51-9930-4b5d-9e61-e74c82a0d808`, ready heartbeat `4fc91977-b635-474b-812b-c364847e2b4b`, and witnessed executable digest `sha256-1060dd64e8c2b520ec8744b67e8306db04a6618d463fc2117c352adf9e30e6b0`. This closes current-boot artifact generation and recurrence, not reboot/logon survival. - Release `sha256-6f9ab4848207a110e7e3836f41e6a74c0f463369ad4b04ce2d0d3831a6b262ae` packages repository-readiness source commit `360dcb433dda9599fca14282592608d26b8b6cc8`; witnessed Idunn PID 29476 directly owns witnessed projector PID 22840 with launch receipt `50879209-b65f-4411-ae3f-6f647d37bf42` and ready heartbeat `134df620-9e20-4220-ad4e-117ea4ca8188`. Deployment exposed receipt-v2 migration pressure: v1 receipts became correctly ineligible, but the old Succeeded pulse shortcut kept both partitions idle. The shortcut is now cut in source. Valid v2 terminal evidence derives Ready; an exact same-obligation succeeded claim opens Repair only after its consumed grant/recovery authority and exact succeeded attempt are authenticated. Failed repair retries through execute at the next epoch. Forged predecessor authority cannot be laundered by a repair grant. - Yggdrasil exact-SHA pressure release `586147a751c6eec2d59cd8dc10bd17ce0d02a4d1` proved useful GPU work but failed deployment honestly. GTX 1080 stayed near saturation at 2,419 MiB; bounded four-text requests completed continuously. Mind published 215 vectors, while workspace coverage completed 3,389 Ollama HTTP calls total (about 13.3k workspace chunks after Mind) before Idunn's 50-minute candidate-health deadline at 2026-07-17T01:23:50+02:00. Idunn then stopped the service, published no deployment witness, and reported no exact-candidate daemon-health proof; the Modeling collection remained at the inherited 22 points because the monolithic executor had not yet published its accumulated vectors. This is failure evidence for the execution/deployment contract, not for GPU acceleration. Total wall-time extension is forbidden as the repair. - next_action: "Commit and push the corrected successor, authorize that exact commit through Bifrost, and let Idunn deploy it on Yggdrasil under the engaged sleep brake. Require immutable package inspection, app-owned release publication, three healthy local service owners, signed aggregate health, and the final deployment.env witness. Then run the Starfire-offline rehearsal before 2026-07-22: persistent Modeling map -> autonomous bounded Imagination proposal, with classified organization feedback through Persona as an additional pressure path; explicit Mind adoption -> Hands consequence -> Bifrost release -> Idunn deployment remain separate gates. Prove replay/tamper refusal and that conversation alone grants no Mind, Hands, Persona-speech, release, or deployment authority. No host reboot is authorized." + - Exact Epiphany candidate `9165862cce09739a62b20c0c7728ccceb71cfc2f` crossed Bifrost and built immutable release `sha256-39eed3817a10325b39b7f223fb2919c5959c88443ef82d9cc1497f6af631f365` with witness `sha256-9ad9cfe17fcabffb84fb392cbd5ecc1e8823482a338b38ced52e1020532e80c6`, fourteen root-owned 0555 binaries, and app-owned release publication. It was not deployed: live startup exposed three authority faults, the candidate services and Idunn transaction were terminated through rollback, and neither `deployment.env` nor `active-release.env` exists. The immutable package and stable compiler cache remain reusable evidence, not active authority. + - The candidate faults are cut in source. Heartbeat now forms an owner-filtered typed view over its mixed CultCache store and commits exact owner rows with CAS while preserving readiness rows; CultCache supplies typed `load_envelope` for validated in-memory admission without backing-store publication. A cold resident runtime without thread state yields no direction request yet instead of a fatal error. An engaged `heartbeat.scheduler` brake blocks cognitive scheduling but no longer claims daemon service lifecycle physiology; only `daemon.lifecycle_poke`, `daemon.*`, wildcard, or an empty protected list blocks that owner. Focused shared-store/cold-Self/brake tests pass, and the full core library proves 570 passed, zero failed, one ignored. + next_action: "Commit and push the corrected successor and CultCache dependency, revoke the failed 9165862 authority, authorize the successor exact commit through Bifrost, and let Idunn deploy it on Yggdrasil under the engaged sleep brake. Require immutable package inspection, app-owned release publication, three healthy local service owners, signed aggregate health, and the final deployment.env witness. Then run the Starfire-offline rehearsal before 2026-07-22: persistent Modeling map -> autonomous bounded Imagination proposal, with classified organization feedback through Persona as an additional pressure path; explicit Mind adoption -> Hands consequence -> Bifrost release -> Idunn deployment remain separate gates. Prove replay/tamper refusal and that conversation alone grants no Mind, Hands, Persona-speech, release, or deployment authority. No host reboot is authorized." diff --git a/vendor/cultcache-rs b/vendor/cultcache-rs index 2c5042a1..d6a1d72f 160000 --- a/vendor/cultcache-rs +++ b/vendor/cultcache-rs @@ -1 +1 @@ -Subproject commit 2c5042a16fbe6cf14dfec079effebb5e480b7c9a +Subproject commit d6a1d72faf4d8b2a39a333c81c2c63527e59d893