Move Epiphany launch routes into bridge service

This commit is contained in:
Meta Vens 2026-05-13 06:55:37 +01:00
commit 98ee32d21e
5 changed files with 162 additions and 87 deletions

View file

@ -1,24 +1,32 @@
use chrono::SecondsFormat;
use chrono::Utc;
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyJob;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedField;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use codex_core::CodexThread;
use codex_core::EpiphanyJobInterruptRequest;
use codex_core::EpiphanyJobLaunchRequest;
use codex_core::EpiphanyPromotionInput;
use codex_core::evaluate_promotion;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use codex_protocol::protocol::EpiphanyEvidenceRecord;
use codex_protocol::protocol::EpiphanyThreadState;
use crate::jobs::map_interrupted_epiphany_job;
use crate::jobs::map_launched_epiphany_job;
use crate::launch::build_epiphany_role_launch_request;
use crate::launch::epiphany_role_label;
use crate::mutation::build_reorient_acceptance_update;
use crate::mutation::build_role_acceptance_update;
use crate::mutation::epiphany_job_launch_changed_fields;
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::launch::epiphany_role_label;
use crate::runtime_results::load_completed_epiphany_reorient_finding;
use crate::runtime_results::load_completed_epiphany_role_finding;
use crate::state::client_visible_live_thread_epiphany_state;
@ -60,6 +68,24 @@ pub struct EpiphanyReorientAcceptApplied {
pub finding: ThreadEpiphanyReorientFinding,
}
#[derive(Debug, Clone)]
pub struct EpiphanyJobLaunchApplied {
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub job: ThreadEpiphanyJob,
}
#[derive(Debug, Clone)]
pub struct EpiphanyJobInterruptApplied {
pub cancel_requested: bool,
pub interrupted_thread_ids: Vec<String>,
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub job: ThreadEpiphanyJob,
}
pub async fn apply_thread_epiphany_update(
thread: &CodexThread,
expected_revision: Option<u64>,
@ -223,6 +249,90 @@ pub async fn apply_thread_epiphany_reorient_accept(
})
}
pub async fn launch_thread_epiphany_role(
thread: &CodexThread,
thread_id: &str,
role_id: ThreadEpiphanyRoleId,
expected_revision: Option<u64>,
max_runtime_seconds: Option<u64>,
) -> Result<EpiphanyJobLaunchApplied, CodexErr> {
let state = thread.epiphany_state().await.ok_or_else(|| {
CodexErr::InvalidRequest(
"cannot launch an Epiphany role specialist without authoritative Epiphany state"
.to_string(),
)
})?;
let launch_request = build_epiphany_role_launch_request(
thread_id,
role_id,
expected_revision,
max_runtime_seconds,
&state,
)
.map_err(CodexErr::InvalidRequest)?;
launch_thread_epiphany_job(
thread,
launch_request,
CoreEpiphanyJobKind::Specialist,
"missing launched role projection",
)
.await
}
pub async fn launch_thread_epiphany_job(
thread: &CodexThread,
launch_request: EpiphanyJobLaunchRequest,
kind: CoreEpiphanyJobKind,
missing_projection_reason: &str,
) -> Result<EpiphanyJobLaunchApplied, CodexErr> {
let changed_fields = epiphany_job_launch_changed_fields();
let launched = thread.epiphany_launch_job(launch_request).await?;
let epiphany_state =
client_visible_live_thread_epiphany_state(thread, launched.epiphany_state).await;
let job = map_launched_epiphany_job(
&epiphany_state,
launched.binding_id.as_str(),
launched.launcher_job_id.as_str(),
launched.backend_job_id.as_str(),
kind,
missing_projection_reason,
);
Ok(EpiphanyJobLaunchApplied {
revision: epiphany_state.revision,
changed_fields,
epiphany_state,
job,
})
}
pub async fn interrupt_thread_epiphany_job(
thread: &CodexThread,
expected_revision: Option<u64>,
binding_id: &str,
reason: Option<String>,
) -> Result<EpiphanyJobInterruptApplied, CodexErr> {
let changed_fields = vec![ThreadEpiphanyStateUpdatedField::JobBindings];
let interrupted = thread
.epiphany_interrupt_job(EpiphanyJobInterruptRequest {
expected_revision,
binding_id: binding_id.to_string(),
reason,
})
.await?;
let epiphany_state =
client_visible_live_thread_epiphany_state(thread, interrupted.epiphany_state).await;
let job = map_interrupted_epiphany_job(&epiphany_state, binding_id);
Ok(EpiphanyJobInterruptApplied {
cancel_requested: interrupted.cancel_requested,
interrupted_thread_ids: interrupted.interrupted_thread_ids,
revision: epiphany_state.revision,
changed_fields,
epiphany_state,
job,
})
}
fn validate_expected_revision(
expected_revision: Option<u64>,
actual_revision: u64,

View file

@ -630,8 +630,15 @@ Role/reorient accept have followed into the same bridge service.
checks, typed runtime-spine finding load, acceptance id/timestamp generation,
acceptance update construction, state application, and client-visible state
projection. The vendored mutation route still parses JSON-RPC params, loads the
thread, shapes responses, and emits notifications. Launch/interrupt route
orchestration remains to cut.
thread, shapes responses, and emits notifications.
Role launch, generic job launch, and job interrupt now route through the bridge
service too. `launch_thread_epiphany_role`, `launch_thread_epiphany_job`, and
`interrupt_thread_epiphany_job` own launch/interrupt application,
changed-field derivation, live-state projection, and job projection. The
remaining thick mutation-route knot is reorient launch: it still composes
freshness, pressure, watcher state, reorientation decision, launch request, and
response shaping inside the vendored route.
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

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`, native state-update contract/validation/application in `epiphany-core`, and the 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/role-accept/reorient-accept 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 launch/interrupt mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 914 lines after the accept 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/generic launch plus 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.
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 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/role-accept/reorient-accept/role-launch/job-launch/job-interrupt 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 reorient-launch orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 872 lines after the launch/interrupt 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 reorient-launch orchestration out of `codex_message_processor.rs` / `epiphany_mutation_routes.rs` behind native view/launch 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

@ -1,16 +1,11 @@
use codex_app_server_protocol::*;
use codex_core::EpiphanyJobInterruptRequest;
use codex_core::EpiphanyJobLaunchRequest;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use epiphany_codex_bridge::jobs::epiphany_blocked_state_job;
use epiphany_codex_bridge::jobs::map_epiphany_jobs;
use epiphany_codex_bridge::jobs::map_interrupted_epiphany_job;
use epiphany_codex_bridge::jobs::map_launched_epiphany_job;
use epiphany_codex_bridge::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
use epiphany_codex_bridge::launch::build_epiphany_reorient_launch_request;
use epiphany_codex_bridge::launch::build_epiphany_role_launch_request;
use epiphany_codex_bridge::launch::epiphany_role_binding_id;
use epiphany_codex_bridge::launch::map_core_worker_launch_document;
use epiphany_codex_bridge::mutation::epiphany_job_launch_changed_fields;
@ -20,6 +15,9 @@ use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_reorient_acce
use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_role_accept;
use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_promote;
use epiphany_codex_bridge::mutation_service::apply_thread_epiphany_update;
use epiphany_codex_bridge::mutation_service::interrupt_thread_epiphany_job;
use epiphany_codex_bridge::mutation_service::launch_thread_epiphany_job;
use epiphany_codex_bridge::mutation_service::launch_thread_epiphany_role;
use epiphany_codex_bridge::pressure::map_epiphany_pressure;
use epiphany_codex_bridge::reorient::map_epiphany_freshness;
use epiphany_codex_bridge::reorient::map_epiphany_reorient;
@ -64,35 +62,16 @@ impl CodexMessageProcessor {
return;
}
};
let state = match loaded_thread.epiphany_state().await {
Some(state) => state,
None => {
self.send_invalid_request_error(
request_id,
"cannot launch an Epiphany role specialist without authoritative Epiphany state"
.to_string(),
)
.await;
return;
}
};
let launch_request = match build_epiphany_role_launch_request(
let applied = match launch_thread_epiphany_role(
loaded_thread.as_ref(),
&thread_id,
role_id,
expected_revision,
max_runtime_seconds,
&state,
) {
Ok(request) => request,
Err(message) => {
self.send_invalid_request_error(request_id, message).await;
return;
}
};
let changed_fields = epiphany_job_launch_changed_fields();
let launched = match loaded_thread.epiphany_launch_job(launch_request).await {
Ok(launched) => launched,
)
.await
{
Ok(applied) => applied,
Err(CodexErr::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
@ -106,20 +85,8 @@ impl CodexMessageProcessor {
return;
}
};
let epiphany_state = client_visible_live_thread_epiphany_state(
loaded_thread.as_ref(),
launched.epiphany_state,
)
.await;
let job = map_launched_epiphany_job(
&epiphany_state,
launched.binding_id.as_str(),
launched.launcher_job_id.as_str(),
launched.backend_job_id.as_str(),
CoreEpiphanyJobKind::Specialist,
"missing launched role projection",
);
let changed_fields = applied.changed_fields;
let epiphany_state = applied.epiphany_state;
self.outgoing
.send_response(
@ -127,10 +94,10 @@ impl CodexMessageProcessor {
ThreadEpiphanyRoleLaunchResponse {
thread_id: thread_uuid.to_string(),
role_id,
revision: epiphany_state.revision,
revision: applied.revision,
changed_fields: changed_fields.clone(),
epiphany_state: epiphany_state.clone(),
job,
job: applied.job,
},
)
.await;
@ -757,9 +724,9 @@ impl CodexMessageProcessor {
};
let launch_document = map_core_worker_launch_document(launch_document);
let changed_fields = epiphany_job_launch_changed_fields();
let launched = match thread
.epiphany_launch_job(EpiphanyJobLaunchRequest {
let applied = match launch_thread_epiphany_job(
thread.as_ref(),
EpiphanyJobLaunchRequest {
expected_revision,
binding_id: binding_id.clone(),
kind,
@ -772,10 +739,13 @@ impl CodexMessageProcessor {
launch_document,
output_contract_id,
max_runtime_seconds,
})
.await
},
kind,
"missing launched job projection",
)
.await
{
Ok(launched) => launched,
Ok(applied) => applied,
Err(CodexErr::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
@ -789,26 +759,17 @@ impl CodexMessageProcessor {
return;
}
};
let epiphany_state =
client_visible_live_thread_epiphany_state(thread.as_ref(), launched.epiphany_state)
.await;
let job = map_launched_epiphany_job(
&epiphany_state,
launched.binding_id.as_str(),
launched.launcher_job_id.as_str(),
launched.backend_job_id.as_str(),
kind,
"missing launched job projection",
);
let changed_fields = applied.changed_fields;
let epiphany_state = applied.epiphany_state;
self.outgoing
.send_response(
request_id,
ThreadEpiphanyJobLaunchResponse {
revision: epiphany_state.revision,
revision: applied.revision,
changed_fields: changed_fields.clone(),
epiphany_state: epiphany_state.clone(),
job,
job: applied.job,
},
)
.await;
@ -858,16 +819,15 @@ impl CodexMessageProcessor {
}
};
let changed_fields = vec![ThreadEpiphanyStateUpdatedField::JobBindings];
let interrupted = match thread
.epiphany_interrupt_job(EpiphanyJobInterruptRequest {
expected_revision,
binding_id: binding_id.clone(),
reason,
})
.await
let applied = match interrupt_thread_epiphany_job(
thread.as_ref(),
expected_revision,
&binding_id,
reason,
)
.await
{
Ok(interrupted) => interrupted,
Ok(applied) => applied,
Err(CodexErr::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
@ -881,21 +841,19 @@ impl CodexMessageProcessor {
return;
}
};
let epiphany_state =
client_visible_live_thread_epiphany_state(thread.as_ref(), interrupted.epiphany_state)
.await;
let job = map_interrupted_epiphany_job(&epiphany_state, &binding_id);
let changed_fields = applied.changed_fields;
let epiphany_state = applied.epiphany_state;
self.outgoing
.send_response(
request_id,
ThreadEpiphanyJobInterruptResponse {
cancel_requested: interrupted.cancel_requested,
interrupted_thread_ids: interrupted.interrupted_thread_ids.clone(),
revision: epiphany_state.revision,
cancel_requested: applied.cancel_requested,
interrupted_thread_ids: applied.interrupted_thread_ids.clone(),
revision: applied.revision,
changed_fields: changed_fields.clone(),
epiphany_state: epiphany_state.clone(),
job,
job: applied.job,
},
)
.await;