diff --git a/epiphany-core/src/agent_memory.rs b/epiphany-core/src/agent_memory.rs index d14f4b30..2b877483 100644 --- a/epiphany-core/src/agent_memory.rs +++ b/epiphany-core/src/agent_memory.rs @@ -367,23 +367,13 @@ pub fn review_agent_self_patch( }; match decode_agent_self_patch(patch_value) { - Ok(patch) => reasons.extend(review_agent_self_patch_contract(&target_agent_id, &patch)), + Ok(patch) => { + return review_agent_self_patch_document(role_id, &patch, store_path); + } Err(reason) => reasons.push(reason), } - AgentMemoryReview { - status: if reasons.is_empty() { - "accepted" - } else { - "rejected" - } - .to_string(), - target_agent_id, - target_role_id: role_id.to_string(), - target_store: store_path.display().to_string(), - reasons, - applied: None, - } + agent_memory_review(role_id, &target_agent_id, store_path, reasons, None) } pub(crate) fn decode_agent_self_patch( @@ -396,6 +386,30 @@ pub(crate) fn decode_agent_self_patch( .map_err(|err| format!("selfPatch is not a valid AgentSelfPatch document: {err}")) } +pub fn review_agent_self_patch_document( + role_id: &str, + patch: &AgentSelfPatch, + store_path: impl AsRef, +) -> AgentMemoryReview { + let store_path = store_path.as_ref(); + let target_agent_id = match agent_id_for_role(role_id) { + Ok(agent_id) => agent_id, + Err(reason) => { + return AgentMemoryReview { + status: "rejected".to_string(), + target_agent_id: String::new(), + target_role_id: role_id.to_string(), + target_store: store_path.display().to_string(), + reasons: vec![reason], + applied: None, + }; + } + }; + + let reasons = review_agent_self_patch_contract(target_agent_id, patch); + agent_memory_review(role_id, target_agent_id, store_path, reasons, None) +} + pub(crate) fn review_agent_self_patch_contract( expected_agent_id: &str, patch: &AgentSelfPatch, @@ -466,14 +480,24 @@ pub fn apply_agent_self_patch( role_id: &str, patch_value: &Value, store_path: impl AsRef, +) -> Result { + let patch = match decode_agent_self_patch(patch_value) { + Ok(patch) => patch, + Err(_) => return Ok(review_agent_self_patch(role_id, patch_value, store_path)), + }; + apply_agent_self_patch_document(role_id, patch, store_path) +} + +pub fn apply_agent_self_patch_document( + role_id: &str, + patch: AgentSelfPatch, + store_path: impl AsRef, ) -> Result { let store_path = store_path.as_ref(); - let mut review = review_agent_self_patch(role_id, patch_value, store_path); + let mut review = review_agent_self_patch_document(role_id, &patch, store_path); if review.status != "accepted" { return Ok(review); } - let patch: AgentSelfPatch = serde_json::from_value(patch_value.clone()) - .context("failed to decode accepted selfPatch")?; let mut cache = agent_memory_cache(store_path)?; cache.pull_all_backing_stores()?; let mut entry = cache @@ -510,6 +534,28 @@ pub fn apply_agent_self_patch( Ok(review) } +fn agent_memory_review( + role_id: &str, + target_agent_id: &str, + store_path: &Path, + reasons: Vec, + applied: Option, +) -> AgentMemoryReview { + AgentMemoryReview { + status: if reasons.is_empty() { + "accepted" + } else { + "rejected" + } + .to_string(), + target_agent_id: target_agent_id.to_string(), + target_role_id: role_id.to_string(), + target_store: store_path.display().to_string(), + reasons, + applied, + } +} + pub fn apply_agent_canonical_trait_seeds( seeds: &[AgentCanonicalTraitSeed], store_path: impl AsRef, @@ -1244,7 +1290,7 @@ mod tests { migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &store)?; assert!(validate_agent_memory_store(&store)?.is_empty()); - let patch = serde_json::json!({ + let patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({ "agentId": "epiphany.body", "reason": "The Body should remember accepted graph growth must stay source-grounded.", "semanticMemories": [{ @@ -1253,10 +1299,10 @@ mod tests { "salience": 0.74, "confidence": 0.86 }] - }); - let review = review_agent_self_patch("modeling", &patch, &store); + }))?; + let review = review_agent_self_patch_document("modeling", &patch, &store); assert_eq!(review.status, "accepted"); - let applied = apply_agent_self_patch("modeling", &patch, &store)?; + let applied = apply_agent_self_patch_document("modeling", patch, &store)?; assert_eq!(applied.status, "accepted"); assert_eq!(applied.applied, Some(true)); diff --git a/epiphany-core/src/bin/epiphany-agent-memory-store.rs b/epiphany-core/src/bin/epiphany-agent-memory-store.rs index 418a4836..58b75268 100644 --- a/epiphany-core/src/bin/epiphany-agent-memory-store.rs +++ b/epiphany-core/src/bin/epiphany-agent-memory-store.rs @@ -1,10 +1,11 @@ use anyhow::Context; use anyhow::Result; +use epiphany_core::AgentSelfPatch; use epiphany_core::agent_memory_status; -use epiphany_core::apply_agent_self_patch; +use epiphany_core::apply_agent_self_patch_document; use epiphany_core::migrate_agent_memory_json_dir_to_cultcache; use epiphany_core::project_agent_memory_to_json_dir; -use epiphany_core::review_agent_self_patch; +use epiphany_core::review_agent_self_patch_document; use epiphany_core::validate_agent_memory_store; use std::env; use std::fs; @@ -51,13 +52,13 @@ fn main() -> Result<()> { let store = require_path_arg(&mut args, "--store")?; let role_id = require_string_arg(&mut args, "--role-id")?; let patch = read_patch_arg(&require_string_arg(&mut args, "--patch")?)?; - print_json(&review_agent_self_patch(&role_id, &patch, store))?; + print_json(&review_agent_self_patch_document(&role_id, &patch, store))?; } "apply-patch" => { let store = require_path_arg(&mut args, "--store")?; let role_id = require_string_arg(&mut args, "--role-id")?; let patch = read_patch_arg(&require_string_arg(&mut args, "--patch")?)?; - let result = apply_agent_self_patch(&role_id, &patch, store)?; + let result = apply_agent_self_patch_document(&role_id, patch, store)?; let accepted = result.status == "accepted"; print_json(&result)?; if !accepted { @@ -111,15 +112,15 @@ fn optional_path_arg( Ok(Some(PathBuf::from(&values[1]))) } -fn read_patch_arg(value: &str) -> Result { +fn read_patch_arg(value: &str) -> Result { let path = PathBuf::from(value); if path.exists() { let raw = fs::read_to_string(&path) .with_context(|| format!("failed to read patch {}", path.display()))?; return serde_json::from_str(&raw) - .with_context(|| format!("failed to decode patch {}", path.display())); + .with_context(|| format!("failed to decode typed selfPatch {}", path.display())); } - serde_json::from_str(value).context("failed to decode patch JSON") + serde_json::from_str(value).context("failed to decode typed selfPatch JSON") } fn print_json(value: &T) -> Result<()> { @@ -142,7 +143,7 @@ fn run_smoke(store: &Path) -> Result { "errors": errors, })); } - let accepted_patch = serde_json::json!({ + let accepted_patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({ "agentId": "epiphany.body", "reason": "The Body should remember accepted graph growth must be source-grounded and bounded.", "semanticMemories": [{ @@ -151,10 +152,10 @@ fn run_smoke(store: &Path) -> Result { "salience": 0.74, "confidence": 0.86, }], - }); - let accepted = review_agent_self_patch("modeling", &accepted_patch, store); - let wrong_role = review_agent_self_patch("verification", &accepted_patch, store); - let forbidden_patch = serde_json::json!({ + }))?; + let accepted = review_agent_self_patch_document("modeling", &accepted_patch, store); + let wrong_role = review_agent_self_patch_document("verification", &accepted_patch, store); + let forbidden_patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({ "agentId": "epiphany.body", "reason": "This tries to put project state in lane memory, which should be refused.", "graphs": {}, @@ -164,8 +165,8 @@ fn run_smoke(store: &Path) -> Result { "salience": 0.5, "confidence": 0.5, }], - }); - let forbidden = review_agent_self_patch("modeling", &forbidden_patch, store); + }))?; + let forbidden = review_agent_self_patch_document("modeling", &forbidden_patch, store); let temp_dir = scoped_temp_dir("epiphany-agent-memory-smoke")?; let temp_store = temp_dir.join("agents.msgpack"); @@ -176,7 +177,7 @@ fn run_smoke(store: &Path) -> Result { temp_store.display() ) })?; - let applied = apply_agent_self_patch("modeling", &accepted_patch, &temp_store)?; + let applied = apply_agent_self_patch_document("modeling", accepted_patch, &temp_store)?; let temp_validation_errors = validate_agent_memory_store(&temp_store)?; let _ = fs::remove_dir_all(&temp_dir); diff --git a/epiphany-core/src/bin/epiphany-heartbeat-store.rs b/epiphany-core/src/bin/epiphany-heartbeat-store.rs index 7a89487c..34ddf15b 100644 --- a/epiphany-core/src/bin/epiphany-heartbeat-store.rs +++ b/epiphany-core/src/bin/epiphany-heartbeat-store.rs @@ -6,7 +6,7 @@ use epiphany_core::HeartbeatCompleteOptions; use epiphany_core::HeartbeatPumpOptions; use epiphany_core::HeartbeatTickOptions; use epiphany_core::VoidRoutineOptions; -use epiphany_core::apply_agent_self_patch; +use epiphany_core::apply_agent_self_patch_document; use epiphany_core::complete_heartbeat_store; use epiphany_core::heartbeat_status_projection; use epiphany_core::initialize_ghostlight_scene_heartbeat_store; @@ -398,8 +398,9 @@ fn run_smoke(agent_store: &Path) -> Result { .as_str() .ok_or_else(|| anyhow!("idle rumination has no roleId"))? .to_string(); - let patch = serde_json::Value::Object(patch.clone()); - let applied = apply_agent_self_patch(&role_id, &patch, &temp_agent_store)?; + let patch = serde_json::from_value(serde_json::Value::Object(patch.clone())) + .context("idle rumination selfPatch is not a typed AgentSelfPatch")?; + let applied = apply_agent_self_patch_document(&role_id, patch, &temp_agent_store)?; idle["rumination"]["result"] = serde_json::to_value(applied)?; idle["rumination"]["applied"] = serde_json::Value::Bool(true); write_rumination_artifact(&artifact_dir, "smoke-idle", &idle["rumination"])?; @@ -587,8 +588,9 @@ fn apply_rumination_patch( .as_str() .ok_or_else(|| anyhow!("rumination has no roleId"))? .to_string(); - let patch = serde_json::Value::Object(patch.clone()); - let applied = apply_agent_self_patch(&role_id, &patch, agent_store)?; + let patch = serde_json::from_value(serde_json::Value::Object(patch.clone())) + .context("rumination selfPatch is not a typed AgentSelfPatch")?; + let applied = apply_agent_self_patch_document(&role_id, patch, agent_store)?; result["rumination"]["result"] = serde_json::to_value(applied)?; result["rumination"]["applied"] = serde_json::Value::Bool(true); write_rumination_artifact(artifact_dir, schedule_id, &result["rumination"])?; diff --git a/epiphany-core/src/bin/epiphany-repo-personality.rs b/epiphany-core/src/bin/epiphany-repo-personality.rs index b3dc0260..7b08fd80 100644 --- a/epiphany-core/src/bin/epiphany-repo-personality.rs +++ b/epiphany-core/src/bin/epiphany-repo-personality.rs @@ -6,11 +6,12 @@ use cultcache_rs::CultCache; use cultcache_rs::DatabaseEntry; use cultcache_rs::SingleFileMessagePackBackingStore; use epiphany_core::AgentCanonicalTraitSeed; +use epiphany_core::AgentSelfPatch; use epiphany_core::apply_agent_canonical_trait_seeds; -use epiphany_core::apply_agent_self_patch; +use epiphany_core::apply_agent_self_patch_document; use epiphany_core::default_heartbeat_state; use epiphany_core::load_heartbeat_state_entry; -use epiphany_core::review_agent_self_patch; +use epiphany_core::review_agent_self_patch_document; use epiphany_core::write_heartbeat_state_entry; use ignore::WalkBuilder; use serde::Deserialize; @@ -1414,9 +1415,9 @@ fn process_initialization_result_patches( let mut applied = 0usize; for patch in patches { let review = if apply_self_patches { - apply_agent_self_patch(&patch.role_id, &patch.self_patch, store)? + apply_agent_self_patch_document(&patch.role_id, patch.self_patch.clone(), store)? } else { - review_agent_self_patch(&patch.role_id, &patch.self_patch, store) + review_agent_self_patch_document(&patch.role_id, &patch.self_patch, store) }; if review.status == "accepted" { accepted += 1; @@ -2114,7 +2115,7 @@ fn number_from_map(values: &BTreeMap, key: &str) -> f64 { struct InitializationSelfPatch { role_id: String, source: String, - self_patch: Value, + self_patch: AgentSelfPatch, } fn extract_initialization_self_patches( @@ -2135,13 +2136,13 @@ fn extract_trajectory_self_patches(result: &Value) -> Result Result Result Result Result Result Result Result<&'static str, Str pub fn review_role_self_patch( role_id: EpiphanyRoleResultRoleId, patch: &serde_json::Value, +) -> EpiphanyRoleSelfPersistenceReview { + match decode_agent_self_patch(patch) { + Ok(patch) => review_role_self_patch_document(role_id, &patch), + Err(reason) => { + let (expected_agent_id, target_path) = role_self_memory_target(role_id); + EpiphanyRoleSelfPersistenceReview { + status: EpiphanyRoleSelfPersistenceStatus::Rejected, + target_agent_id: Some(expected_agent_id.to_string()), + target_path: Some(target_path.to_string()), + reasons: vec![reason], + } + } + } +} + +pub fn review_role_self_patch_document( + role_id: EpiphanyRoleResultRoleId, + patch: &AgentSelfPatch, ) -> EpiphanyRoleSelfPersistenceReview { let (expected_agent_id, target_path) = role_self_memory_target(role_id); - let reasons = match decode_agent_self_patch(patch) { - Ok(patch) => review_agent_self_patch_contract(expected_agent_id, &patch), - Err(reason) => vec![reason], - }; + let reasons = review_agent_self_patch_contract(expected_agent_id, patch); EpiphanyRoleSelfPersistenceReview { status: if reasons.is_empty() { diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 39297911..bd64b0f1 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -37,6 +37,7 @@ remembers doctrine; the branch remembers the blade. - `notes/codex-auth-spine-inventory.md` is the source-grounded keeper list for that reliquary: retain `codex-login` credential storage/refresh, `codex-model-provider` auth resolution, the model-transport subset of `codex-core::client`, and the narrow model-catalog subset of `models-manager`; do not treat Codex app-server, apps, skills, marketplace, plugin UX, MCP OAuth handlers, or collaboration-mode surfaces as auth-spine machinery. - Edge JSON is allowed for schema description, hostile ingress before immediate typed parsing, sealed forensic artifacts, or named quarantine experiments. When both subsystems are ours, runtime data must remain typed CultCache documents and move over CultNet typed contracts. Generic `serde_json::Value` in worker launch/result/selfPatch/runtime flow is contamination until classified and replaced. - `epiphany-openai-runtime` worker result ingress now parses assistant output directly into typed role/reorient ingress structs selected from the durable launch document, stores nested `statePatch` and `selfPatch` as typed MessagePack in `EpiphanyRuntimeRoleWorkerResult`, and no longer passes worker result data through generic `serde_json::Value`. JSON survives there only as the OpenAI text-output edge before typed deserialization. +- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. JSON-value selfPatch functions are compatibility wrappers for CLI/artifact ingress; native callers should parse once at the edge and then call `review_agent_self_patch_document` / `apply_agent_self_patch_document`. - Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency. - The May 2026 foundation migration in `notes/epiphany-ideal-architecture-rebuild-plan.md` is complete enough to defend. Read `notes/epiphany-architectural-teardown.md` before touching Epiphany control-plane code, but do not treat it as an open-ended excuse to keep shaving the altar. The rebuild moved scene projection, freshness derivation, pressure policy, targeted context shards, bounded graph traversal, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, pure CRRC recommendation policy, fixed-lane coordinator decision policy, role-board projection policy, role/reorient result interpretation, role self-persistence review policy, and role/reorient acceptance bundle policy into `epiphany-core`; typed acceptance receipts replaced summary-string identity for live accept paths, runtime links are dual-written on launch, and result read-back now prefers runtime links. - Read `notes/epiphany-ideal-architecture-rebuild-plan.md` immediately after the teardown. It defines the smallest coherent replacement architecture: durable `EpiphanyState`, runtime-spine-owned `RuntimeState`, separate role memory/heartbeat state, `epiphany-core` as policy owner, app-server as adapter, Aquarium as reflector, typed acceptance receipts, and derived view lenses instead of a protocol verb zoo. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 13470710..baf209e1 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index b55eace9..141ca558 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -96,6 +96,7 @@ accepted_design: - `notes/codex-starvation-and-cultnet-liberation-plan.md` is the active foundation directive. It supersedes outward product/bridge/dogfood work and supersedes the weaker goal of merely making Codex app-server a cleaner adapter. Epiphany must become a native CultCache/CultNet runtime; Codex remains only as an OpenAI subscription auth/model-transport reliquary until that too can be replaced or minimized. - `notes/codex-auth-spine-inventory.md` is the current source-grounded inventory of the Codex organ that may survive: `codex-login` credential storage/refresh, `codex-model-provider` auth resolution, the model-transport subset of `codex-core::client`, and the narrow model-catalog subset of `models-manager`. Codex app-server, apps, skills, marketplace, plugin UX, MCP OAuth handlers, and collaboration-mode surfaces are not part of the keeper spine. - `epiphany-openai-runtime` worker result ingress now parses assistant output directly into launch-kind-specific typed documents and stores typed nested `statePatch` / `selfPatch` payloads as MessagePack inside `EpiphanyRuntimeRoleWorkerResult`; generic `serde_json::Value` no longer mediates role/reorient worker results in that crate. JSON still exists at the OpenAI text edge because the model's output is text, but the runtime boundary after parsing is typed. + - Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. The old JSON-value selfPatch functions remain only as edge compatibility wrappers for CLI/artifact ingress; `epiphany-agent-memory-store`, heartbeat rumination application, and repo initialization selfPatch processing parse once at the edge and then operate on typed documents. - `notes/epiphany-architectural-teardown.md` is the current foundation-cleanup directive. Its suspicion pass supersedes outward Phase 6 expansion as the active priority until the control-plane ownership problems are resolved. - `notes/epiphany-ideal-architecture-rebuild-plan.md` is the replacement blueprint: the smallest coherent architecture is durable `EpiphanyState`, runtime-spine-owned `RuntimeState`, separate role memory/heartbeat state, `epiphany-core` as policy owner, app-server as adapter, Aquarium as reflector, typed result acceptance receipts, and derived view lenses instead of a protocol verb zoo. - Epiphany is a fork-level modeling architecture, not merely a Codex collaboration mode.