diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs index 312b8ad0..848521fb 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -76,6 +76,7 @@ use codex_protocol::protocol::EpiphanyModeState as CoreEpiphanyModeState; use codex_protocol::protocol::EpiphanyObservation as CoreEpiphanyObservation; use codex_protocol::protocol::EpiphanyPlanningState as CoreEpiphanyPlanningState; use codex_protocol::protocol::EpiphanyRetrievalStatus as CoreEpiphanyRetrievalStatus; +use codex_protocol::protocol::EpiphanyRuntimeLink as CoreEpiphanyRuntimeLink; use codex_protocol::protocol::EpiphanyScratchPad as CoreEpiphanyScratchPad; use codex_protocol::protocol::EpiphanySubgoal as CoreEpiphanySubgoal; use codex_protocol::protocol::EpiphanyThreadState as CoreEpiphanyThreadState; @@ -5426,6 +5427,8 @@ pub struct ThreadEpiphanyUpdatePatch { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub acceptance_receipts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub runtime_links: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub observations: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub evidence: Vec, @@ -5487,6 +5490,7 @@ pub enum ThreadEpiphanyStateUpdatedField { InvestigationCheckpoint, JobBindings, AcceptanceReceipts, + RuntimeLinks, Observations, Evidence, Churn, diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index aa9fa054..f465506c 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -5207,6 +5207,7 @@ impl CodexMessageProcessor { 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, @@ -6567,6 +6568,7 @@ impl CodexMessageProcessor { investigation_checkpoint: patch.investigation_checkpoint, job_bindings: patch.job_bindings, acceptance_receipts: patch.acceptance_receipts, + runtime_links: patch.runtime_links, observations: patch.observations, evidence, churn: patch.churn, @@ -6658,6 +6660,7 @@ impl CodexMessageProcessor { 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, @@ -15691,6 +15694,12 @@ fn imagination_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Ve "acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(), ); } + if !patch.runtime_links.is_empty() { + errors.push( + "runtime link changes are owned by launch/read-back surfaces, not worker statePatch" + .to_string(), + ); + } if patch.churn.is_some() || patch.mode.is_some() { errors.push( "churn or mode changes are not allowed through imagination role acceptance".to_string(), @@ -15753,6 +15762,12 @@ fn modeling_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec, pub job_bindings: Option>, pub acceptance_receipts: Vec, + pub runtime_links: Vec, pub observations: Vec, pub evidence: Vec, pub churn: Option, @@ -186,6 +188,7 @@ impl EpiphanyStateUpdate { && self.investigation_checkpoint.is_none() && self.job_bindings.is_none() && self.acceptance_receipts.is_empty() + && self.runtime_links.is_empty() && self.observations.is_empty() && self.evidence.is_empty() && self.churn.is_none() @@ -525,15 +528,22 @@ impl CodexThread { launcher_job_id.as_str(), backend_job_id.as_str(), ); + let replacement_runtime_link = + build_epiphany_runtime_link(&request, backend_job_id.as_str()); let next_job_bindings = replace_or_append_epiphany_job_binding( current_state.job_bindings.clone(), replacement_binding, ); + let next_runtime_links = replace_or_append_epiphany_runtime_link( + current_state.runtime_links.clone(), + replacement_runtime_link, + ); let validation_errors = epiphany_state_update_validation_errors( ¤t_state, &EpiphanyStateUpdate { job_bindings: Some(next_job_bindings.clone()), + runtime_links: next_runtime_links.clone(), ..Default::default() }, ); @@ -548,6 +558,7 @@ impl CodexThread { .epiphany_update_state(EpiphanyStateUpdate { expected_revision: request.expected_revision, job_bindings: Some(next_job_bindings), + runtime_links: next_runtime_links, ..Default::default() }) .await?; @@ -852,6 +863,23 @@ fn build_epiphany_job_launch_binding( } } +fn build_epiphany_runtime_link( + request: &EpiphanyJobLaunchRequest, + runtime_job_id: &str, +) -> EpiphanyRuntimeLink { + EpiphanyRuntimeLink { + id: format!("runtime-link-{}-{runtime_job_id}", request.binding_id), + binding_id: request.binding_id.clone(), + surface: "jobLaunch".to_string(), + role_id: request.owner_role.clone(), + authority_scope: request.authority_scope.clone(), + runtime_job_id: runtime_job_id.to_string(), + runtime_result_id: None, + linked_subgoal_ids: request.linked_subgoal_ids.clone(), + linked_graph_node_ids: request.linked_graph_node_ids.clone(), + } +} + fn open_epiphany_runtime_spine_job( store_path: &std::path::Path, state: &EpiphanyThreadState, @@ -931,6 +959,18 @@ fn replace_or_append_epiphany_job_binding( bindings } +fn replace_or_append_epiphany_runtime_link( + mut links: Vec, + replacement: EpiphanyRuntimeLink, +) -> Vec { + if let Some(existing) = links.iter_mut().find(|link| link.id == replacement.id) { + *existing = replacement; + return links; + } + links.push(replacement); + links +} + fn clear_epiphany_job_binding_backend( mut bindings: Vec, binding_index: usize, @@ -1050,6 +1090,12 @@ fn epiphany_state_update_validation_errors( &known_evidence_ids, )); } + if !update.runtime_links.is_empty() { + errors.extend(validate_epiphany_runtime_links( + &state.runtime_links, + &update.runtime_links, + )); + } if let Some(planning) = update.planning.as_ref() { errors.extend(validate_epiphany_planning_state(planning)); } @@ -1058,6 +1104,37 @@ fn epiphany_state_update_validation_errors( errors } +fn validate_epiphany_runtime_links( + existing: &[EpiphanyRuntimeLink], + links: &[EpiphanyRuntimeLink], +) -> Vec { + let mut errors = Vec::new(); + let mut seen_ids = existing + .iter() + .map(|link| link.id.as_str()) + .collect::>(); + for link in links { + require_nonempty_update(&link.id, "runtime_link.id", &mut errors); + require_nonempty_update(&link.binding_id, "runtime_link.binding_id", &mut errors); + require_nonempty_update(&link.surface, "runtime_link.surface", &mut errors); + require_nonempty_update(&link.role_id, "runtime_link.role_id", &mut errors); + require_nonempty_update( + &link.authority_scope, + "runtime_link.authority_scope", + &mut errors, + ); + require_nonempty_update( + &link.runtime_job_id, + "runtime_link.runtime_job_id", + &mut errors, + ); + if !seen_ids.insert(link.id.as_str()) { + errors.push(format!("runtime link id {:?} is duplicated", link.id)); + } + } + errors +} + fn validate_epiphany_acceptance_receipts( existing: &[EpiphanyAcceptanceReceipt], receipts: &[EpiphanyAcceptanceReceipt], @@ -1407,6 +1484,7 @@ fn apply_epiphany_state_update( state.job_bindings = job_bindings; } prepend_recent(&mut state.acceptance_receipts, update.acceptance_receipts); + prepend_recent(&mut state.runtime_links, update.runtime_links); if let Some(churn) = update.churn { state.churn = Some(churn); } @@ -2072,6 +2150,7 @@ mod epiphany_update_tests { let binding = build_epiphany_job_launch_binding(&request, "epiphany-heartbeat-launch-1", "turn-1"); + let runtime_link = build_epiphany_runtime_link(&request, "turn-1"); assert_eq!( binding.backend_kind, @@ -2084,6 +2163,13 @@ mod epiphany_update_tests { .as_deref() .is_some_and(|note| note.contains("heartbeat activation")) ); + assert_eq!( + runtime_link.id, + "runtime-link-modeling-checkpoint-worker-turn-1" + ); + assert_eq!(runtime_link.runtime_job_id, "turn-1"); + assert_eq!(runtime_link.runtime_result_id, None); + assert_eq!(runtime_link.role_id, "epiphany-modeler"); } } diff --git a/vendor/codex/codex-rs/core/src/session/tests.rs b/vendor/codex/codex-rs/core/src/session/tests.rs index f6b5c509..d8f4db50 100644 --- a/vendor/codex/codex-rs/core/src/session/tests.rs +++ b/vendor/codex/codex-rs/core/src/session/tests.rs @@ -339,6 +339,7 @@ fn sample_epiphany_state_for_prompt() -> EpiphanyThreadState { investigation_checkpoint: None, job_bindings: Vec::new(), acceptance_receipts: Vec::new(), + runtime_links: Vec::new(), scratch: Some(EpiphanyScratchPad { summary: Some("Inject the state as a developer fragment, not as a user message.".to_string()), hypothesis: Some("Keeping the block bounded will help resume without making prompt sludge.".to_string()), diff --git a/vendor/codex/codex-rs/protocol/src/protocol.rs b/vendor/codex/codex-rs/protocol/src/protocol.rs index 7b894206..6d2b6f68 100644 --- a/vendor/codex/codex-rs/protocol/src/protocol.rs +++ b/vendor/codex/codex-rs/protocol/src/protocol.rs @@ -3027,6 +3027,9 @@ pub struct EpiphanyThreadState { #[ts(type = "Array")] pub acceptance_receipts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(type = "Array")] + pub runtime_links: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] #[ts(type = "Array")] pub observations: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -3067,6 +3070,23 @@ pub struct EpiphanyAcceptanceReceipt { pub summary: Option, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)] +pub struct EpiphanyRuntimeLink { + pub id: String, + pub binding_id: String, + pub surface: String, + pub role_id: String, + pub authority_scope: String, + pub runtime_job_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "string | null")] + pub runtime_result_id: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub linked_subgoal_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub linked_graph_node_ids: Vec, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)] pub struct EpiphanySubgoal { pub id: String,