orchestrator sm: report deferred and aborted updates
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs
index 877e1c9..7d31b67 100644
--- a/services/orchestrator/sm/src/lib.rs
+++ b/services/orchestrator/sm/src/lib.rs
@@ -577,7 +577,11 @@
                     // returns `Handled` and the update continues untouched, so
                     // its staged image must survive — do not discard then.
                     if matches!(outcome, Outcome::Transition(State::Recovering(_))) {
+                        // Dispose of the orphaned image *and* answer the update
+                        // requester: the update it was awaiting a verdict on has
+                        // been superseded by recovery, not merely delayed.
                         ctx.emit(Effect::DiscardStaged);
+                        ctx.emit(Effect::ReportUpdateAborted);
                     }
                     outcome
                 }
@@ -685,6 +689,17 @@
                     Outcome::Handled
                 }
             }
+            // A new update cannot start from any supervised state except
+            // `Ready` (the machine is mid-walk, mid-update, or mid-recovery).
+            // `Ready` accepts `UpdateRequest` upstream in `handle` (→ `Updating`)
+            // and so never reaches here; this arm therefore fires only for
+            // `AwaitingReady`, `Updating`, and `Recovering`. Report the refusal
+            // rather than dropping it silently, and leave the in-flight
+            // walk/update/recovery untouched (`Handled`, no transition).
+            Event::UpdateRequest => {
+                ctx.emit(Effect::ReportUpdateDeferred);
+                Outcome::Handled
+            }
             Event::EffectFailed => Outcome::Transition(State::Locked),
             _ => Outcome::Super,
         }
diff --git a/services/orchestrator/sm/src/model.rs b/services/orchestrator/sm/src/model.rs
index a937c84..f9537e9 100644
--- a/services/orchestrator/sm/src/model.rs
+++ b/services/orchestrator/sm/src/model.rs
@@ -274,6 +274,28 @@
     /// immediately before the machine latches to [`Locked`](crate::State::Locked).
     /// Names the component that forced the halt.
     ReportRecoveryFailed(ComponentId),
+    /// Report that an [`Event::UpdateRequest`] was declined because the machine
+    /// is busy in a supervised state other than [`Ready`](crate::State::Ready)
+    /// — a chain walk is finishing ([`AwaitingReady`](crate::State::AwaitingReady)),
+    /// an update is already staged ([`Updating`](crate::State::Updating)), or a
+    /// recovery is in flight ([`Recovering`](crate::State::Recovering)). Emitted
+    /// instead of silently dropping the request, so the refusal is visible in
+    /// the effect trace and the driver can answer the requester (e.g. a PLDM
+    /// "retry later" completion code). Advisory only: it changes no state and
+    /// does not perturb the in-flight update or recovery. `Ready` never produces
+    /// this — it accepts the request and transitions to `Updating`.
+    ReportUpdateDeferred,
+    /// Report that an update *already in flight* ([`Updating`](crate::State::Updating))
+    /// was aborted because a `Required` component was found corrupt and recovery
+    /// preempted it (the machine left `Updating` for
+    /// [`Recovering`](crate::State::Recovering)). Its counterpart on the intake
+    /// side is [`ReportUpdateDeferred`]: `Deferred` refuses an update that never
+    /// started, `Aborted` announces the loss of one that had. Paired with the
+    /// [`DiscardStaged`] that cleans up the orphaned image — `DiscardStaged`
+    /// disposes of the *image*, this answers the *request*, so the requester
+    /// (which was awaiting `UpdateVerified`/`UpdateRejected`) learns the update
+    /// was superseded by recovery and can retry once the platform is whole.
+    ReportUpdateAborted,
     LatchLockdown,
     /// Internal only — tells the orchestrator to handle this event next.
     /// Never forwarded to a [`Platform`](crate::Platform).
diff --git a/services/orchestrator/sm/src/tests.rs b/services/orchestrator/sm/src/tests.rs
index 99896e0..bc4636d 100644
--- a/services/orchestrator/sm/src/tests.rs
+++ b/services/orchestrator/sm/src/tests.rs
@@ -1061,6 +1061,74 @@
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
 
