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 faa46537..bee7749b 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 3fddd96a..c66c7bed 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -279,5 +279,5 @@ rejected_paths: reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet. current_status: - summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, and native state-update contract/validation/application in `epiphany-core`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,071 lines, `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader, and `codex_thread.rs` is about 1,425 lines after state-update law moved native. `epiphany-openai-adapter` defines CultCache `DatabaseEntry` request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. `epiphany-openai-runtime` writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, and its smoke proves the route without network. `epiphany-core::plan_runtime_spine_heartbeat_launch` validates launch contracts/conflicts and projects durable job bindings/runtime links; `epiphany-core::open_runtime_spine_heartbeat_job` owns the initialize-runtime/ensure-session/create-job sequence; `epiphany-core::EpiphanyStateUpdate`, `epiphany_state_update_validation_errors`, and `apply_epiphany_state_update` own state-update document shape and mutation law while `codex-core` calls them for compatibility. Vendored `CodexThread::epiphany_launch_job` is now mostly revision check, persistence validation, and rollout/session writeback for the compatibility route. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the native caller now has a real storage route instead of only JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after live callers move. - next_action: Continue Codex starvation by moving mutation route orchestration out of `codex_message_processor.rs` / `epiphany_mutation_routes.rs` behind a native service boundary, leaving Codex as persistence/JSON-RPC compatibility only. Then move live model-turn callers onto `epiphany-openai-runtime` or the same native CultNet contract and shrink the surviving Codex dependency surface to credential refresh, provider/model routing, and the smallest OpenAI Responses call required for Codex subscription compatibility. + summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, native state-update contract/validation/application in `epiphany-core`, and the first route-facing mutation service in `epiphany-codex-bridge`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,061 lines after the update/promote service cut, `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader, and `codex_thread.rs` is about 1,425 lines after state-update law moved native. `epiphany-openai-adapter` defines CultCache `DatabaseEntry` request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. `epiphany-openai-runtime` writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, and its smoke proves the route without network. `epiphany-core::plan_runtime_spine_heartbeat_launch` validates launch contracts/conflicts and projects durable job bindings/runtime links; `epiphany-core::open_runtime_spine_heartbeat_job` owns the initialize-runtime/ensure-session/create-job sequence; `epiphany-core::EpiphanyStateUpdate`, `epiphany_state_update_validation_errors`, and `apply_epiphany_state_update` own state-update document shape and mutation law while `codex-core` calls them for compatibility. Vendored `CodexThread::epiphany_launch_job` is now mostly revision check, persistence validation, and rollout/session writeback for the compatibility route. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the native caller now has a real storage route instead of only JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after live callers move. + next_action: Continue Codex starvation by moving role/reorient accept plus launch/interrupt route orchestration out of `codex_message_processor.rs` / `epiphany_mutation_routes.rs` behind native service boundaries, leaving Codex as persistence/JSON-RPC compatibility only. Then move live model-turn callers onto `epiphany-openai-runtime` or the same native CultNet contract and shrink the surviving Codex dependency surface to credential refresh, provider/model routing, and the smallest OpenAI Responses call required for Codex subscription compatibility. diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_mutation_routes.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_mutation_routes.rs index a4eb457e..411789ed 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_mutation_routes.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_mutation_routes.rs @@ -3,8 +3,6 @@ use chrono::Utc; use codex_app_server_protocol::*; use codex_core::EpiphanyJobInterruptRequest; use codex_core::EpiphanyJobLaunchRequest; -use codex_core::EpiphanyPromotionInput; -use codex_core::evaluate_promotion; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind; @@ -21,11 +19,11 @@ use epiphany_codex_bridge::launch::map_core_worker_launch_document; use epiphany_codex_bridge::mutation::build_reorient_acceptance_update; use epiphany_codex_bridge::mutation::build_role_acceptance_update; use epiphany_codex_bridge::mutation::epiphany_job_launch_changed_fields; -use epiphany_codex_bridge::mutation::epiphany_promote_changed_fields; use epiphany_codex_bridge::mutation::epiphany_state_updated_notification; -use epiphany_codex_bridge::mutation::epiphany_update_patch_changed_fields; use epiphany_codex_bridge::mutation::state_update_from_thread_patch; -use epiphany_codex_bridge::mutation::thread_epiphany_patch_has_state_replacements; +use epiphany_codex_bridge::mutation_service::EpiphanyThreadPromoteApplied; +use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_promote; +use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_update; use epiphany_codex_bridge::pressure::map_epiphany_pressure; use epiphany_codex_bridge::reorient::map_epiphany_freshness; use epiphany_codex_bridge::reorient::map_epiphany_reorient; @@ -727,42 +725,15 @@ impl CodexMessageProcessor { } }; - 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 { - self.outgoing - .send_response( - request_id, - ThreadEpiphanyPromoteResponse { - accepted: false, - reasons: decision.reasons, - revision: None, - changed_fields: Vec::new(), - epiphany_state: None, - }, - ) - .await; - return; - } - - 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 = match thread.epiphany_update_state(update).await { - Ok(epiphany_state) => 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(), };