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}