Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.

Commit 9de97c9

Browse files
author
Jobdori
committed
feat(recovery): bridge WorkerFailureKind to FailureScenario (P2.8/P2.13)
Connect worker_boot failure classification to recovery_recipes policy: - Add FailureScenario::ProviderFailure variant - Add FailureScenario::from_worker_failure_kind() bridge function mapping every WorkerFailureKind to a concrete FailureScenario - Add RecoveryStep::RestartWorker for provider failure recovery - Add recipe for ProviderFailure: RestartWorker -> AlertHuman escalation - 3 new tests: bridge mapping, recipe structure, recovery attempt cycle Previously a claw that detected WorkerFailureKind::Provider had no machine-readable path to 'what should I do about this?'. Now it can call from_worker_failure_kind() -> recipe_for() -> attempt_recovery() as a single structured chain. Closes the silo between worker_boot and recovery_recipes.
1 parent 736069f commit 9de97c9

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

rust/crates/runtime/src/recovery_recipes.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::collections::HashMap;
99

1010
use serde::{Deserialize, Serialize};
1111

12+
use crate::worker_boot::WorkerFailureKind;
13+
1214
/// The six failure scenarios that have known recovery recipes.
1315
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1416
#[serde(rename_all = "snake_case")]
@@ -19,6 +21,7 @@ pub enum FailureScenario {
1921
CompileRedCrossCrate,
2022
McpHandshakeFailure,
2123
PartialPluginStartup,
24+
ProviderFailure,
2225
}
2326

2427
impl FailureScenario {
@@ -32,8 +35,21 @@ impl FailureScenario {
3235
Self::CompileRedCrossCrate,
3336
Self::McpHandshakeFailure,
3437
Self::PartialPluginStartup,
38+
Self::ProviderFailure,
3539
]
3640
}
41+
42+
/// Map a `WorkerFailureKind` to the corresponding `FailureScenario`.
43+
/// This is the bridge that lets recovery policy consume worker boot events.
44+
#[must_use]
45+
pub fn from_worker_failure_kind(kind: WorkerFailureKind) -> Self {
46+
match kind {
47+
WorkerFailureKind::TrustGate => Self::TrustPromptUnresolved,
48+
WorkerFailureKind::PromptDelivery => Self::PromptMisdelivery,
49+
WorkerFailureKind::Protocol => Self::McpHandshakeFailure,
50+
WorkerFailureKind::Provider => Self::ProviderFailure,
51+
}
52+
}
3753
}
3854

3955
impl std::fmt::Display for FailureScenario {
@@ -45,6 +61,7 @@ impl std::fmt::Display for FailureScenario {
4561
Self::CompileRedCrossCrate => write!(f, "compile_red_cross_crate"),
4662
Self::McpHandshakeFailure => write!(f, "mcp_handshake_failure"),
4763
Self::PartialPluginStartup => write!(f, "partial_plugin_startup"),
64+
Self::ProviderFailure => write!(f, "provider_failure"),
4865
}
4966
}
5067
}
@@ -59,6 +76,7 @@ pub enum RecoveryStep {
5976
CleanBuild,
6077
RetryMcpHandshake { timeout: u64 },
6178
RestartPlugin { name: String },
79+
RestartWorker,
6280
EscalateToHuman { reason: String },
6381
}
6482

@@ -196,6 +214,12 @@ pub fn recipe_for(scenario: &FailureScenario) -> RecoveryRecipe {
196214
max_attempts: 1,
197215
escalation_policy: EscalationPolicy::LogAndContinue,
198216
},
217+
FailureScenario::ProviderFailure => RecoveryRecipe {
218+
scenario: *scenario,
219+
steps: vec![RecoveryStep::RestartWorker],
220+
max_attempts: 1,
221+
escalation_policy: EscalationPolicy::AlertHuman,
222+
},
199223
}
200224
}
201225

@@ -551,4 +575,56 @@ mod tests {
551575
assert_eq!(recipe.escalation_policy, EscalationPolicy::Abort);
552576
assert_eq!(recipe.max_attempts, 1);
553577
}
578+
579+
#[test]
580+
fn worker_failure_kind_maps_to_failure_scenario() {
581+
// given / when / then — verify the bridge is correct
582+
assert_eq!(
583+
FailureScenario::from_worker_failure_kind(WorkerFailureKind::TrustGate),
584+
FailureScenario::TrustPromptUnresolved,
585+
);
586+
assert_eq!(
587+
FailureScenario::from_worker_failure_kind(WorkerFailureKind::PromptDelivery),
588+
FailureScenario::PromptMisdelivery,
589+
);
590+
assert_eq!(
591+
FailureScenario::from_worker_failure_kind(WorkerFailureKind::Protocol),
592+
FailureScenario::McpHandshakeFailure,
593+
);
594+
assert_eq!(
595+
FailureScenario::from_worker_failure_kind(WorkerFailureKind::Provider),
596+
FailureScenario::ProviderFailure,
597+
);
598+
}
599+
600+
#[test]
601+
fn provider_failure_recipe_uses_restart_worker_step() {
602+
// given
603+
let recipe = recipe_for(&FailureScenario::ProviderFailure);
604+
605+
// then
606+
assert_eq!(recipe.scenario, FailureScenario::ProviderFailure);
607+
assert!(recipe.steps.contains(&RecoveryStep::RestartWorker));
608+
assert_eq!(recipe.escalation_policy, EscalationPolicy::AlertHuman);
609+
assert_eq!(recipe.max_attempts, 1);
610+
}
611+
612+
#[test]
613+
fn provider_failure_recovery_attempt_succeeds_then_escalates() {
614+
// given
615+
let mut ctx = RecoveryContext::new();
616+
let scenario = FailureScenario::ProviderFailure;
617+
618+
// when — first attempt
619+
let first = attempt_recovery(&scenario, &mut ctx);
620+
assert!(matches!(first, RecoveryResult::Recovered { .. }));
621+
622+
// when — second attempt should escalate (max_attempts=1)
623+
let second = attempt_recovery(&scenario, &mut ctx);
624+
assert!(matches!(second, RecoveryResult::EscalationRequired { .. }));
625+
assert!(ctx
626+
.events()
627+
.iter()
628+
.any(|e| matches!(e, RecoveryEvent::Escalated)));
629+
}
554630
}

0 commit comments

Comments
 (0)