diff --git a/epiphany-codex-bridge/src/mutation_service.rs b/epiphany-codex-bridge/src/mutation_service.rs index 82195501..429fe05b 100644 --- a/epiphany-codex-bridge/src/mutation_service.rs +++ b/epiphany-codex-bridge/src/mutation_service.rs @@ -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, + pub epiphany_state: EpiphanyThreadState, + pub job: ThreadEpiphanyJob, +} + +#[derive(Debug, Clone)] +pub struct EpiphanyJobInterruptApplied { + pub cancel_requested: bool, + pub interrupted_thread_ids: Vec, + pub revision: u64, + pub changed_fields: Vec, + pub epiphany_state: EpiphanyThreadState, + pub job: ThreadEpiphanyJob, +} + pub async fn apply_thread_epiphany_update( thread: &CodexThread, expected_revision: Option, @@ -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, + max_runtime_seconds: Option, +) -> Result { + 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 { + 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, + binding_id: &str, + reason: Option, +) -> Result { + 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, actual_revision: u64, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 6c9a37a2..1ca39e2d 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index b4a2712c..7043c82e 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index ef4118aa..2110b2f9 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`, 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. 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 fd815264..19910167 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 @@ -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;