From 0503ca1c5c91c33ef03e3500e1a5546856957f63 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 9 Jun 2026 05:50:12 +0100 Subject: [PATCH] Move distill propose route policy into bridge --- epiphany-codex-bridge/src/view_protocol.rs | 31 ++++++++++++ notes/epiphany-current-algorithmic-map.md | 39 +++++++++------ notes/fresh-workspace-handoff.md | 9 +++- state/ledgers.cc | Bin 179780 -> 180255 bytes state/map.yaml | 2 +- .../epiphany_read_routes.rs | 46 ++++++------------ 6 files changed, 80 insertions(+), 47 deletions(-) diff --git a/epiphany-codex-bridge/src/view_protocol.rs b/epiphany-codex-bridge/src/view_protocol.rs index 343ddf60..88d0858b 100644 --- a/epiphany-codex-bridge/src/view_protocol.rs +++ b/epiphany-codex-bridge/src/view_protocol.rs @@ -35,8 +35,11 @@ use crate::coordinator_protocol::protocol_crrc_recommendation; use crate::coordinator_protocol::protocol_role_board_lanes; use crate::cultnet::EpiphanyFreshnessSurface; use crate::cultnet::EpiphanySurfaceSource; +use crate::error::EpiphanyBridgeError; +use crate::error::Result as BridgeResult; use crate::jobs::map_epiphany_jobs; use crate::pressure::derive_epiphany_pressure; +use crate::protocol_edge::protocol_distill_params_to_core; use crate::protocol_edge::protocol_job_from_surface; use crate::protocol_edge::protocol_pressure_from_core; use crate::protocol_edge::protocol_reorient_decision; @@ -570,6 +573,19 @@ pub fn map_epiphany_distill_response( }) } +pub fn map_thread_epiphany_distill_response( + params: ThreadEpiphanyDistillParams, + state: Option<&EpiphanyThreadState>, +) -> BridgeResult { + let expected_revision = state.map(|state| state.revision).unwrap_or(0); + let input = protocol_distill_params_to_core(params); + map_epiphany_distill_response(expected_revision, input).map_err(|err| { + EpiphanyBridgeError::InvalidRequest(format!( + "failed to distill Epiphany observation: {err}" + )) + }) +} + pub fn map_epiphany_propose_response( state: EpiphanyThreadState, observation_ids: Vec, @@ -593,3 +609,18 @@ pub fn map_epiphany_propose_response( }, }) } + +pub fn map_thread_epiphany_propose_response( + params: ThreadEpiphanyProposeParams, + state: Option, +) -> BridgeResult { + let thread_id = params.thread_id.clone(); + let Some(state) = state else { + return Err(EpiphanyBridgeError::InvalidRequest(format!( + "thread has no Epiphany state: {thread_id}" + ))); + }; + map_epiphany_propose_response(state, params.observation_ids).map_err(|err| { + EpiphanyBridgeError::InvalidRequest(format!("failed to propose Epiphany map update: {err}")) + }) +} diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 39be1087..426937bc 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -752,9 +752,16 @@ The app-server handler: 1. parses the thread id. 2. requires the thread to be loaded. -3. reads the current Epiphany revision, defaulting to `0` if the thread has not yet created state. -4. calls `distill_observation`. -5. returns a `ThreadEpiphanyUpdatePatch` containing one observation and one evidence record. +3. reads the current Epiphany state as a host fact. +4. calls `epiphany-codex-bridge::view_protocol::map_thread_epiphany_distill_response`. +5. maps bridge invalid-request errors to JSON-RPC errors. + +`map_thread_epiphany_distill_response`: + +1. chooses `expectedRevision` from the supplied Epiphany state, defaulting to `0` if the thread has not yet created state. +2. converts the Codex protocol params into the core distillation input. +3. calls `distill_observation`. +4. returns a `ThreadEpiphanyUpdatePatch` containing one observation and one evidence record. `distill_observation`: @@ -768,9 +775,8 @@ The app-server handler: Code refs: -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4173) -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4202) -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4225) +- [epiphany_read_routes.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_read_routes.rs:479) +- [view_protocol.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/view_protocol.rs:576) - [epiphany-core/src/distillation.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/distillation.rs:28) ### Output @@ -794,7 +800,7 @@ A client sends experimental `thread/epiphany/propose` for a loaded thread with: Proposal is the tracing-paper path. It starts from observations that are already in the thread's typed state, chooses a small proposal-ready set when `observationIds` is omitted, requires selected observations to have verified/accepted status, requires each selected observation to cite accepting `recent_evidence`, requires source-grounding through code refs, and returns a candidate patch that can replace graph/frontier/churn fields only if a later promotion accepts it. -This is the first shipped chewing motion. It is deliberately small: no model call, no watcher invalidation, no automatic graph truth. When ids are omitted, proposal now ranks proposal-ready observations by source/evidence strength, code refs, and current map focus, then keeps a bounded coherent path cluster instead of grabbing every shiny pebble in the yard. It then checks that selected observations are backed by current accepting evidence, scores the selected observations so the strongest verifier/test/smoke-backed signal becomes the proposal's primary summary, and tries to match observed code refs against existing architecture nodes. Concrete matching still wins: exact code refs, same-path refs, and deterministic path-node ids are tried before anything semantic. Only after those fail can strict semantic overlap reuse an existing architecture node, and only when that graph node has no code refs yet; anchored nodes stay governed by concrete refs because otherwise the map starts doing interpretive dance in a trench coat. Proposal then enriches matching nodes with newly observed refs, creates path-derived candidate nodes only for genuinely unmapped surfaces, expands frontier focus through graph links, marks named incident edges active, emits a proposal observation/evidence pair, and judges the map delta by match kind: exact-ref reuse is a narrow refinement, same-path reuse is broadening, deterministic-id reuse preserves a known candidate, semantic reuse anchors previously unanchored prose, and new nodes expand the map. Diff pressure now reflects that judgment, touched paths, observation count, and unresolved write risk instead of blindly inheriting the previous pressure. It is a candidate, not an oracle. We do not crown the goblin because it found a wrench. +This is the first shipped chewing motion. It is deliberately small: no model call, no watcher invalidation, no automatic graph truth. When ids are omitted, proposal now ranks proposal-ready observations by source/evidence strength, code refs, and current map focus, then keeps a bounded coherent path cluster instead of grabbing every shiny pebble in the yard. It then checks that selected observations are backed by current accepting evidence, scores the selected observations so the strongest verifier/test/smoke-backed signal becomes the proposal's primary summary, and tries to match observed code refs against existing architecture nodes. Concrete matching still wins: exact code refs, same-path refs, and deterministic path-node ids are tried before anything semantic. Only after those fail can strict semantic overlap reuse an existing architecture node, and only when that graph node has no code refs yet; anchored nodes stay governed by concrete refs because otherwise the map starts doing interpretive dance in a trench coat. Proposal then enriches matching nodes with newly observed refs, creates path-derived candidate nodes only for genuinely unmapped surfaces, expands frontier focus through graph links, marks named incident edges active, emits a proposal observation/evidence pair, and judges the map delta by match kind: exact-ref reuse is a narrow refinement, same-path reuse is broadening, deterministic-id reuse preserves a known candidate, semantic reuse anchors previously unanchored prose, and new nodes expand the map. Diff pressure now reflects that judgment, touched paths, observation count, and unresolved write risk instead of blindly inheriting the previous pressure. It is a candidate, not an oracle. Protocol shape: @@ -823,10 +829,16 @@ The app-server handler: 1. parses the thread id. 2. requires the thread to be loaded. -3. requires existing Epiphany state. -4. reads the current state revision as `expectedRevision`. -5. calls `propose_map_update`. -6. returns a `ThreadEpiphanyUpdatePatch` containing proposal observation/evidence, replacement graphs/frontier, and churn. +3. reads the current Epiphany state as a host fact. +4. calls `epiphany-codex-bridge::view_protocol::map_thread_epiphany_propose_response`. +5. maps bridge invalid-request errors to JSON-RPC errors. + +`map_thread_epiphany_propose_response`: + +1. requires existing Epiphany state. +2. reads the current state revision as `expectedRevision`. +3. calls `propose_map_update`. +4. returns a `ThreadEpiphanyUpdatePatch` containing proposal observation/evidence, replacement graphs/frontier, and churn. `propose_map_update`: @@ -853,9 +865,8 @@ The app-server handler: Code refs: -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4244) -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4287) -- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4301) +- [epiphany_read_routes.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor/epiphany_read_routes.rs:528) +- [view_protocol.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/view_protocol.rs:613) - [epiphany-core/src/proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:115) - [epiphany-core/src/proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:244) - [epiphany-core/src/proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:271) diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 5139f487..ed89ca96 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -840,7 +840,7 @@ loading and watcher registration because those are host lifecycle seams. propose patch-response builders have followed into the same bridge view module; app-server still checks that the target thread is loaded and maps errors onto JSON-RPC responses, but it no longer shapes those patches itself. -`epiphany_read_routes.rs` is about 579 lines after this cut. +`epiphany_read_routes.rs` is about 570 lines after this latest cut. The retrieve read route has now lost its local normalization/projection opinion too. `epiphany-codex-bridge::retrieve_protocol::retrieve_thread_epiphany_for_paths` @@ -849,6 +849,13 @@ retrieve response projection. App-server keeps thread-id parsing, loaded-thread lookup, config/Codex-home fact supply, response emission, and bridge-error to JSON-RPC mapping. +Distill/propose route response policy is bridge-owned too. +`map_thread_epiphany_distill_response` and +`map_thread_epiphany_propose_response` own expected-revision selection, +missing-state rejection, protocol-to-core input conversion, candidate patch +construction, and invalid-request text. App-server supplies the loaded thread +state as a host fact and maps bridge errors to JSON-RPC responses. + 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.cc b/state/ledgers.cc index b2d7304b8f8876427f170422b2a818ed10b1dd22..e3460e78afa5a825e636e95cd607dea7742f89c7 100644 GIT binary patch delta 381 zcmXw#ze)o^5XRlBB!OV51YtxgaWzE{6DzT?G-4%!tap=SWwU$P**nuIr0@YkxY7p@ ziC|%&q!ORNMk`z2A%%M}>E_Qj-~49YCx!dl;?oK%)auo}vyX*fJb7McUB*iNVQ@DL z*vl{oMz5J}u*JeIuhdQ})q1ryXpG96-|XQqaS%yzQmLKTn%EG6MGV>dxh>#*mt#wROhk87-0nth2krE+L9 H7zZ1FO1g|d delta 40 wcmbQ=zLD`{GV(T0Won response, + Err(EpiphanyBridgeError::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; + return; + } Err(err) => { - self.send_invalid_request_error( - request_id, - format!("failed to distill Epiphany observation: {err}"), - ) - .await; + self.send_internal_error(request_id, err.to_string()).await; return; } }; @@ -552,25 +546,15 @@ impl CodexMessageProcessor { } }; - let state = match thread.epiphany_state().await { - Some(state) => state, - None => { - self.send_invalid_request_error( - request_id, - format!("thread has no Epiphany state: {thread_uuid}"), - ) - .await; + let state = thread.epiphany_state().await; + let response = match map_thread_epiphany_propose_response(params, state) { + Ok(response) => response, + Err(EpiphanyBridgeError::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; return; } - }; - let response = match map_epiphany_propose_response(state, params.observation_ids) { - Ok(response) => response, Err(err) => { - self.send_invalid_request_error( - request_id, - format!("failed to propose Epiphany map update: {err}"), - ) - .await; + self.send_internal_error(request_id, err.to_string()).await; return; } };