agentmux_launcher\reducer/saga.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Saga-related reducer handlers. Extracted from reducer/mod.rs
5//! in task #182 PR-D for navigability.
6//!
7//! Both handlers are pure pass-through: state is untouched, the
8//! reducer just translates the wire command into the typed event
9//! so saga subscribers (in the saga coordinator's bus loop) can react.
10
11use agentmux_common::ipc::Event;
12
13use crate::state::State;
14
15/// Phase F.6 — host-emitted signal that browser-pane HWNDs for a
16/// closing top-level window have been reaped. Pure pass-through:
17/// state stays untouched (the host owns pane bookkeeping); the
18/// reducer just translates the wire command into the typed event so
19/// the window-cleanup-cascade saga can advance.
20///
21/// Idempotent / context-free: the saga matches the `label` against
22/// its own `closed_label`, so a stray report for a label that no
23/// in-flight saga is tracking is a harmless broadcast.
24pub(super) fn handle_report_panes_reaped(
25 state: &mut State,
26 label: String,
27 saga_id: Option<u64>,
28) -> Vec<Event> {
29 // No state.windows gate — round 4's gate had an ordering bug:
30 // host sends ReportWindowClosed BEFORE ReportPanesReaped on the
31 // same channel, so by the time the reducer processes this, the
32 // label is already gone from state.windows (closed by the prior
33 // command's reducer arm). The gate then dropped EVERY
34 // PanesReaped, leaving the F.6 saga stuck in WaitingForPanesReaped
35 // indefinitely. Round 5 reversal: emit unconditionally; for
36 // unpromoted-pool drains where no saga is in flight, the event
37 // appears stray on the bus but is harmless (no subscriber acts
38 // on it). Cosmetic only; correct saga lifecycle restored.
39 //
40 // CPD-1: `saga_id` flows through unchanged (None for organic
41 // reports, Some(N) once CPD-3 hosts echo back the saga's id).
42 let v = state.bump_version();
43 vec![Event::PanesReaped {
44 label,
45 version: v,
46 saga_id,
47 }]
48}
49
50/// Phase CPD-1 — host reported a saga-issued action failed. Pure
51/// pass-through translation into `Event::SagaActionFailed`. The
52/// saga coordinator's bus loop will (CPD-3) treat the event as a
53/// terminal signal for the matching `saga_id` and emit
54/// `Event::SagaFailed`, dropping the saga from in-flight.
55pub(super) fn handle_report_saga_action_failed(
56 state: &mut State,
57 saga_id: u64,
58 reason: String,
59) -> Vec<Event> {
60 let v = state.bump_version();
61 vec![Event::SagaActionFailed {
62 saga_id,
63 reason,
64 version: v,
65 }]
66}