Move Epiphany update routes into bridge service

This commit is contained in:
Meta Vens 2026-05-13 06:42:57 +01:00
commit f3ae9f4cb5
6 changed files with 138 additions and 53 deletions

View file

@ -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;

View file

@ -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<ThreadEpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
}
#[derive(Debug, Clone)]
pub enum EpiphanyThreadPromoteApplied {
Rejected { reasons: Vec<String> },
Accepted(EpiphanyThreadUpdateApplied),
}
pub async fn apply_thread_epiphany_update(
thread: &CodexThread,
expected_revision: Option<u64>,
patch: ThreadEpiphanyUpdatePatch,
) -> Result<EpiphanyThreadUpdateApplied, CodexErr> {
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<u64>,
patch: ThreadEpiphanyUpdatePatch,
verifier_evidence: EpiphanyEvidenceRecord,
) -> Result<EpiphanyThreadPromoteApplied, CodexErr> {
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,
},
))
}

View file

@ -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.

Binary file not shown.

View file

@ -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.

View file

@ -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(),
};