From 1cb3e7648e4ef866fefeaa29aaf0553e5b2aedd6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 7 Aug 2026 05:21:32 +0100 Subject: [PATCH] Repair typed worker retry contracts --- epiphany-core/src/agent_launch.rs | 118 ++++++++++++++++++------ epiphany-core/src/coordinator_launch.rs | 57 +++++++++++- epiphany-core/src/runtime_spine.rs | 40 +++++--- 3 files changed, 170 insertions(+), 45 deletions(-) diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index f122f2b3..ff6bd2b4 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -204,6 +204,53 @@ fn evidence_output_schema() -> serde_json::Value { }) } +fn self_patch_memory_output_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "required": ["memoryId", "summary", "salience", "confidence"], + "properties": { + "memoryId": {"type": "string"}, + "summary": {"type": "string", "minLength": 1, "maxLength": 600}, + "salience": {"type": "number", "minimum": 0, "maximum": 1}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + "linkedEventIds": {"type": "array", "items": {"type": "string"}}, + "linkedRelationshipId": {"type": "string"} + }, + "additionalProperties": false + }) +} + +fn self_patch_goal_output_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "required": ["goalId", "description", "scope", "priority", "emotionalStake", "status"], + "properties": { + "goalId": {"type": "string"}, + "description": {"type": "string", "minLength": 1, "maxLength": 700}, + "scope": {"type": "string", "enum": ["immediate", "scene", "case", "arc", "life"]}, + "priority": {"type": "number", "minimum": 0, "maximum": 1}, + "emotionalStake": {"type": "string", "minLength": 1, "maxLength": 400}, + "blockers": {"type": "array", "items": {"type": "string"}}, + "status": {"type": "string", "enum": ["active", "blocked", "dormant", "resolved", "abandoned"]} + }, + "additionalProperties": false + }) +} + +fn self_patch_value_output_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "required": ["valueId", "label", "priority", "unforgivableIfBetrayed"], + "properties": { + "valueId": {"type": "string"}, + "label": {"type": "string", "minLength": 1, "maxLength": 240}, + "priority": {"type": "number", "minimum": 0, "maximum": 1}, + "unforgivableIfBetrayed": {"type": "boolean"} + }, + "additionalProperties": false + }) +} + pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> serde_json::Value { let verdict_enum = match role_id { EpiphanyRoleResultRoleId::Imagination => { @@ -283,47 +330,33 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> }, "semanticMemories": { "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } + "maxItems": 8, + "items": self_patch_memory_output_schema() }, "episodicMemories": { "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } + "maxItems": 8, + "items": self_patch_memory_output_schema() }, "relationshipMemories": { "type": "array", - "items": { - "type": "object", - "required": ["memoryId", "summary", "salience", "confidence"], - "additionalProperties": true - } + "maxItems": 8, + "items": self_patch_memory_output_schema() }, "goals": { "type": "array", - "items": { - "type": "object", - "required": ["goalId", "description", "scope", "priority", "emotionalStake", "status"], - "additionalProperties": true - } + "maxItems": 6, + "items": self_patch_goal_output_schema() }, "values": { "type": "array", - "items": { - "type": "object", - "required": ["valueId", "label", "priority", "unforgivableIfBetrayed"], - "additionalProperties": true - } + "maxItems": 6, + "items": self_patch_value_output_schema() }, "privateNotes": { "type": "array", - "items": {"type": "string"} + "maxItems": 6, + "items": {"type": "string", "minLength": 1, "maxLength": 600} } }, "additionalProperties": false @@ -1529,8 +1562,8 @@ mod tests { schema["properties"]["repositoryBodyObservationBasis"]["additionalProperties"], false ); - let frontier_item = &schema["properties"]["repoModelPatch"]["properties"]["operations"] - ["items"]["anyOf"][6]["properties"]["item"]; + let frontier_item = &schema["properties"]["repoModelPatch"]["properties"]["operations"]["items"] + ["anyOf"][6]["properties"]["item"]; assert!( frontier_item["required"] .as_array() @@ -1538,7 +1571,10 @@ mod tests { .iter() .any(|value| value == "target_claim_ids") ); - assert_eq!(frontier_item["properties"]["target_claim_ids"]["minItems"], 1); + assert_eq!( + frontier_item["properties"]["target_claim_ids"]["minItems"], + 1 + ); let purposes = schema["properties"]["repoModelPatch"]["properties"]["purpose"]["oneOf"] .as_array() .expect("typed Modeling purpose alternatives"); @@ -1600,6 +1636,30 @@ mod tests { ); } + #[test] + fn role_self_patch_schema_matches_canonical_numeric_memory_contract() { + let schema = epiphany_role_launch_output_schema(EpiphanyRoleResultRoleId::Verification); + let self_patch = &schema["properties"]["selfPatch"]; + for bundle in [ + "semanticMemories", + "episodicMemories", + "relationshipMemories", + ] { + let item = &self_patch["properties"][bundle]["items"]; + assert_eq!(item["properties"]["salience"]["type"], "number"); + assert_eq!(item["properties"]["confidence"]["type"], "number"); + assert_eq!(item["additionalProperties"], false); + } + assert_eq!( + self_patch["properties"]["goals"]["items"]["properties"]["priority"]["type"], + "number" + ); + assert_eq!( + self_patch["properties"]["values"]["items"]["properties"]["priority"]["type"], + "number" + ); + } + #[test] fn frontier_planning_schema_exposes_candidate_without_generic_patch_mouths() { let schema = epiphany_frontier_planning_output_schema(); diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 992fbbba..1a5385f8 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -971,10 +971,11 @@ fn validate_proposal_modeling_launch( if cache .get_all::()? .iter() - .any(|binding| binding.proposal_modeling_request_id == request_id) + .filter(|binding| binding.proposal_modeling_request_id == request_id) + .any(|binding| !proposal_modeling_launch_was_superseded(state, binding)) { return Err(anyhow!( - "proposal Modeling selection request is already bound to a launch" + "proposal Modeling selection request already has an unsuperseded launch" )); } let identity = cache @@ -1012,6 +1013,20 @@ fn validate_proposal_modeling_launch( Ok((selection, proposal, identity)) } +fn proposal_modeling_launch_was_superseded( + state: &EpiphanyThreadState, + binding: &RepoFrontierProposalModelingLaunchBinding, +) -> bool { + state.acceptance_receipts.iter().any(|receipt| { + receipt.job_id == binding.job_id + && receipt.binding_id == binding.binding_id + && receipt.surface == "roleFailureReview" + && receipt.role_id == "modeling" + && receipt.status == "superseded" + && !receipt.result_id.trim().is_empty() + }) +} + fn terminal_runtime_link_for_binding( state: &EpiphanyThreadState, binding_id: &str, @@ -1806,6 +1821,44 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn proposal_launch_binding_is_reusable_only_after_exact_failure_supersession() { + let binding = RepoFrontierProposalModelingLaunchBinding { + schema_version: REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_record_id: "proposal-launch-1".into(), + proposal_modeling_request_id: "proposal-request-1".into(), + proposal_id: "proposal-1".into(), + proposal_payload_sha256: "a".repeat(64), + job_id: "job-1".into(), + binding_id: EPIPHANY_MODELING_ROLE_BINDING_ID.into(), + runtime_id: "runtime-1".into(), + thread_id: "thread-1".into(), + launched_at: "2026-07-13T05:00:03Z".into(), + worker_launch_document_sha256: "b".repeat(64), + contract: REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT.into(), + }; + let mut state = EpiphanyThreadState::default(); + assert!(!proposal_modeling_launch_was_superseded(&state, &binding)); + + state + .acceptance_receipts + .push(epiphany_state_model::EpiphanyAcceptanceReceipt { + id: "failure-review-1".into(), + result_id: "failed-result-1".into(), + job_id: binding.job_id.clone(), + binding_id: binding.binding_id.clone(), + surface: "roleFailureReview".into(), + role_id: "modeling".into(), + status: "superseded".into(), + accepted_at: "2026-07-13T05:01:00Z".into(), + ..Default::default() + }); + assert!(proposal_modeling_launch_was_superseded(&state, &binding)); + + state.acceptance_receipts[0].job_id = "other-job".into(); + assert!(!proposal_modeling_launch_was_superseded(&state, &binding)); + } + #[test] fn claim_repair_launch_is_coordinator_owned_bound_and_single_use() -> Result<()> { let root = tempfile::tempdir()?; diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 6c8901cb..79cf89f9 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -3935,8 +3935,7 @@ pub fn promote_autonomous_direction_options_for_modeling( else { return Ok(Vec::new()); }; - let Some(model) = opening - .get::(crate::MEMORY_GRAPH_KEY)? + let Some(model) = opening.get::(crate::MEMORY_GRAPH_KEY)? else { return Ok(Vec::new()); }; @@ -6900,12 +6899,17 @@ pub fn commit_repo_frontier_verification_request_for_chain( )); } let authority = &authorities[0]; + let request_id = format!( + "frontier-verification-{}-{}", + authority.route_id, chain.commit_receipt_id + ); + let requested_at = cache + .get::(&request_id)? + .map(|existing| existing.requested_at) + .unwrap_or_else(|| requested_at.to_string()); let request = RepoFrontierVerificationRequest { schema_version: REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION.to_string(), - request_id: format!( - "frontier-verification-{}-{}", - authority.route_id, chain.commit_receipt_id - ), + request_id, route_id: authority.route_id.clone(), model_revision: authority.model_revision, model_hash: authority.model_hash.clone(), @@ -6916,7 +6920,7 @@ pub fn commit_repo_frontier_verification_request_for_chain( hands_patch_receipt_id: chain.patch_receipt_id.clone(), hands_command_receipt_id: chain.command_receipt_id.clone(), hands_commit_receipt_id: chain.commit_receipt_id.clone(), - requested_at: requested_at.to_string(), + requested_at, contract: REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT.to_string(), }; put_repo_frontier_verification_request(store_path, &request)?; @@ -9780,13 +9784,11 @@ pub(crate) mod tests { )?; cache.put( crate::MEMORY_GRAPH_KEY, - &crate::EpiphanyMemoryGraphEntry::from_snapshot( - &crate::EpiphanyMemoryGraphSnapshot { - schema_version: Some(crate::MEMORY_GRAPH_SCHEMA_VERSION.into()), - graph_id: "bootstrap-model".into(), - ..Default::default() - }, - )?, + &crate::EpiphanyMemoryGraphEntry::from_snapshot(&crate::EpiphanyMemoryGraphSnapshot { + schema_version: Some(crate::MEMORY_GRAPH_SCHEMA_VERSION.into()), + graph_id: "bootstrap-model".into(), + ..Default::default() + })?, )?; let before = std::fs::read(&store)?; assert!( @@ -12511,6 +12513,16 @@ pub(crate) mod tests { &hands_chain, "2026-05-06T00:07:11Z", )?; + let retried_verification_request = commit_repo_frontier_verification_request_for_chain( + &store, + &hands_chain, + "2026-05-06T00:08:11Z", + )?; + assert_eq!(retried_verification_request, verification_request); + assert_eq!( + retried_verification_request.requested_at, + "2026-05-06T00:07:11Z" + ); assert_eq!(verification_request.route_id, frontier_route.route_id); assert_eq!(verification_request.hands_patch_receipt_id, "hands-patch-1"); assert_eq!(