+/// An `UpdateRequest` arriving mid-recovery is declined, not silently dropped:
+/// the machine emits `ReportUpdateDeferred` and stays in `Recovering`, leaving
+/// the in-flight recovery untouched (single-flight, recovery-priority).
+#[test]
+fn update_request_while_recovering_is_deferred() {
+    let (effects, state) = drive(
+        passive_required(&[C0, C1]),
+        &[
+            BOOT,
+            Event::VerificationFailed(C0), // → Recovering(C0)
+            Event::UpdateRequest,          // declined while recovering
+        ],
+    );
+    assert_eq!(state, State::Recovering(C0));
+    assert_eq!(effects.last(), Some(&Effect::ReportUpdateDeferred));
+}
+
+/// A second `UpdateRequest` while an update is already staged (`Updating`) is
+/// declined the same way — reported, not dropped, and the staged update is
+/// left in place.
+#[test]
+fn update_request_while_updating_is_deferred() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::UpdateRequest, // → Updating
+            Event::UpdateRequest, // declined while updating
+        ],
+    );
+    assert_eq!(state, State::Updating);
+    assert_eq!(effects.last(), Some(&Effect::ReportUpdateDeferred));
+}
+
+/// An `UpdateRequest` while the chain walk is still finishing (`AwaitingReady`)
+/// is declined too: the machine is not yet `Ready`, so it reports the refusal
+/// and keeps waiting on the outstanding component.
+#[test]
+fn update_request_while_awaiting_ready_is_deferred() {
+    let (effects, state) = drive(
+        chain(&[
+            (C0, ComponentAttrs::active_required()),
+            (C1, ComponentAttrs::passive_required()),
+        ]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0), // active released → AwaitingReady(Some(C0))
+            Event::UpdateRequest,          // declined while awaiting readiness
+        ],
+    );
+    assert_eq!(state, State::AwaitingReady(Some(C0)));
+    assert_eq!(effects.last(), Some(&Effect::ReportUpdateDeferred));
+}
+
+/// Negative control: from `Ready` an `UpdateRequest` is *accepted* — it starts
+/// an update (→ `Updating`) and never emits `ReportUpdateDeferred`. `Ready`
+/// intercepts the request upstream, so it can never reach the deferral arm.
+#[test]
+fn update_request_in_ready_starts_update_not_deferred() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[BOOT, Event::VerificationPassed(C0), Event::UpdateRequest],
+    );
+    assert_eq!(state, State::Updating);
+    assert!(!effects.contains(&Effect::ReportUpdateDeferred));
+}
+
 /// UpdateVerified activates the staged image and returns to Ready.
 /// (Complements update_rollback_is_not_recovery which tests UpdateRejected.)
 #[test]
@@ -1482,6 +1550,34 @@
         effects.contains(&Effect::DiscardStaged),
         "leaving Updating for recovery must discard the staged image",
     );
+    assert!(
+        effects.contains(&Effect::ReportUpdateAborted),
+        "preempting an in-flight update must report it aborted, not drop it silently",
+    );
+}
+
+/// The abort report is *only* for a genuine preemption. An `Isolable`
+/// corruption of another device while `Updating` is contained (`Handled`) and
+/// the update continues untouched — so neither `DiscardStaged` nor
+/// `ReportUpdateAborted` is emitted, and the machine stays in `Updating`.
+#[test]
+fn contained_corruption_during_update_does_not_abort() {
+    let (effects, state) = drive(
+        chain(&[
+            (C0, ComponentAttrs::passive_required()),
+            (C1, ComponentAttrs::passive_isolable()),
+        ]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::VerificationPassed(C1), // → Ready
+            Event::UpdateRequest,          // → Updating
+            Event::CorruptionDetected(C1), // isolable: contained, update survives
+        ],
+    );
+    assert_eq!(state, State::Updating);
+    assert!(!effects.contains(&Effect::ReportUpdateAborted));
+    assert!(!effects.contains(&Effect::DiscardStaged));
 }
 
 /// GAP 2 (red): a second `Required` corruption while already recovering clobbers