agentmux_srv\reducer/
lifecycle.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4use agentmux_common::ipc::{ErrorCode, Event};
5
6use crate::state::State;
7
8use super::Ctx;
9
10use agentmux_common::ipc::{ClientKind, LifecyclePhase};
11use crate::state::{ProcessRecord, ProcessState};
12
13pub(super) fn handle_register(
14    state: &mut State,
15    ctx: &Ctx,
16    kind: ClientKind,
17    pid: u32,
18    version: String,
19) -> Vec<Event> {
20    // Idempotent on duplicate Register from the same PID — preserve
21    // the original record (per launcher's pattern). Accept fresh
22    // Registers only when the PID has no record OR the existing
23    // record is Exited (PID recycled).
24    let prior_state = state.processes.get(&pid).map(|r| r.state);
25    let allow_register = match prior_state {
26        None => true,
27        Some(ProcessState::Exited { .. }) => true,
28        _ => false,
29    };
30    if !allow_register {
31        let v = state.bump_version();
32        return vec![Event::Error {
33            code: ErrorCode::AlreadyRegistered,
34            message: format!("pid {} is already registered with srv", pid),
35            fatal: false,
36            version: v,
37        }];
38    }
39
40    let mut out = Vec::with_capacity(3);
41
42    // Insert the process record.
43    state.processes.insert(
44        pid,
45        ProcessRecord {
46            pid,
47            kind,
48            state: ProcessState::Running,
49            spawned_at: ctx.now_rfc3339.clone(),
50            version: version.clone(),
51        },
52    );
53    let v = state.bump_version();
54    out.push(Event::ProcessSpawned {
55        pid,
56        kind,
57        client_version: version,
58        version: v,
59    });
60
61    // First Register transitions srv to Running. Same lifecycle
62    // pattern as launcher.
63    if state.lifecycle == LifecyclePhase::Starting {
64        state.lifecycle = LifecyclePhase::Running;
65        let v = state.bump_version();
66        out.push(Event::LifecyclePhaseChanged {
67            from: LifecyclePhase::Starting,
68            to: LifecyclePhase::Running,
69            version: v,
70        });
71    }
72
73    let client_id = state.alloc_client_id();
74    let v = state.bump_version();
75    // Sentinel launcher_pid / launcher_version on Registered — IPC
76    // server patches these to the real srv identity before broadcast.
77    out.push(Event::Registered {
78        client_id,
79        launcher_pid: 0,
80        launcher_version: String::new(),
81        version: v,
82    });
83    out
84}
85
86pub(super) fn handle_goodbye(state: &mut State, ctx: &Ctx) -> Vec<Event> {
87    let Some(pid) = ctx.registered_pid else {
88        return Vec::new();
89    };
90    let Some(record) = state.processes.get_mut(&pid) else {
91        return Vec::new();
92    };
93    if matches!(record.state, ProcessState::Exited { .. }) {
94        return Vec::new();
95    }
96    record.state = ProcessState::Exited { code: 0 };
97    let v = state.bump_version();
98    vec![Event::ProcessExited {
99        pid,
100        code: 0,
101        version: v,
102    }]
103}