Accept typed runtime findings

This commit is contained in:
Meta Vens 2026-05-20 01:39:27 +01:00
commit 24d229983b
6 changed files with 95 additions and 36 deletions

View file

@ -8,7 +8,9 @@ use codex_app_server_protocol::ThreadEpiphanyStateUpdatedNotification;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedSource;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use epiphany_core::EpiphanyReorientAcceptanceFinding;
use epiphany_core::EpiphanyReorientFindingInterpretation as CoreEpiphanyReorientFinding;
use epiphany_core::EpiphanyRoleAcceptanceFinding;
use epiphany_core::EpiphanyRoleFindingInterpretation as CoreEpiphanyRoleFinding;
use epiphany_core::EpiphanyRoleStatePatchDocument;
use epiphany_core::EpiphanyStateUpdate;
use epiphany_core::build_reorient_acceptance_bundle;
@ -21,10 +23,6 @@ use epiphany_state_model::EpiphanyInvestigationDisposition;
use epiphany_state_model::EpiphanyScratchPad;
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,
@ -51,6 +49,15 @@ pub fn parse_role_finding_state_patch(
.ok_or_else(|| "completed role finding did not include a reviewable statePatch".to_string())
}
pub fn parse_core_role_finding_state_patch(
finding: &CoreEpiphanyRoleFinding,
) -> Result<EpiphanyRoleStatePatchDocument, String> {
finding
.state_patch
.clone()
.ok_or_else(|| "completed role finding did not include a reviewable statePatch".to_string())
}
pub fn core_state_patch_from_protocol(
patch: &ThreadEpiphanyUpdatePatch,
) -> EpiphanyRoleStatePatchDocument {
@ -75,6 +82,30 @@ pub fn core_state_patch_from_protocol(
}
}
pub fn protocol_patch_from_core(
patch: EpiphanyRoleStatePatchDocument,
) -> ThreadEpiphanyUpdatePatch {
ThreadEpiphanyUpdatePatch {
objective: patch.objective,
active_subgoal_id: patch.active_subgoal_id,
subgoals: patch.subgoals,
invariants: patch.invariants,
graphs: patch.graphs,
graph_frontier: patch.graph_frontier,
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
job_bindings: patch.job_bindings,
acceptance_receipts: patch.acceptance_receipts,
runtime_links: patch.runtime_links,
observations: patch.observations,
evidence: patch.evidence,
churn: patch.churn,
mode: patch.mode,
planning: patch.planning,
}
}
pub fn state_update_from_thread_patch(
expected_revision: Option<u64>,
patch: ThreadEpiphanyUpdatePatch,
@ -148,15 +179,15 @@ pub fn epiphany_imagination_finding_has_reviewable_state_patch(
pub fn build_role_acceptance_update(
role_id: ThreadEpiphanyRoleId,
binding_id: &str,
finding: &ThreadEpiphanyRoleFinding,
finding: &CoreEpiphanyRoleFinding,
accepted_evidence_id: String,
accepted_observation_id: String,
accepted_at: String,
) -> Result<RoleAcceptanceUpdate, String> {
let mut patch = match role_id {
let mut core_patch = match role_id {
ThreadEpiphanyRoleId::Imagination => {
let patch = parse_role_finding_state_patch(finding)?;
let patch_errors = imagination_role_accept_patch_errors(&patch);
let patch = parse_core_role_finding_state_patch(finding)?;
let patch_errors = imagination_role_state_patch_policy_errors(&patch);
if !patch_errors.is_empty() {
return Err(format!(
"imagination role state patch is not acceptable: {}",
@ -166,8 +197,8 @@ pub fn build_role_acceptance_update(
patch
}
ThreadEpiphanyRoleId::Modeling => {
let patch = parse_role_finding_state_patch(finding)?;
let patch_errors = modeling_role_accept_patch_errors(&patch);
let patch = parse_core_role_finding_state_patch(finding)?;
let patch_errors = modeling_role_state_patch_policy_errors(&patch);
if !patch_errors.is_empty() {
return Err(format!(
"modeling role state patch is not acceptable: {}",
@ -176,7 +207,7 @@ pub fn build_role_acceptance_update(
}
patch
}
ThreadEpiphanyRoleId::Verification => ThreadEpiphanyUpdatePatch::default(),
ThreadEpiphanyRoleId::Verification => EpiphanyRoleStatePatchDocument::default(),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
return Err(format!(
"role {:?} cannot be accepted through roleAccept",
@ -185,7 +216,8 @@ pub fn build_role_acceptance_update(
}
};
let projected_fields = epiphany_update_patch_changed_fields(&patch)
let patch_for_projection = protocol_patch_from_core(core_patch.clone());
let projected_fields = epiphany_update_patch_changed_fields(&patch_for_projection)
.into_iter()
.map(|field| format!("{field:?}"))
.collect();
@ -197,8 +229,8 @@ pub fn build_role_acceptance_update(
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),
runtime_result_id: finding.runtime_result_id.clone(),
runtime_job_id: finding.runtime_job_id.clone(),
projected_fields,
},
accepted_evidence_id,
@ -208,9 +240,12 @@ pub fn build_role_acceptance_update(
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);
core_patch.evidence.push(acceptance_bundle.evidence);
core_patch.observations.push(acceptance_bundle.observation);
core_patch
.acceptance_receipts
.push(acceptance_bundle.receipt);
let patch = protocol_patch_from_core(core_patch);
let changed_fields = epiphany_update_patch_changed_fields(&patch);
Ok(RoleAcceptanceUpdate {
@ -225,7 +260,7 @@ pub fn build_role_acceptance_update(
pub fn build_reorient_acceptance_update(
expected_revision: Option<u64>,
binding_id: &str,
finding: &ThreadEpiphanyReorientFinding,
finding: &CoreEpiphanyReorientFinding,
accepted_evidence_id: String,
accepted_observation_id: String,
accepted_at: String,
@ -241,8 +276,8 @@ pub fn build_reorient_acceptance_update(
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),
runtime_result_id: finding.runtime_result_id.clone(),
runtime_job_id: finding.runtime_job_id.clone(),
},
accepted_evidence_id,
accepted_observation_id,

View file

@ -35,8 +35,8 @@ use epiphany_state_model::EpiphanyThreadState;
use crate::error::EpiphanyBridgeError;
use crate::error::Result as BridgeResult;
use crate::jobs::epiphany_blocked_state_job;
use crate::jobs::derive_epiphany_jobs;
use crate::jobs::epiphany_blocked_state_job;
use crate::jobs::map_interrupted_epiphany_job;
use crate::jobs::map_launched_epiphany_job;
use crate::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
@ -54,8 +54,10 @@ use crate::pressure::derive_epiphany_pressure;
use crate::reorient::EpiphanyFreshnessWatcherSnapshot;
use crate::reorient::derive_epiphany_freshness_view;
use crate::reorient::derive_epiphany_reorient;
use crate::runtime_results::load_completed_epiphany_reorient_finding;
use crate::runtime_results::load_completed_epiphany_role_finding;
use crate::results::map_protocol_reorient_finding;
use crate::results::map_protocol_role_finding;
use crate::runtime_results::load_completed_core_epiphany_reorient_finding;
use crate::runtime_results::load_completed_core_epiphany_role_finding;
use uuid::Uuid;
#[allow(async_fn_in_trait)]
@ -381,7 +383,7 @@ pub async fn apply_thread_epiphany_role_accept(
validate_expected_revision(expected_revision, state.revision)?;
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let finding = load_completed_epiphany_role_finding(
let finding = load_completed_core_epiphany_role_finding(
Some(runtime_store_path.as_path()),
&state,
role_id,
@ -418,7 +420,7 @@ pub async fn apply_thread_epiphany_role_accept(
accepted_observation_id,
accepted_evidence_id,
applied_patch,
finding,
finding: map_protocol_role_finding(role_id, finding),
})
}
@ -444,7 +446,7 @@ pub async fn apply_thread_epiphany_reorient_accept(
}
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let finding = load_completed_epiphany_reorient_finding(
let finding = load_completed_core_epiphany_reorient_finding(
Some(runtime_store_path.as_path()),
&state,
binding_id,
@ -477,7 +479,7 @@ pub async fn apply_thread_epiphany_reorient_accept(
accepted_receipt_id,
accepted_observation_id,
accepted_evidence_id,
finding,
finding: map_protocol_reorient_finding(finding),
})
}

View file

@ -392,19 +392,29 @@ pub fn load_completed_epiphany_role_finding(
role_id: ThreadEpiphanyRoleId,
binding_id: &str,
) -> BridgeResult<ThreadEpiphanyRoleFinding> {
load_completed_core_epiphany_role_finding(runtime_store_path, state, role_id, binding_id)
.map(|finding| map_protocol_role_finding(role_id, finding))
}
pub fn load_completed_core_epiphany_role_finding(
runtime_store_path: Option<&Path>,
state: &EpiphanyThreadState,
role_id: ThreadEpiphanyRoleId,
binding_id: &str,
) -> BridgeResult<EpiphanyRoleFindingInterpretation> {
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
let (status, finding, _note) = load_epiphany_role_result_from_runtime_spine_job(
let snapshot = load_core_epiphany_role_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
runtime_store_path,
role_id,
);
if status != ThreadEpiphanyRoleResultStatus::Completed {
if snapshot.status != CoreEpiphanyCoordinatorRoleResultStatus::Completed {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"cannot accept role result while worker status is {:?}",
status
snapshot.status
)));
}
return finding.ok_or_else(|| {
return snapshot.finding.ok_or_else(|| {
EpiphanyBridgeError::InvalidRequest(
"cannot accept completed role worker because no typed runtime-spine result was recorded"
.to_string(),
@ -489,18 +499,27 @@ pub fn load_completed_epiphany_reorient_finding(
state: &EpiphanyThreadState,
binding_id: &str,
) -> BridgeResult<ThreadEpiphanyReorientFinding> {
load_completed_core_epiphany_reorient_finding(runtime_store_path, state, binding_id)
.map(map_protocol_reorient_finding)
}
pub fn load_completed_core_epiphany_reorient_finding(
runtime_store_path: Option<&Path>,
state: &EpiphanyThreadState,
binding_id: &str,
) -> BridgeResult<EpiphanyReorientFindingInterpretation> {
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
let (status, finding, _note) = load_epiphany_reorient_result_from_runtime_spine_job(
let snapshot = load_core_epiphany_reorient_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
runtime_store_path,
);
if status != ThreadEpiphanyReorientResultStatus::Completed {
if snapshot.status != CoreEpiphanyCrrcResultStatus::Completed {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"cannot accept reorientation result while worker status is {:?}",
status
snapshot.status
)));
}
return finding.ok_or_else(|| {
return snapshot.finding.ok_or_else(|| {
EpiphanyBridgeError::InvalidRequest(
"cannot accept completed reorientation worker because no typed runtime-spine result was recorded"
.to_string(),

View file

@ -52,6 +52,7 @@ remembers doctrine; the branch remembers the blade.
- Core role/reorient finding interpretations and acceptance bundles now serialize natively too. Use those typed result surfaces as the next authority target; do not let `ThreadEpiphanyRoleFinding` or `ThreadEpiphanyReorientFinding` remain the bridge's internal result truth just because legacy view/accept responses still expose them.
- Runtime-result loading now has core typed snapshot APIs for role and reorient worker results, with legacy protocol tuple wrappers layered over them. This intentionally creates temporary duplicate note rendering; the next cut should move coordinator and acceptance consumers to typed snapshots, then delete protocol-shaped result rendering instead of preserving both.
- Coordinator result gating now consumes typed runtime result snapshots and core finding interpretations directly. `ThreadEpiphanyRoleFinding` / `ThreadEpiphanyReorientFinding` remain in legacy runtime wrapper/view/mutation response paths; the next cut is acceptance/mutation so protocol findings stop being the accepted-result authority.
- Acceptance/mutation now builds role/reorient receipts, evidence, observations, scratch, and checkpoint updates from core typed finding interpretations. Protocol findings remain as legacy response payloads and helper residue; delete those wrappers once view/result responses are also typed internally.
- MCP remains JSON because MCP is a JSON protocol. The latest processor cut only moved MCP refresh/OAuth/status/resource/tool-call route handling into `codex_message_processor/mcp_routes.rs`, reducing the root processor to about 8,977 lines; it did not claim MCP payloads are CultNet or make MCP an Epiphany-owned runtime contract.
- Codex account auth route handling remains in vendored Codex but now lives in `codex_message_processor/auth_routes.rs`: API-key login, ChatGPT browser/device-code/external-token login, logout, auth status, account, rate limits, and add-credits nudge handling. This preserves Codex ownership of subscription auth while cutting the root processor to about 8,094 lines.
- One-off command execution and command stream write/resize/terminate route handling now live in `codex_message_processor/command_routes.rs`; `CommandExecManager` remains the lifecycle owner, while the root processor is about 7,800 lines.

Binary file not shown.

View file

@ -320,7 +320,9 @@ current_status:
policy now consumes those typed result snapshots directly for role and
reorient acceptance/gating signals. Delete the duplicate protocol-shaped
result rendering once acceptance and view response edges finish moving off
protocol finding authority. The bridge still
protocol finding authority. Role/reorient acceptance now builds receipts
and evidence from core typed findings, mapping to protocol findings only for
legacy accept response cargo. The bridge still
depends on vendored Codex for app-server protocol DTOs and absolute path
validation, so the purification is not done.
next_action: Continue Codex starvation and CultNet liberation by cutting bridge-internal `ThreadEpiphany*` authority. Prioritize role-result/reorient-result DTO coupling next: derive typed CultNet surfaces first, push JSON-RPC projection into app-server, and keep MCP/OpenAI JSON only at their true protocol/text edges. Do not add compatibility adapters unless they protect a named ownership invariant.