From f3ae9f4cb58e04d82fe7f08cae77dbd8fabdda01 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 13 May 2026 06:42:57 +0100 Subject: [PATCH] Move Epiphany update routes into bridge service --- epiphany-codex-bridge/src/lib.rs | 1 + epiphany-codex-bridge/src/mutation_service.rs | 84 ++++++++++++++++ notes/fresh-workspace-handoff.md | 10 ++ state/ledgers.msgpack | Bin 83870 -> 84291 bytes state/map.yaml | 4 +- .../epiphany_mutation_routes.rs | 92 ++++++++---------- 6 files changed, 138 insertions(+), 53 deletions(-) create mode 100644 epiphany-codex-bridge/src/mutation_service.rs diff --git a/epiphany-codex-bridge/src/lib.rs b/epiphany-codex-bridge/src/lib.rs index 100e22e5..eb1983df 100644 --- a/epiphany-codex-bridge/src/lib.rs +++ b/epiphany-codex-bridge/src/lib.rs @@ -3,6 +3,7 @@ pub mod coordinator; pub mod jobs; pub mod launch; pub mod mutation; +pub mod mutation_service; pub mod pressure; pub mod reorient; pub mod results; diff --git a/epiphany-codex-bridge/src/mutation_service.rs b/epiphany-codex-bridge/src/mutation_service.rs new file mode 100644 index 00000000..4e6d7087 --- /dev/null +++ b/epiphany-codex-bridge/src/mutation_service.rs @@ -0,0 +1,84 @@ +use codex_app_server_protocol::ThreadEpiphanyStateUpdatedField; +use codex_app_server_protocol::ThreadEpiphanyUpdatePatch; +use codex_core::CodexThread; +use codex_core::EpiphanyPromotionInput; +use codex_core::evaluate_promotion; +use codex_protocol::error::CodexErr; +use codex_protocol::protocol::EpiphanyEvidenceRecord; +use codex_protocol::protocol::EpiphanyThreadState; + +use crate::mutation::epiphany_promote_changed_fields; +use crate::mutation::epiphany_update_patch_changed_fields; +use crate::mutation::state_update_from_thread_patch; +use crate::mutation::thread_epiphany_patch_has_state_replacements; +use crate::state::client_visible_live_thread_epiphany_state; + +#[derive(Debug, Clone)] +pub struct EpiphanyThreadUpdateApplied { + pub revision: u64, + pub changed_fields: Vec, + pub epiphany_state: EpiphanyThreadState, +} + +#[derive(Debug, Clone)] +pub enum EpiphanyThreadPromoteApplied { + Rejected { reasons: Vec }, + Accepted(EpiphanyThreadUpdateApplied), +} + +pub async fn apply_thread_epiphany_update( + thread: &CodexThread, + expected_revision: Option, + patch: ThreadEpiphanyUpdatePatch, +) -> Result { + let changed_fields = epiphany_update_patch_changed_fields(&patch); + let update = state_update_from_thread_patch(expected_revision, patch); + let epiphany_state = thread.epiphany_update_state(update).await?; + let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await; + Ok(EpiphanyThreadUpdateApplied { + revision: epiphany_state.revision, + changed_fields, + epiphany_state, + }) +} + +pub async fn apply_thread_epiphany_promote( + thread: &CodexThread, + expected_revision: Option, + patch: ThreadEpiphanyUpdatePatch, + verifier_evidence: EpiphanyEvidenceRecord, +) -> Result { + let decision = evaluate_promotion(EpiphanyPromotionInput { + has_state_replacements: thread_epiphany_patch_has_state_replacements(&patch), + active_subgoal_id: patch.active_subgoal_id.clone(), + subgoals: patch.subgoals.clone(), + invariants: patch.invariants.clone(), + graphs: patch.graphs.clone(), + graph_frontier: patch.graph_frontier.clone(), + graph_checkpoint: patch.graph_checkpoint.clone(), + investigation_checkpoint: patch.investigation_checkpoint.clone(), + churn: patch.churn.clone(), + observations: patch.observations.clone(), + evidence: patch.evidence.clone(), + verifier_evidence: verifier_evidence.clone(), + }); + if !decision.accepted { + return Ok(EpiphanyThreadPromoteApplied::Rejected { + reasons: decision.reasons, + }); + } + + let changed_fields = epiphany_promote_changed_fields(&patch); + let mut patch = patch; + patch.evidence.push(verifier_evidence); + let update = state_update_from_thread_patch(expected_revision, patch); + let epiphany_state = thread.epiphany_update_state(update).await?; + let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await; + Ok(EpiphanyThreadPromoteApplied::Accepted( + EpiphanyThreadUpdateApplied { + revision: epiphany_state.revision, + changed_fields, + epiphany_state, + }, + )) +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 68fc9c77..758f00f1 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -615,6 +615,16 @@ persistence validation, and rollout/session writeback. The remaining impurity is route-facing orchestration in `codex_message_processor.rs` / `epiphany_mutation_routes.rs`; move that behind a native service boundary next. +The first route-facing mutation service cut has started. Update/promote +mutation application now routes through +`epiphany-codex-bridge::mutation_service::{apply_thread_epiphany_update, +apply_thread_epiphany_promote}` so the vendored app-server handler no longer +owns promotion evaluation, patch-to-update projection, changed-field derivation, +or state application for those two write verbs. The handler still owns +thread-id parsing, loaded-thread lookup, JSON-RPC response shaping, and +notification emission; role/reorient accept plus launch/interrupt route +orchestration remain to cut. + Also: MCP itself is allowed to be JSON. The target is not "replace MCP JSON"; the target is an Epiphany-owned boundary that speaks typed Epiphany intent/result/receipt documents internally and normal MCP JSON-RPC externally. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index faa465377f770f85e85db9af70cdb6e0fc3d2ead..bee7749bb729c257a539c0c2d041474ef54bc5ab 100644 GIT binary patch delta 407 zcmXYtzfQw25Qh~ckQk9*KM8efnzXbGVP;@NAO;p>;!AUi>$7a9f2Jb!0YD(pjdwso zV(G>RkHAat1~^Jb|LnW(^Y86(_v?B0>3HX2|Go1XMZc>}^tp+mpY!#q6CLc0vf&^b zzVyGlOL66e^(et|?J+JyPI?TJ+6zyt!J(Z}j(~G-Mczai)J6)AiLpFkkKn>n1YwM# zydBa!14UDGKpN1HDTQ9dIZK?!_m<=svusPa)OTA%7Zi$V0}rd>CPdpxK^T_`tpm?s zgmsuG7BaEu3~QNa_Eao-aD$c#TK^@{;c~{daz+I6yu`dZYb>=CiZw#3&|ZX`l!B6c z*j^%3!Ck`?wPYJgQJhzSS`o*-)??>UQ}}Fw!q*mI#1(w5>b6{RfYzm9GE* delta 36 scmX>+iFIB->jY&c_mqvQ(-;|VH}7HGzK4 epiphany_state, + let applied = match apply_thread_epiphany_promote( + thread.as_ref(), + expected_revision, + patch, + verifier_evidence, + ) + .await + { + Ok(applied) => applied, Err(CodexErr::InvalidRequest(message)) => { self.send_invalid_request_error(request_id, message).await; return; @@ -776,12 +747,31 @@ impl CodexMessageProcessor { return; } }; - let epiphany_state = - client_visible_live_thread_epiphany_state(thread.as_ref(), epiphany_state).await; + + let applied = match applied { + EpiphanyThreadPromoteApplied::Accepted(applied) => applied, + EpiphanyThreadPromoteApplied::Rejected { reasons } => { + self.outgoing + .send_response( + request_id, + ThreadEpiphanyPromoteResponse { + accepted: false, + reasons, + revision: None, + changed_fields: Vec::new(), + epiphany_state: None, + }, + ) + .await; + return; + } + }; + let changed_fields = applied.changed_fields; + let epiphany_state = applied.epiphany_state; let response = ThreadEpiphanyPromoteResponse { accepted: true, reasons: Vec::new(), - revision: Some(epiphany_state.revision), + revision: Some(applied.revision), changed_fields: changed_fields.clone(), epiphany_state: Some(epiphany_state.clone()), }; @@ -832,10 +822,10 @@ impl CodexMessageProcessor { } }; - let changed_fields = epiphany_update_patch_changed_fields(&patch); - let update = state_update_from_thread_patch(expected_revision, patch); - let epiphany_state = match thread.epiphany_update_state(update).await { - Ok(epiphany_state) => epiphany_state, + let applied = match apply_thread_epiphany_update(thread.as_ref(), expected_revision, patch) + .await + { + Ok(applied) => applied, Err(CodexErr::InvalidRequest(message)) => { self.send_invalid_request_error(request_id, message).await; return; @@ -849,10 +839,10 @@ impl CodexMessageProcessor { return; } }; - let epiphany_state = - client_visible_live_thread_epiphany_state(thread.as_ref(), epiphany_state).await; + let changed_fields = applied.changed_fields; + let epiphany_state = applied.epiphany_state; let response = ThreadEpiphanyUpdateResponse { - revision: epiphany_state.revision, + revision: applied.revision, changed_fields: changed_fields.clone(), epiphany_state: epiphany_state.clone(), };