diff --git a/epiphany-codex-bridge/src/lib.rs b/epiphany-codex-bridge/src/lib.rs index eb1983df..6c121446 100644 --- a/epiphany-codex-bridge/src/lib.rs +++ b/epiphany-codex-bridge/src/lib.rs @@ -11,6 +11,7 @@ pub mod retrieve; pub mod runtime_results; pub mod scene; pub mod state; +pub mod view; pub use epiphany_openai_adapter as openai_adapter; pub use epiphany_openai_codex_spine as openai_codex_spine; diff --git a/epiphany-codex-bridge/src/view.rs b/epiphany-codex-bridge/src/view.rs new file mode 100644 index 00000000..e8563544 --- /dev/null +++ b/epiphany-codex-bridge/src/view.rs @@ -0,0 +1,314 @@ +use std::path::Path; + +use codex_app_server_protocol::*; +use codex_protocol::protocol::EpiphanyRetrievalState; +use codex_protocol::protocol::EpiphanyThreadState; +use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; +use epiphany_core::EpiphanySceneInput; +use epiphany_core::derive_scene; + +use crate::context::map_epiphany_planning; +use crate::coordinator::derive_epiphany_coordinator_status; +use crate::coordinator::epiphany_reorient_finding_already_accepted; +use crate::coordinator::map_epiphany_coordinator_view; +use crate::coordinator::map_epiphany_crrc_recommendation; +use crate::coordinator::map_epiphany_roles; +use crate::coordinator::render_epiphany_roles_note; +use crate::jobs::map_epiphany_jobs; +use crate::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID; +use crate::pressure::map_epiphany_pressure; +use crate::reorient::EpiphanyFreshnessWatcherSnapshot; +use crate::reorient::map_epiphany_freshness; +use crate::reorient::map_epiphany_reorient; +use crate::runtime_results::load_epiphany_reorient_result_snapshot; +use crate::scene::map_core_epiphany_scene_action; +use crate::scene::map_epiphany_scene; + +pub fn default_epiphany_view_lenses() -> Vec { + vec![ + ThreadEpiphanyViewLens::Scene, + ThreadEpiphanyViewLens::Jobs, + ThreadEpiphanyViewLens::Roles, + ThreadEpiphanyViewLens::Planning, + ThreadEpiphanyViewLens::Pressure, + ThreadEpiphanyViewLens::Reorient, + ThreadEpiphanyViewLens::Crrc, + ThreadEpiphanyViewLens::Coordinator, + ] +} + +pub fn epiphany_view_needs_jobs(lenses: &[ThreadEpiphanyViewLens]) -> bool { + lenses.contains(&ThreadEpiphanyViewLens::Jobs) + || lenses.contains(&ThreadEpiphanyViewLens::Roles) + || lenses.contains(&ThreadEpiphanyViewLens::Crrc) + || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) +} + +pub fn epiphany_view_needs_reorientation_inputs(lenses: &[ThreadEpiphanyViewLens]) -> bool { + lenses.contains(&ThreadEpiphanyViewLens::Roles) + || lenses.contains(&ThreadEpiphanyViewLens::Reorient) + || lenses.contains(&ThreadEpiphanyViewLens::Crrc) + || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) +} + +pub fn epiphany_view_needs_pressure(lenses: &[ThreadEpiphanyViewLens]) -> bool { + lenses.contains(&ThreadEpiphanyViewLens::Pressure) + || epiphany_view_needs_reorientation_inputs(lenses) +} + +pub fn epiphany_view_needs_runtime_store(lenses: &[ThreadEpiphanyViewLens]) -> bool { + lenses.contains(&ThreadEpiphanyViewLens::Roles) + || lenses.contains(&ThreadEpiphanyViewLens::Crrc) + || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) +} + +pub struct EpiphanyViewResponseInput<'a> { + pub thread_id: String, + pub lenses: Vec, + pub loaded: bool, + pub state: Option<&'a EpiphanyThreadState>, + pub retrieval_override: Option<&'a EpiphanyRetrievalState>, + pub watcher_snapshot: Option>, + pub token_usage_info: Option<&'a CoreTokenUsageInfo>, + pub runtime_store_path: Option<&'a Path>, +} + +pub async fn map_epiphany_view_response( + input: EpiphanyViewResponseInput<'_>, +) -> ThreadEpiphanyViewResponse { + let EpiphanyViewResponseInput { + thread_id, + lenses, + loaded, + state, + retrieval_override, + watcher_snapshot, + token_usage_info, + runtime_store_path, + } = input; + + let needs_jobs = epiphany_view_needs_jobs(&lenses); + let needs_reorientation_inputs = epiphany_view_needs_reorientation_inputs(&lenses); + let needs_pressure = epiphany_view_needs_pressure(&lenses); + let pressure = needs_pressure.then(|| map_epiphany_pressure(token_usage_info)); + let freshness = needs_reorientation_inputs + .then(|| map_epiphany_freshness(state, retrieval_override, watcher_snapshot)); + let (state_revision, reorient_state_status, reorient_decision) = + if let (Some((state_revision, retrieval, graph, watcher)), Some(pressure)) = + (freshness.as_ref(), pressure.as_ref()) + { + let (state_status, decision) = + map_epiphany_reorient(state, pressure, retrieval, graph, watcher); + (*state_revision, state_status, Some(decision)) + } else { + (None, ThreadEpiphanyReorientStateStatus::Missing, None) + }; + let jobs = if needs_jobs { + map_epiphany_jobs(state, retrieval_override) + } else { + Vec::new() + }; + let reorient_job = jobs + .iter() + .find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID) + .cloned(); + let (reorient_result_status, reorient_finding, reorient_result_note) = if runtime_store_path + .is_some() + || lenses.contains(&ThreadEpiphanyViewLens::Crrc) + || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) + { + load_epiphany_reorient_result_snapshot( + state, + runtime_store_path, + EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + ) + .await + } else { + ( + ThreadEpiphanyReorientResultStatus::MissingState, + None, + "Reorient result was not requested.".to_string(), + ) + }; + let checkpoint_present = state + .and_then(|state| state.investigation_checkpoint.as_ref()) + .is_some(); + let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| { + state.is_some_and(|state| epiphany_reorient_finding_already_accepted(state, finding)) + }); + let recommendation = + if let (Some(pressure), Some(decision)) = (pressure.as_ref(), reorient_decision.as_ref()) { + Some(map_epiphany_crrc_recommendation( + loaded, + reorient_state_status, + pressure, + decision, + reorient_result_status, + checkpoint_present, + reorient_finding.is_some(), + reorient_finding_accepted, + )) + } else { + None + }; + let roles = if let (Some(pressure), Some(decision), Some(recommendation)) = ( + pressure.as_ref(), + reorient_decision.as_ref(), + recommendation.as_ref(), + ) { + Some(map_epiphany_roles( + state, + &jobs, + decision, + pressure, + recommendation, + reorient_result_status, + reorient_job.as_ref(), + )) + } else { + None + }; + let coordinator_response = if lenses.contains(&ThreadEpiphanyViewLens::Coordinator) { + if let (Some(pressure), Some(recommendation), Some(roles)) = + (pressure.as_ref(), recommendation.as_ref(), roles.clone()) + { + let status = derive_epiphany_coordinator_status( + state, + runtime_store_path, + reorient_state_status, + pressure, + recommendation, + roles, + reorient_decision.as_ref(), + reorient_result_status, + reorient_finding.as_ref(), + checkpoint_present, + ) + .await; + Some(map_epiphany_coordinator_view( + thread_id.clone(), + loaded, + reorient_state_status, + state_revision, + status, + )) + } else { + None + } + } else { + None + }; + + ThreadEpiphanyViewResponse { + thread_id: thread_id.clone(), + scene: lenses + .contains(&ThreadEpiphanyViewLens::Scene) + .then(|| map_epiphany_scene(state, loaded, EPIPHANY_REORIENT_LAUNCH_BINDING_ID)), + jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) { + jobs.clone() + } else { + Vec::new() + }, + roles: lenses.contains(&ThreadEpiphanyViewLens::Roles).then(|| { + let roles = roles.clone().unwrap_or_default(); + ThreadEpiphanyViewRoles { + thread_id: thread_id.clone(), + source: if loaded { + ThreadEpiphanyRolesSource::Live + } else { + ThreadEpiphanyRolesSource::Stored + }, + state_status: reorient_state_status, + state_revision, + note: render_epiphany_roles_note( + &roles, + reorient_state_status, + recommendation + .as_ref() + .map(|recommendation| recommendation.action) + .unwrap_or(ThreadEpiphanyCrrcAction::Continue), + ), + roles, + } + }), + planning: lenses.contains(&ThreadEpiphanyViewLens::Planning).then(|| { + let (state_status, state_revision, planning, summary) = map_epiphany_planning(state); + ThreadEpiphanyViewPlanning { + thread_id: thread_id.clone(), + source: if loaded { + ThreadEpiphanyContextSource::Live + } else { + ThreadEpiphanyContextSource::Stored + }, + state_status, + state_revision, + planning, + summary, + } + }), + pressure: lenses + .contains(&ThreadEpiphanyViewLens::Pressure) + .then(|| pressure.clone()) + .flatten(), + reorient: lenses + .contains(&ThreadEpiphanyViewLens::Reorient) + .then(|| { + reorient_decision + .clone() + .map(|decision| ThreadEpiphanyViewReorient { + thread_id: thread_id.clone(), + source: if loaded { + ThreadEpiphanyReorientSource::Live + } else { + ThreadEpiphanyReorientSource::Stored + }, + state_status: reorient_state_status, + state_revision, + decision, + }) + }) + .flatten(), + crrc: lenses + .contains(&ThreadEpiphanyViewLens::Crrc) + .then(|| { + let pressure = pressure.clone()?; + let decision = reorient_decision.clone()?; + let recommendation = recommendation.clone()?; + let available_actions = derive_scene(EpiphanySceneInput { + state, + loaded, + reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + }) + .available_actions + .into_iter() + .map(map_core_epiphany_scene_action) + .collect(); + let note = format!( + "{} Result status: {:?}. {}", + recommendation.reason, reorient_result_status, reorient_result_note + ); + Some(ThreadEpiphanyViewCrrc { + thread_id: thread_id.clone(), + source: if loaded { + ThreadEpiphanyReorientSource::Live + } else { + ThreadEpiphanyReorientSource::Stored + }, + state_status: reorient_state_status, + state_revision, + pressure, + decision, + recommendation, + reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(), + reorient_result_status, + reorient_job: reorient_job.clone(), + reorient_finding: reorient_finding.clone(), + available_actions, + note, + }) + }) + .flatten(), + coordinator: coordinator_response, + lenses, + } +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 397ec8ce..05dd786c 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -698,6 +698,17 @@ longer owns that coordinator policy. The root `codex_message_processor.rs` imports for the evacuated Epiphany protocol/policy registry were cut as proof that those names no longer belong to the host file. +The full `thread/epiphany/view` projection has now followed. App-server still +parses the thread id, loads live/stored thread state, registers watcher input, +fetches token usage, and obtains the runtime-spine store path. The actual lens +selection, freshness/pressure/reorient/CRRC/roles/coordinator composition, +scene/planning/job projection, and `ThreadEpiphanyViewResponse` construction +now live in `epiphany-codex-bridge::view`. `epiphany_read_routes.rs` is about +722 lines after this cut. The remaining read-route bodies are mostly individual +legacy read verbs (`roleResult`, `freshness`, `context`, `graphQuery`, +`reorientResult`, `retrieve`, `distill`, `propose`) plus host loading and +response/error shaping. + 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 5d6148fc..1713a07d 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 001eb53a..1256d7c1 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, nineteen 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/coordinator projection services in `epiphany-codex-bridge`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, coordinator status/view construction, 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, JSON-RPC response shaping, watcher registration/snapshotting, and notification emission, but its root no longer imports the evacuated Epiphany policy/protocol registry. The processor was about 10,339 lines before the coordinator extraction, `epiphany_mutation_routes.rs` is about 691 lines after the route-load/notification helper cut and index projection move, `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`, maps deltas / completion into typed stream events and receipts, and no longer directly depends on `codex-app-server-protocol` just to name auth mode. `epiphany-openai-runtime` writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, can emit the assistant text artifact for legacy result consumers, is now the default executor for startup-only repo birth specialists, and now has `run-worker` to load a durable `EpiphanyRuntimeWorkerLaunchRequest`, call the typed OpenAI runtime, persist typed `EpiphanyRuntimeRoleWorkerResult` / `EpiphanyRuntimeReorientWorkerResult` documents, and complete the original heartbeat/specialist runtime job without Codex worker execution; `codex exec` survives only as explicit fallback in repo-birth startup. `epiphany-mvp-coordinator` now invokes `epiphany-openai-runtime run-worker` for launched runtime job ids and no longer creates a second coordinator-owned shadow runtime job/result as a compensator. `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 and now persists `EpiphanyRuntimeWorkerLaunchRequest` CultCache records keyed by runtime job id, with indexed kind/contract fields plus MessagePack-encoded typed launch documents instead of discarding work intent after queue receipt creation; `roleResult` and `reorientResult` now prefer typed worker result documents and fall back only to generic job summaries for older receipts; `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, launch request handoff, and rollout/session writeback for the compatibility route. Vendored `codex-core` no longer has `epiphany_distillation`, `epiphany_promotion`, `epiphany_proposal`, `epiphany_retrieval`, or `epiphany_rollout` husk modules; it re-exports native surfaces from `epiphany-core` directly, and the only rollout adapter left is the real host seam that supplies Codex's turn-boundary predicate. `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 richer live callers move. + summary: The Codex starvation pass has five typing receipts, twenty 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/view/coordinator projection services in `epiphany-codex-bridge`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, full `thread/epiphany/view` response construction, coordinator status/view construction, 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, JSON-RPC response shaping, watcher registration/snapshotting, and notification emission, but its root no longer imports the evacuated Epiphany policy/protocol registry. `epiphany_read_routes.rs` is about 722 lines after the full view-response extraction; `epiphany_mutation_routes.rs` is about 691 lines after the route-load/notification helper cut and index projection move; `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`, maps deltas / completion into typed stream events and receipts, and no longer directly depends on `codex-app-server-protocol` just to name auth mode. `epiphany-openai-runtime` writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, can emit the assistant text artifact for legacy result consumers, is now the default executor for startup-only repo birth specialists, and now has `run-worker` to load a durable `EpiphanyRuntimeWorkerLaunchRequest`, call the typed OpenAI runtime, persist typed `EpiphanyRuntimeRoleWorkerResult` / `EpiphanyRuntimeReorientWorkerResult` documents, and complete the original heartbeat/specialist runtime job without Codex worker execution; `codex exec` survives only as explicit fallback in repo-birth startup. `epiphany-mvp-coordinator` now invokes `epiphany-openai-runtime run-worker` for launched runtime job ids and no longer creates a second coordinator-owned shadow runtime job/result as a compensator. `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 and now persists `EpiphanyRuntimeWorkerLaunchRequest` CultCache records keyed by runtime job id, with indexed kind/contract fields plus MessagePack-encoded typed launch documents instead of discarding work intent after queue receipt creation; `roleResult` and `reorientResult` now prefer typed worker result documents and fall back only to generic job summaries for older receipts; `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, launch request handoff, and rollout/session writeback for the compatibility route. Vendored `codex-core` no longer has `epiphany_distillation`, `epiphany_promotion`, `epiphany_proposal`, `epiphany_retrieval`, or `epiphany_rollout` husk modules; it re-exports native surfaces from `epiphany-core` directly, and the only rollout adapter left is the real host seam that supplies Codex's turn-boundary predicate. `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 richer live callers move. next_action: Move remaining coordinator/runtime execution and operator surfaces off app-server JSON-RPC where possible, then 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_read_routes.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_read_routes.rs index bc9a5c85..ee560d00 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_read_routes.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_read_routes.rs @@ -8,26 +8,20 @@ use codex_core::distill_observation; use codex_protocol::ThreadId; use epiphany_codex_bridge::context::map_epiphany_context; use epiphany_codex_bridge::context::map_epiphany_graph_query; -use epiphany_codex_bridge::context::map_epiphany_planning; -use epiphany_codex_bridge::coordinator::derive_epiphany_coordinator_status; -use epiphany_codex_bridge::coordinator::epiphany_reorient_finding_already_accepted; -use epiphany_codex_bridge::coordinator::map_epiphany_coordinator_view; -use epiphany_codex_bridge::coordinator::map_epiphany_crrc_recommendation; -use epiphany_codex_bridge::coordinator::map_epiphany_roles; -use epiphany_codex_bridge::coordinator::render_epiphany_roles_note; use epiphany_codex_bridge::jobs::map_epiphany_jobs; use epiphany_codex_bridge::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID; use epiphany_codex_bridge::launch::epiphany_role_binding_id; -use epiphany_codex_bridge::pressure::map_epiphany_pressure; use epiphany_codex_bridge::reorient::map_epiphany_freshness; -use epiphany_codex_bridge::reorient::map_epiphany_reorient; use epiphany_codex_bridge::retrieve::map_epiphany_retrieve_response; use epiphany_codex_bridge::runtime_results::load_epiphany_reorient_result_snapshot; use epiphany_codex_bridge::runtime_results::load_epiphany_role_result_snapshot; -use epiphany_codex_bridge::scene::map_core_epiphany_scene_action; -use epiphany_codex_bridge::scene::map_epiphany_scene; -use epiphany_core::EpiphanySceneInput; -use epiphany_core::derive_scene; +use epiphany_codex_bridge::view::EpiphanyViewResponseInput; +use epiphany_codex_bridge::view::default_epiphany_view_lenses; +use epiphany_codex_bridge::view::epiphany_view_needs_jobs; +use epiphany_codex_bridge::view::epiphany_view_needs_pressure; +use epiphany_codex_bridge::view::epiphany_view_needs_reorientation_inputs; +use epiphany_codex_bridge::view::epiphany_view_needs_runtime_store; +use epiphany_codex_bridge::view::map_epiphany_view_response; use super::CodexMessageProcessor; use super::ConnectionRequestId; @@ -43,16 +37,7 @@ impl CodexMessageProcessor { ) { let ThreadEpiphanyViewParams { thread_id, lenses } = params; let lenses = if lenses.is_empty() { - vec![ - ThreadEpiphanyViewLens::Scene, - ThreadEpiphanyViewLens::Jobs, - ThreadEpiphanyViewLens::Roles, - ThreadEpiphanyViewLens::Planning, - ThreadEpiphanyViewLens::Pressure, - ThreadEpiphanyViewLens::Reorient, - ThreadEpiphanyViewLens::Crrc, - ThreadEpiphanyViewLens::Coordinator, - ] + default_epiphany_view_lenses() } else { lenses }; @@ -80,16 +65,9 @@ impl CodexMessageProcessor { } }; - let needs_jobs = lenses.contains(&ThreadEpiphanyViewLens::Jobs) - || lenses.contains(&ThreadEpiphanyViewLens::Roles) - || lenses.contains(&ThreadEpiphanyViewLens::Crrc) - || lenses.contains(&ThreadEpiphanyViewLens::Coordinator); - let needs_reorientation_inputs = lenses.contains(&ThreadEpiphanyViewLens::Roles) - || lenses.contains(&ThreadEpiphanyViewLens::Reorient) - || lenses.contains(&ThreadEpiphanyViewLens::Crrc) - || lenses.contains(&ThreadEpiphanyViewLens::Coordinator); - let needs_pressure = - lenses.contains(&ThreadEpiphanyViewLens::Pressure) || needs_reorientation_inputs; + let needs_jobs = epiphany_view_needs_jobs(&lenses); + let needs_reorientation_inputs = epiphany_view_needs_reorientation_inputs(&lenses); + let needs_pressure = epiphany_view_needs_pressure(&lenses); let retrieval_override = if (needs_jobs || needs_reorientation_inputs) && thread .epiphany_state @@ -134,40 +112,7 @@ impl CodexMessageProcessor { } else { None }; - let pressure = needs_pressure.then(|| map_epiphany_pressure(token_usage_info.as_ref())); - let freshness = needs_reorientation_inputs.then(|| { - map_epiphany_freshness( - thread.epiphany_state.as_ref(), - retrieval_override.as_ref(), - watcher_snapshot - .as_ref() - .map(epiphany_freshness_watcher_snapshot), - ) - }); - let (state_revision, reorient_state_status, reorient_decision) = - if let (Some((state_revision, retrieval, graph, watcher)), Some(pressure)) = - (freshness.as_ref(), pressure.as_ref()) - { - let (state_status, decision) = map_epiphany_reorient( - thread.epiphany_state.as_ref(), - pressure, - retrieval, - graph, - watcher, - ); - (*state_revision, state_status, Some(decision)) - } else { - (None, ThreadEpiphanyReorientStateStatus::Missing, None) - }; - let jobs = if needs_jobs { - map_epiphany_jobs(thread.epiphany_state.as_ref(), retrieval_override.as_ref()) - } else { - Vec::new() - }; - let runtime_store_path = if lenses.contains(&ThreadEpiphanyViewLens::Roles) - || lenses.contains(&ThreadEpiphanyViewLens::Crrc) - || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) - { + let runtime_store_path = if epiphany_view_needs_runtime_store(&lenses) { if let Some(loaded_thread) = loaded_thread.as_ref() { Some(loaded_thread.epiphany_runtime_spine_store_path().await) } else { @@ -176,220 +121,19 @@ impl CodexMessageProcessor { } else { None }; - let reorient_job = jobs - .iter() - .find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID) - .cloned(); - let (reorient_result_status, reorient_finding, reorient_result_note) = if runtime_store_path - .is_some() - || lenses.contains(&ThreadEpiphanyViewLens::Crrc) - || lenses.contains(&ThreadEpiphanyViewLens::Coordinator) - { - load_epiphany_reorient_result_snapshot( - thread.epiphany_state.as_ref(), - runtime_store_path.as_deref(), - EPIPHANY_REORIENT_LAUNCH_BINDING_ID, - ) - .await - } else { - ( - ThreadEpiphanyReorientResultStatus::MissingState, - None, - "Reorient result was not requested.".to_string(), - ) - }; - let checkpoint_present = thread - .epiphany_state - .as_ref() - .and_then(|state| state.investigation_checkpoint.as_ref()) - .is_some(); - let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| { - thread - .epiphany_state - .as_ref() - .is_some_and(|state| epiphany_reorient_finding_already_accepted(state, finding)) - }); - let recommendation = if let (Some(pressure), Some(decision)) = - (pressure.as_ref(), reorient_decision.as_ref()) - { - Some(map_epiphany_crrc_recommendation( - loaded, - reorient_state_status, - pressure, - decision, - reorient_result_status, - checkpoint_present, - reorient_finding.is_some(), - reorient_finding_accepted, - )) - } else { - None - }; - let roles = if let (Some(pressure), Some(decision), Some(recommendation)) = ( - pressure.as_ref(), - reorient_decision.as_ref(), - recommendation.as_ref(), - ) { - Some(map_epiphany_roles( - thread.epiphany_state.as_ref(), - &jobs, - decision, - pressure, - recommendation, - reorient_result_status, - reorient_job.as_ref(), - )) - } else { - None - }; - let coordinator_response = if lenses.contains(&ThreadEpiphanyViewLens::Coordinator) { - if let (Some(pressure), Some(recommendation), Some(roles)) = - (pressure.as_ref(), recommendation.as_ref(), roles.clone()) - { - let status = derive_epiphany_coordinator_status( - thread.epiphany_state.as_ref(), - runtime_store_path.as_deref(), - reorient_state_status, - pressure, - recommendation, - roles, - reorient_decision.as_ref(), - reorient_result_status, - reorient_finding.as_ref(), - checkpoint_present, - ) - .await; - Some(map_epiphany_coordinator_view( - thread_id.clone(), - loaded, - reorient_state_status, - state_revision, - status, - )) - } else { - None - } - } else { - None - }; - - let response = ThreadEpiphanyViewResponse { + let response = map_epiphany_view_response(EpiphanyViewResponseInput { thread_id: thread_id.clone(), - scene: lenses.contains(&ThreadEpiphanyViewLens::Scene).then(|| { - map_epiphany_scene( - thread.epiphany_state.as_ref(), - loaded, - EPIPHANY_REORIENT_LAUNCH_BINDING_ID, - ) - }), - jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) { - jobs.clone() - } else { - Vec::new() - }, - roles: lenses.contains(&ThreadEpiphanyViewLens::Roles).then(|| { - let roles = roles.clone().unwrap_or_default(); - ThreadEpiphanyViewRoles { - thread_id: thread_id.clone(), - source: if loaded { - ThreadEpiphanyRolesSource::Live - } else { - ThreadEpiphanyRolesSource::Stored - }, - state_status: reorient_state_status, - state_revision, - note: render_epiphany_roles_note( - &roles, - reorient_state_status, - recommendation - .as_ref() - .map(|recommendation| recommendation.action) - .unwrap_or(ThreadEpiphanyCrrcAction::Continue), - ), - roles, - } - }), - planning: lenses.contains(&ThreadEpiphanyViewLens::Planning).then(|| { - let (state_status, state_revision, planning, summary) = - map_epiphany_planning(thread.epiphany_state.as_ref()); - ThreadEpiphanyViewPlanning { - thread_id: thread_id.clone(), - source: if loaded { - ThreadEpiphanyContextSource::Live - } else { - ThreadEpiphanyContextSource::Stored - }, - state_status, - state_revision, - planning, - summary, - } - }), - pressure: lenses - .contains(&ThreadEpiphanyViewLens::Pressure) - .then(|| pressure.clone()) - .flatten(), - reorient: lenses - .contains(&ThreadEpiphanyViewLens::Reorient) - .then(|| { - reorient_decision - .clone() - .map(|decision| ThreadEpiphanyViewReorient { - thread_id: thread_id.clone(), - source: if loaded { - ThreadEpiphanyReorientSource::Live - } else { - ThreadEpiphanyReorientSource::Stored - }, - state_status: reorient_state_status, - state_revision, - decision, - }) - }) - .flatten(), - crrc: lenses - .contains(&ThreadEpiphanyViewLens::Crrc) - .then(|| { - let pressure = pressure.clone()?; - let decision = reorient_decision.clone()?; - let recommendation = recommendation.clone()?; - let available_actions = derive_scene(EpiphanySceneInput { - state: thread.epiphany_state.as_ref(), - loaded, - reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID, - }) - .available_actions - .into_iter() - .map(map_core_epiphany_scene_action) - .collect(); - let note = format!( - "{} Result status: {:?}. {}", - recommendation.reason, reorient_result_status, reorient_result_note - ); - Some(ThreadEpiphanyViewCrrc { - thread_id: thread_id.clone(), - source: if loaded { - ThreadEpiphanyReorientSource::Live - } else { - ThreadEpiphanyReorientSource::Stored - }, - state_status: reorient_state_status, - state_revision, - pressure, - decision, - recommendation, - reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(), - reorient_result_status, - reorient_job: reorient_job.clone(), - reorient_finding: reorient_finding.clone(), - available_actions, - note, - }) - }) - .flatten(), - coordinator: coordinator_response, lenses, - }; + loaded, + state: thread.epiphany_state.as_ref(), + retrieval_override: retrieval_override.as_ref(), + watcher_snapshot: watcher_snapshot + .as_ref() + .map(epiphany_freshness_watcher_snapshot), + token_usage_info: token_usage_info.as_ref(), + runtime_store_path: runtime_store_path.as_deref(), + }) + .await; self.outgoing.send_response(request_id, response).await; }