diff --git a/epiphany-codex-bridge/src/mutation.rs b/epiphany-codex-bridge/src/mutation.rs index f2207695..39a1adbc 100644 --- a/epiphany-codex-bridge/src/mutation.rs +++ b/epiphany-codex-bridge/src/mutation.rs @@ -9,10 +9,40 @@ use codex_protocol::protocol::EpiphanyCodeRef; use codex_protocol::protocol::EpiphanyInvestigationCheckpoint; use codex_protocol::protocol::EpiphanyInvestigationDisposition; use codex_protocol::protocol::EpiphanyScratchPad; +use epiphany_core::EpiphanyReorientAcceptanceFinding; +use epiphany_core::EpiphanyRoleAcceptanceFinding; use epiphany_core::EpiphanyRoleStatePatchDocument; +use epiphany_core::build_reorient_acceptance_bundle; +use epiphany_core::build_role_acceptance_bundle; use epiphany_core::imagination_role_state_patch_policy_errors; use epiphany_core::modeling_role_state_patch_policy_errors; +use crate::results::map_core_role_result_role_id; +use crate::runtime_results::reorient_finding_runtime_job_id; +use crate::runtime_results::reorient_finding_runtime_result_id; +use crate::runtime_results::role_finding_runtime_job_id; +use crate::runtime_results::role_finding_runtime_result_id; + +pub struct RoleAcceptanceUpdate { + pub patch: ThreadEpiphanyUpdatePatch, + pub changed_fields: Vec, + pub accepted_receipt_id: String, + pub accepted_observation_id: String, + pub accepted_evidence_id: String, +} + +pub struct ReorientAcceptanceUpdate { + pub scratch: Option, + pub investigation_checkpoint: Option, + pub receipt: codex_protocol::protocol::EpiphanyAcceptanceReceipt, + pub observation: codex_protocol::protocol::EpiphanyObservation, + pub evidence: codex_protocol::protocol::EpiphanyEvidenceRecord, + pub changed_fields: Vec, + pub accepted_receipt_id: String, + pub accepted_observation_id: String, + pub accepted_evidence_id: String, +} + pub fn parse_role_finding_state_patch( finding: &ThreadEpiphanyRoleFinding, ) -> Result { @@ -74,6 +104,141 @@ pub fn epiphany_imagination_finding_has_reviewable_state_patch( .is_some_and(|patch| imagination_role_accept_patch_errors(patch).is_empty()) } +pub fn build_role_acceptance_update( + role_id: ThreadEpiphanyRoleId, + binding_id: &str, + finding: &ThreadEpiphanyRoleFinding, + accepted_evidence_id: String, + accepted_observation_id: String, + accepted_at: String, +) -> Result { + let mut patch = match role_id { + ThreadEpiphanyRoleId::Imagination => { + let patch = parse_role_finding_state_patch(finding)?; + let patch_errors = imagination_role_accept_patch_errors(&patch); + if !patch_errors.is_empty() { + return Err(format!( + "imagination role state patch is not acceptable: {}", + patch_errors.join("; ") + )); + } + patch + } + ThreadEpiphanyRoleId::Modeling => { + let patch = parse_role_finding_state_patch(finding)?; + let patch_errors = modeling_role_accept_patch_errors(&patch); + if !patch_errors.is_empty() { + return Err(format!( + "modeling role state patch is not acceptable: {}", + patch_errors.join("; ") + )); + } + patch + } + ThreadEpiphanyRoleId::Verification => ThreadEpiphanyUpdatePatch::default(), + ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { + return Err(format!( + "role {:?} cannot be accepted through roleAccept", + role_id + )); + } + }; + + let projected_fields = epiphany_update_patch_changed_fields(&patch) + .into_iter() + .map(|field| format!("{field:?}")) + .collect(); + let acceptance_bundle = build_role_acceptance_bundle( + binding_id, + EpiphanyRoleAcceptanceFinding { + role_id: map_core_role_result_role_id(role_id), + verdict: finding.verdict.clone(), + summary: finding.summary.clone(), + next_safe_move: finding.next_safe_move.clone(), + files_inspected: finding.files_inspected.clone(), + runtime_result_id: role_finding_runtime_result_id(finding), + runtime_job_id: role_finding_runtime_job_id(finding), + projected_fields, + }, + accepted_evidence_id, + accepted_observation_id, + accepted_at, + )?; + let accepted_receipt_id = acceptance_bundle.accepted_receipt_id.clone(); + let accepted_observation_id = acceptance_bundle.accepted_observation_id.clone(); + let accepted_evidence_id = acceptance_bundle.accepted_evidence_id.clone(); + patch.evidence.push(acceptance_bundle.evidence); + patch.observations.push(acceptance_bundle.observation); + patch.acceptance_receipts.push(acceptance_bundle.receipt); + let changed_fields = epiphany_update_patch_changed_fields(&patch); + + Ok(RoleAcceptanceUpdate { + patch, + changed_fields, + accepted_receipt_id, + accepted_observation_id, + accepted_evidence_id, + }) +} + +pub fn build_reorient_acceptance_update( + binding_id: &str, + finding: &ThreadEpiphanyReorientFinding, + accepted_evidence_id: String, + accepted_observation_id: String, + accepted_at: String, + update_scratch: bool, + update_investigation_checkpoint: bool, + checkpoint: Option, +) -> Result { + let acceptance_bundle = build_reorient_acceptance_bundle( + binding_id, + EpiphanyReorientAcceptanceFinding { + mode: finding.mode.clone(), + summary: finding.summary.clone(), + next_safe_move: finding.next_safe_move.clone(), + checkpoint_still_valid: finding.checkpoint_still_valid, + files_inspected: finding.files_inspected.clone(), + runtime_result_id: reorient_finding_runtime_result_id(finding), + runtime_job_id: reorient_finding_runtime_job_id(finding), + }, + accepted_evidence_id, + accepted_observation_id, + accepted_at, + update_scratch, + update_investigation_checkpoint.then_some(checkpoint).flatten(), + )?; + let accepted_receipt_id = acceptance_bundle.accepted_receipt_id.clone(); + let accepted_observation_id = acceptance_bundle.accepted_observation_id.clone(); + let accepted_evidence_id = acceptance_bundle.accepted_evidence_id.clone(); + let scratch = acceptance_bundle.scratch; + let investigation_checkpoint = acceptance_bundle.investigation_checkpoint; + + let mut changed_fields = vec![ + ThreadEpiphanyStateUpdatedField::AcceptanceReceipts, + ThreadEpiphanyStateUpdatedField::Observations, + ThreadEpiphanyStateUpdatedField::Evidence, + ]; + if scratch.is_some() { + changed_fields.push(ThreadEpiphanyStateUpdatedField::Scratch); + } + if investigation_checkpoint.is_some() { + changed_fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint); + } + + Ok(ReorientAcceptanceUpdate { + scratch, + investigation_checkpoint, + receipt: acceptance_bundle.receipt, + observation: acceptance_bundle.observation, + evidence: acceptance_bundle.evidence, + changed_fields, + accepted_receipt_id, + accepted_observation_id, + accepted_evidence_id, + }) +} + pub fn role_finding_summary(finding: &ThreadEpiphanyRoleFinding) -> String { let summary = finding .summary diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 7caeba95..456645a5 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -515,14 +515,23 @@ longer hand-build duplicate `ThreadEpiphanyJob` fallback structs inside `epiphany_mutation_routes.rs`. The route module is about 1,245 lines. Small cut, real smell: duplicate projection authority belonged with the job adapter, not in each JSON-RPC launch handler. +The twenty-fifth cut moved role/reorient acceptance update bundle construction +into `epiphany-codex-bridge/src/mutation.rs`. Role accept now delegates +state-patch validation, projected-field capture, acceptance receipt/evidence/ +observation assembly, and changed-field derivation to the bridge; reorient +accept delegates scratch/checkpoint/evidence bundle construction the same way. +`epiphany_mutation_routes.rs` is about 1,143 lines. The route still performs +Codex thread lookup, revisioned state writes, and JSON-RPC responses, but no +longer owns the acceptance document contract. 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. -Continue with the actual whale-carcass cut: carve route-level mutation -orchestration into explicit typed bridge/service calls, starting with launch -response/notification construction and acceptance update bundles. Success is a +Continue with the actual whale-carcass cut: carve remaining route-level mutation +orchestration into explicit typed bridge/service calls, starting with generic +update/promote patch-to-state-update construction and launch response/ +notification shaping. Success is a visibly smaller processor and less mutation authority in Codex, not merely nicer payload names or another adapter reliquary. Do not resume Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, marketplace, or bridge expansion diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 3a6e7b40..7342a6fa 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 8a3e869b..480910c5 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -278,5 +278,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, and now a real physical extraction: `epiphany-codex-bridge` lives outside `vendor/codex` and owns the 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,245 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader after moving typed mutation helpers, reviewability checks, completed runtime-result acceptance loading, live-state projection, and launched-job fallback projection out to the bridge. This is progress, not absolution: the bridge is still Codex-facing compatibility, not the final CultNet runtime, and route-level launch/accept/update/promote/interrupt orchestration still lives inside the Codex processor. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. - next_action: Continue Codex starvation by carving route-level mutation orchestration into explicit outside-vendor service calls, starting with launch response/notification construction and acceptance update bundles that can take small typed inputs instead of borrowing `CodexMessageProcessor`. Do not resume app/skill/marketplace expansion. + summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, and now a real physical extraction: `epiphany-codex-bridge` lives outside `vendor/codex` and owns the 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,143 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader after moving typed mutation helpers, reviewability checks, completed runtime-result acceptance loading, live-state projection, launched-job fallback projection, and role/reorient acceptance update bundle construction out to the bridge. This is progress, not absolution: the bridge is still Codex-facing compatibility, not the final CultNet runtime, and route-level launch/update/promote/interrupt orchestration still lives inside the Codex processor. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. + next_action: Continue Codex starvation by carving remaining route-level mutation orchestration into explicit outside-vendor service calls, starting with generic update/promote patch-to-state-update construction and launch response/notification shaping. Do not resume app/skill/marketplace expansion. 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 8daceb22..e2d690dd 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 @@ -18,29 +18,19 @@ use epiphany_codex_bridge::launch::build_epiphany_role_launch_request; use epiphany_codex_bridge::launch::epiphany_role_binding_id; use epiphany_codex_bridge::launch::epiphany_role_label; 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_update_patch_changed_fields; -use epiphany_codex_bridge::mutation::imagination_role_accept_patch_errors; -use epiphany_codex_bridge::mutation::modeling_role_accept_patch_errors; -use epiphany_codex_bridge::mutation::parse_role_finding_state_patch; use epiphany_codex_bridge::mutation::thread_epiphany_patch_has_state_replacements; 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::results::map_core_role_result_role_id; use epiphany_codex_bridge::retrieve::map_epiphany_retrieve_index_summary; use epiphany_codex_bridge::runtime_results::load_completed_epiphany_reorient_finding; use epiphany_codex_bridge::runtime_results::load_completed_epiphany_role_finding; -use epiphany_codex_bridge::runtime_results::reorient_finding_runtime_job_id; -use epiphany_codex_bridge::runtime_results::reorient_finding_runtime_result_id; -use epiphany_codex_bridge::runtime_results::role_finding_runtime_job_id; -use epiphany_codex_bridge::runtime_results::role_finding_runtime_result_id; use epiphany_codex_bridge::state::client_visible_live_thread_epiphany_state; -use epiphany_core::EpiphanyReorientAcceptanceFinding; -use epiphany_core::EpiphanyRoleAcceptanceFinding; -use epiphany_core::build_reorient_acceptance_bundle; -use epiphany_core::build_role_acceptance_bundle; use uuid::Uuid; use super::CodexMessageProcessor; @@ -251,98 +241,28 @@ impl CodexMessageProcessor { } }; - let mut patch = match role_id { - ThreadEpiphanyRoleId::Imagination => { - let patch = match parse_role_finding_state_patch(&finding) { - Ok(patch) => patch, - Err(message) => { - self.send_invalid_request_error(request_id, message).await; - return; - } - }; - let patch_errors = imagination_role_accept_patch_errors(&patch); - if !patch_errors.is_empty() { - self.send_invalid_request_error( - request_id, - format!( - "imagination role state patch is not acceptable: {}", - patch_errors.join("; ") - ), - ) - .await; - return; - } - patch - } - ThreadEpiphanyRoleId::Modeling => { - let patch = match parse_role_finding_state_patch(&finding) { - Ok(patch) => patch, - Err(message) => { - self.send_invalid_request_error(request_id, message).await; - return; - } - }; - let patch_errors = modeling_role_accept_patch_errors(&patch); - if !patch_errors.is_empty() { - self.send_invalid_request_error( - request_id, - format!( - "modeling role state patch is not acceptable: {}", - patch_errors.join("; ") - ), - ) - .await; - return; - } - patch - } - ThreadEpiphanyRoleId::Verification => ThreadEpiphanyUpdatePatch::default(), - ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => { - self.send_invalid_request_error( - request_id, - format!("role {:?} cannot be accepted through roleAccept", role_id), - ) - .await; - return; - } - }; - let accepted_prefix = epiphany_role_label(role_id); let accepted_evidence_id = format!("ev-{accepted_prefix}-{}", Uuid::new_v4()); let accepted_observation_id = format!("obs-{accepted_prefix}-{}", Uuid::new_v4()); - let projected_fields = epiphany_update_patch_changed_fields(&patch) - .into_iter() - .map(|field| format!("{field:?}")) - .collect(); - let acceptance_bundle = match build_role_acceptance_bundle( + let acceptance_update = match build_role_acceptance_update( + role_id, &binding_id, - EpiphanyRoleAcceptanceFinding { - role_id: map_core_role_result_role_id(role_id), - verdict: finding.verdict.clone(), - summary: finding.summary.clone(), - next_safe_move: finding.next_safe_move.clone(), - files_inspected: finding.files_inspected.clone(), - runtime_result_id: role_finding_runtime_result_id(&finding), - runtime_job_id: role_finding_runtime_job_id(&finding), - projected_fields, - }, + &finding, accepted_evidence_id, accepted_observation_id, Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), ) { - Ok(bundle) => bundle, + Ok(update) => update, Err(message) => { self.send_invalid_request_error(request_id, message).await; return; } }; - let accepted_receipt_id = acceptance_bundle.accepted_receipt_id.clone(); - let accepted_observation_id = acceptance_bundle.accepted_observation_id.clone(); - let accepted_evidence_id = acceptance_bundle.accepted_evidence_id.clone(); - patch.evidence.push(acceptance_bundle.evidence); - patch.observations.push(acceptance_bundle.observation); - patch.acceptance_receipts.push(acceptance_bundle.receipt); - let changed_fields = epiphany_update_patch_changed_fields(&patch); + let accepted_receipt_id = acceptance_update.accepted_receipt_id.clone(); + let accepted_observation_id = acceptance_update.accepted_observation_id.clone(); + let accepted_evidence_id = acceptance_update.accepted_evidence_id.clone(); + let changed_fields = acceptance_update.changed_fields.clone(); + let patch = acceptance_update.patch; let applied_patch = patch.clone(); let epiphany_state = match loaded_thread @@ -669,57 +589,35 @@ impl CodexMessageProcessor { let accepted_evidence_id = format!("ev-reorient-{}", Uuid::new_v4()); let accepted_observation_id = format!("obs-reorient-{}", Uuid::new_v4()); - let acceptance_bundle = match build_reorient_acceptance_bundle( + let acceptance_update = match build_reorient_acceptance_update( &binding_id, - EpiphanyReorientAcceptanceFinding { - mode: finding.mode.clone(), - summary: finding.summary.clone(), - next_safe_move: finding.next_safe_move.clone(), - checkpoint_still_valid: finding.checkpoint_still_valid, - files_inspected: finding.files_inspected.clone(), - runtime_result_id: reorient_finding_runtime_result_id(&finding), - runtime_job_id: reorient_finding_runtime_job_id(&finding), - }, + &finding, accepted_evidence_id, accepted_observation_id, Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), update_scratch, - update_investigation_checkpoint - .then(|| state.investigation_checkpoint.clone()) - .flatten(), + update_investigation_checkpoint, + state.investigation_checkpoint.clone(), ) { - Ok(bundle) => bundle, + Ok(update) => update, Err(message) => { self.send_invalid_request_error(request_id, message).await; return; } }; - let accepted_receipt_id = acceptance_bundle.accepted_receipt_id.clone(); - let accepted_observation_id = acceptance_bundle.accepted_observation_id.clone(); - let accepted_evidence_id = acceptance_bundle.accepted_evidence_id.clone(); - let scratch = acceptance_bundle.scratch; - let investigation_checkpoint = acceptance_bundle.investigation_checkpoint; - - let mut changed_fields = vec![ - ThreadEpiphanyStateUpdatedField::AcceptanceReceipts, - ThreadEpiphanyStateUpdatedField::Observations, - ThreadEpiphanyStateUpdatedField::Evidence, - ]; - if scratch.is_some() { - changed_fields.push(ThreadEpiphanyStateUpdatedField::Scratch); - } - if investigation_checkpoint.is_some() { - changed_fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint); - } + let accepted_receipt_id = acceptance_update.accepted_receipt_id.clone(); + let accepted_observation_id = acceptance_update.accepted_observation_id.clone(); + let accepted_evidence_id = acceptance_update.accepted_evidence_id.clone(); + let changed_fields = acceptance_update.changed_fields.clone(); let epiphany_state = match loaded_thread .epiphany_update_state(EpiphanyStateUpdate { expected_revision, - scratch, - investigation_checkpoint, - acceptance_receipts: vec![acceptance_bundle.receipt], - observations: vec![acceptance_bundle.observation], - evidence: vec![acceptance_bundle.evidence], + scratch: acceptance_update.scratch, + investigation_checkpoint: acceptance_update.investigation_checkpoint, + acceptance_receipts: vec![acceptance_update.receipt], + observations: vec![acceptance_update.observation], + evidence: vec![acceptance_update.evidence], ..Default::default() }) .await