Skip to main content

rust_supervisor/event/
payload.rs

1//! Lifecycle event payloads and event envelopes.
2//!
3//! This module owns the observable shape of supervisor lifecycle facts. It keeps
4//! payloads typed so state, journal, metrics, and tests do not infer behavior
5//! from strings.
6
7use crate::child_runner::run_exit::TaskExit;
8use crate::control::outcome::{
9    ChildAttemptStatus, ChildControlFailurePhase, ChildControlOperation, ChildControlResult,
10    ChildStopState, RestartLimitState, StaleReportHandling,
11};
12use crate::error::types::TaskFailure;
13use crate::event::time::{CorrelationId, EventSequence, When};
14use crate::id::types::{ChildId, ChildStartCount, Generation, SupervisorPath};
15use crate::policy::task_role_defaults::{PolicySource, TaskRole};
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19/// Wrapper around [`f64`] that implements [`Eq`] via bit comparison.
20///
21/// NaN is disallowed. If a NaN value is constructed at runtime, equality
22/// panics. This type exists solely to satisfy the `Eq` bound on the `What`
23/// enum and should not be used outside this module.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(transparent)]
26pub struct FiniteF64(#[serde(with = "finite_f64_serde")] f64);
27
28impl Eq for FiniteF64 {}
29
30impl FiniteF64 {
31    /// Creates a `FiniteF64` from a raw `f64`.
32    ///
33    /// # Panics
34    ///
35    /// Panics if `value` is NaN or infinity.
36    pub fn new(value: f64) -> Self {
37        assert!(
38            value.is_finite(),
39            "FiniteF64 requires a finite value, got {value}"
40        );
41        Self(value)
42    }
43
44    /// Returns the inner `f64` value.
45    pub fn into_inner(self) -> f64 {
46        self.0
47    }
48}
49
50impl From<f64> for FiniteF64 {
51    /// Creates a `FiniteF64` from a raw `f64`.
52    ///
53    /// # Panics
54    ///
55    /// Panics if `value` is NaN.
56    fn from(value: f64) -> Self {
57        Self::new(value)
58    }
59}
60
61/// Serde helper that serializes `FiniteF64` as a plain JSON number.
62mod finite_f64_serde {
63    use serde::{Deserialize, Deserializer, Serialize, Serializer};
64
65    /// Serializes an `f64` as a plain JSON number.
66    pub fn serialize<S: Serializer>(value: &f64, serializer: S) -> Result<S::Ok, S::Error> {
67        value.serialize(serializer)
68    }
69
70    /// Deserializes an `f64` from a JSON number, rejecting NaN and infinity.
71    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
72        let value = f64::deserialize(deserializer)?;
73        if !value.is_finite() {
74            return Err(serde::de::Error::custom(format!(
75                "FiniteF64 requires a finite value, got {value}"
76            )));
77        }
78        Ok(value)
79    }
80}
81
82/// Meltdown scope identifier for failure tracking.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84pub enum MeltdownScope {
85    /// Child-level scope bound to a specific child identifier.
86    Child,
87    /// Group-level scope bound to a restart execution plan group.
88    Group,
89    /// Supervisor-level scope bound to the supervisor instance boundary.
90    Supervisor,
91}
92
93impl std::fmt::Display for MeltdownScope {
94    /// Formats the meltdown scope as a string.
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Child => write!(f, "child"),
98            Self::Group => write!(f, "group"),
99            Self::Supervisor => write!(f, "supervisor"),
100        }
101    }
102}
103
104/// Protection restrictiveness ladder defining escalation severity levels.
105///
106/// This enum defines six protection tiers from least to most restrictive.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
108pub enum ProtectionAction {
109    /// Restart is allowed without restrictions.
110    RestartAllowed,
111    /// Restart is queued behind concurrency throttle gates.
112    RestartQueued,
113    /// Restart is denied due to policy limits.
114    RestartDenied,
115    /// Supervision is paused temporarily.
116    SupervisionPaused,
117    /// Failure is escalated to parent supervisor.
118    Escalated,
119    /// Supervised stop is enforced for the child.
120    SupervisedStop,
121}
122
123impl std::fmt::Display for ProtectionAction {
124    /// Formats the protection action as a string.
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            Self::RestartAllowed => write!(f, "restart_allowed"),
128            Self::RestartQueued => write!(f, "restart_queued"),
129            Self::RestartDenied => write!(f, "restart_denied"),
130            Self::SupervisionPaused => write!(f, "supervision_paused"),
131            Self::Escalated => write!(f, "escalated"),
132            Self::SupervisedStop => write!(f, "supervised_stop"),
133        }
134    }
135}
136
137/// Reason for cold start budget triggering or exhaustion.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub enum ColdStartReason {
140    /// Cold start budget has not been triggered.
141    NotApplicable,
142    /// Initial startup within cold start window.
143    InitialStartup,
144    /// Cold start budget exhausted within time window.
145    BudgetExhausted,
146    /// Too many restarts during cold start period.
147    ExcessiveRestarts,
148}
149
150impl std::fmt::Display for ColdStartReason {
151    /// Formats the cold start reason as a string.
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            Self::NotApplicable => write!(f, "not_applicable"),
155            Self::InitialStartup => write!(f, "initial_startup"),
156            Self::BudgetExhausted => write!(f, "budget_exhausted"),
157            Self::ExcessiveRestarts => write!(f, "excessive_restarts"),
158        }
159    }
160}
161
162/// Reason for hot loop detection triggering.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub enum HotLoopReason {
165    /// Hot loop detection has not been triggered.
166    NotApplicable,
167    /// Rapid crash detected within sliding time window.
168    RapidCrashDetected,
169    /// Crash-restart cycle exceeded threshold frequency.
170    CycleThresholdExceeded,
171    /// Insufficient stable runtime between restarts.
172    InsufficientStableRuntime,
173}
174
175impl std::fmt::Display for HotLoopReason {
176    /// Formats the hot loop reason as a string.
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        match self {
179            Self::NotApplicable => write!(f, "not_applicable"),
180            Self::RapidCrashDetected => write!(f, "rapid_crash_detected"),
181            Self::CycleThresholdExceeded => write!(f, "cycle_threshold_exceeded"),
182            Self::InsufficientStableRuntime => write!(f, "insufficient_stable_runtime"),
183        }
184    }
185}
186
187/// Ownership of the throttle gate that limited concurrent restarts.
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub enum ThrottleGateOwner {
190    /// No throttle gate was active.
191    None,
192    /// Instance-global supervisor throttle gate.
193    SupervisorInstance,
194    /// Group-level throttle gate with group identifier.
195    Group(String),
196}
197
198impl std::fmt::Display for ThrottleGateOwner {
199    /// Formats the throttle gate owner as a string.
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        match self {
202            Self::None => write!(f, "none"),
203            Self::SupervisorInstance => write!(f, "supervisor_global"),
204            Self::Group(group) => write!(f, "group:{group}"),
205        }
206    }
207}
208
209/// Location data attached to a supervisor event.
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct Where {
212    /// Stable supervisor path that owns the fact.
213    pub supervisor_path: SupervisorPath,
214    /// Parent child identifier when the fact belongs to a nested node.
215    pub parent_id: Option<ChildId>,
216    /// Child identifier related to the fact.
217    pub child_id: Option<ChildId>,
218    /// Human-readable child name.
219    pub child_name: Option<String>,
220    /// Tokio task identifier when it is available.
221    pub tokio_task_id: Option<String>,
222    /// Host name reported by the runtime.
223    pub host: Option<String>,
224    /// Process identifier that emitted the event.
225    pub pid: u32,
226    /// Current thread name when available.
227    pub thread_name: Option<String>,
228    /// Rust module path that emitted the event.
229    pub module_path: Option<String>,
230    /// Source file that emitted the event.
231    pub source_file: Option<String>,
232    /// Source line that emitted the event.
233    pub source_line: Option<u32>,
234}
235
236impl Where {
237    /// Creates a location for a supervisor path.
238    ///
239    /// # Arguments
240    ///
241    /// - `supervisor_path`: Path that owns this lifecycle fact.
242    ///
243    /// # Returns
244    ///
245    /// Returns a [`Where`] value with process and thread defaults.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// let location = rust_supervisor::event::payload::Where::new(
251    ///     rust_supervisor::id::types::SupervisorPath::root(),
252    /// );
253    /// assert_eq!(location.supervisor_path.to_string(), "/");
254    /// ```
255    pub fn new(supervisor_path: SupervisorPath) -> Self {
256        Self {
257            supervisor_path,
258            parent_id: None,
259            child_id: None,
260            child_name: None,
261            tokio_task_id: None,
262            host: None,
263            pid: std::process::id(),
264            thread_name: std::thread::current().name().map(ToOwned::to_owned),
265            module_path: None,
266            source_file: None,
267            source_line: None,
268        }
269    }
270
271    /// Adds child identity to the location.
272    ///
273    /// # Arguments
274    ///
275    /// - `child_id`: Stable child identifier.
276    /// - `child_name`: Human-readable child name.
277    ///
278    /// # Returns
279    ///
280    /// Returns the updated [`Where`] value.
281    pub fn with_child(mut self, child_id: ChildId, child_name: impl Into<String>) -> Self {
282        self.child_id = Some(child_id);
283        self.child_name = Some(child_name.into());
284        self
285    }
286}
287
288/// State transition recorded by an event payload.
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct StateTransition {
291    /// State before the transition.
292    pub from: String,
293    /// State after the transition.
294    pub to: String,
295}
296
297impl StateTransition {
298    /// Creates a state transition description.
299    ///
300    /// # Arguments
301    ///
302    /// - `from`: Previous state name.
303    /// - `to`: New state name.
304    ///
305    /// # Returns
306    ///
307    /// Returns a [`StateTransition`].
308    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
309        Self {
310            from: from.into(),
311            to: to.into(),
312        }
313    }
314}
315
316/// Policy decision data stored with an event.
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct PolicyDecision {
319    /// Low-cardinality decision name.
320    pub decision: String,
321    /// Delay in milliseconds when restart is delayed.
322    pub delay_ms: Option<u64>,
323    /// Human-readable reason for diagnostics.
324    pub reason: Option<String>,
325}
326
327impl PolicyDecision {
328    /// Creates a policy decision value.
329    ///
330    /// # Arguments
331    ///
332    /// - `decision`: Low-cardinality decision name.
333    /// - `delay_ms`: Optional delay in milliseconds.
334    /// - `reason`: Optional diagnostic reason.
335    ///
336    /// # Returns
337    ///
338    /// Returns a [`PolicyDecision`].
339    pub fn new(decision: impl Into<String>, delay_ms: Option<u64>, reason: Option<String>) -> Self {
340        Self {
341            decision: decision.into(),
342            delay_ms,
343            reason,
344        }
345    }
346}
347
348/// Command audit data attached to command lifecycle events.
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350pub struct CommandAudit {
351    /// Stable command identifier.
352    pub command_id: String,
353    /// Caller that requested the command.
354    pub requested_by: String,
355    /// Operator-provided reason.
356    pub reason: String,
357    /// Target path for the command.
358    pub target_path: SupervisorPath,
359    /// Accepted time in nanoseconds since the Unix epoch.
360    pub accepted_at_unix_nanos: u128,
361    /// Command result summary.
362    pub result: String,
363}
364
365/// Typed payload for supervisor lifecycle events.
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
367#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
368pub enum What {
369    /// Child is being started.
370    ChildStarting {
371        /// Optional state transition carried by this event.
372        transition: Option<StateTransition>,
373    },
374    /// Child is running.
375    ChildRunning {
376        /// Optional state transition carried by this event.
377        transition: Option<StateTransition>,
378    },
379    /// Child is ready.
380    ChildReady {
381        /// Optional state transition carried by this event.
382        transition: Option<StateTransition>,
383    },
384    /// Child emitted a heartbeat.
385    ChildHeartbeat {
386        /// Heartbeat age in milliseconds.
387        age_ms: u64,
388    },
389    /// Child failed with a typed failure.
390    ChildFailed {
391        /// Failure payload reported by the task.
392        failure: TaskFailure,
393    },
394    /// Child panicked.
395    ChildPanicked {
396        /// Panic category used for metrics.
397        category: String,
398    },
399    /// Restart backoff was scheduled.
400    BackoffScheduled {
401        /// Backoff delay in milliseconds.
402        delay_ms: u64,
403    },
404    /// Child is restarting.
405    ChildRestarting {
406        /// Restart generation after the transition.
407        generation: u64,
408    },
409    /// Child restarted.
410    ChildRestarted {
411        /// Restart count for the child window.
412        restart_count: u64,
413    },
414    /// Child was quarantined.
415    ChildQuarantined {
416        /// Quarantine reason.
417        reason: String,
418    },
419    /// Child stopped.
420    ChildStopped {
421        /// Exit reason.
422        reason: String,
423    },
424    /// Child became unhealthy.
425    ChildUnhealthy {
426        /// Unhealthy reason.
427        reason: String,
428    },
429    /// Meltdown fuse was tripped.
430    Meltdown {
431        /// Scope that tripped the fuse.
432        scope: String,
433    },
434    /// Shutdown was requested.
435    ShutdownRequested {
436        /// Shutdown cause.
437        cause: String,
438    },
439    /// Shutdown phase changed.
440    ShutdownPhaseChanged {
441        /// Previous phase name.
442        from: String,
443        /// New phase name.
444        to: String,
445    },
446    /// Shutdown completed.
447    ShutdownCompleted {
448        /// Final shutdown phase.
449        phase: String,
450        /// Shutdown result summary.
451        result: String,
452        /// Full pipeline duration in milliseconds.
453        duration_ms: u64,
454    },
455    /// Child shutdown cancel delivered for one supervised child_start_count during shutdown draining.
456    ChildShutdownCancelDelivered {
457        /// Child that received cancellation.
458        child_id: ChildId,
459        /// Generation associated with the child child_start_count.
460        generation: Generation,
461        /// ChildStartCount associated with the child run.
462        child_start_count: ChildStartCount,
463        /// Shutdown phase that delivered cancellation.
464        phase: String,
465    },
466    /// Child finished during graceful shutdown draining.
467    ChildShutdownGraceful {
468        /// Child that completed gracefully.
469        child_id: ChildId,
470        /// Generation associated with the child child_start_count.
471        generation: Generation,
472        /// ChildStartCount associated with the child run.
473        child_start_count: ChildStartCount,
474        /// Shutdown phase that recorded the outcome.
475        phase: String,
476        /// Exit classification reported by the child.
477        exit: String,
478    },
479    /// Child was aborted during shutdown.
480    ChildShutdownAborted {
481        /// Child that was aborted.
482        child_id: ChildId,
483        /// Generation associated with the child child_start_count.
484        generation: Generation,
485        /// ChildStartCount associated with the child run.
486        child_start_count: ChildStartCount,
487        /// Shutdown phase that recorded the outcome.
488        phase: String,
489        /// Low-cardinality abort result.
490        result: String,
491        /// Human-readable abort reason.
492        reason: String,
493    },
494    /// Child reported after its normal shutdown accounting window.
495    ChildShutdownLateReport {
496        /// Child that produced a late report.
497        child_id: ChildId,
498        /// Generation associated with the child child_start_count.
499        generation: Generation,
500        /// ChildStartCount associated with the child run.
501        child_start_count: ChildStartCount,
502        /// Shutdown phase that received the late report.
503        phase: String,
504        /// Exit classification reported by the child.
505        exit: String,
506    },
507    /// Generation fence engaged for an accepted manual restart waiting for an old attempt to stop.
508    ChildRestartFenceEntered {
509        /// Child awaiting restart isolation.
510        child_id: ChildId,
511        /// Old generation pinned until the fence releases.
512        old_generation: Generation,
513        /// Old attempt pinned until the fence releases.
514        old_attempt: ChildStartCount,
515        /// Target generation queued after the old attempt completes.
516        target_generation: Generation,
517        /// Command identifier tying this fence to auditing metadata.
518        command_id: String,
519        /// Restart requester captured from command metadata.
520        requested_by: String,
521        /// Restart reason captured from command metadata.
522        reason: String,
523        /// Deadline for cooperative stop before escalation to abort paths.
524        stop_deadline_at_unix_nanos: u128,
525    },
526    /// Runtime escalated restart isolation to abort the old attempt after the cooperative deadline elapsed.
527    ChildRestartFenceAbortRequested {
528        /// Child awaiting restart isolation.
529        child_id: ChildId,
530        /// Old generation that failed to exit before the graceful deadline expired.
531        old_generation: Generation,
532        /// Old attempt that failed to exit before the graceful deadline expired.
533        old_attempt: ChildStartCount,
534        /// Target generation queued for start after isolation completes.
535        target_generation: Generation,
536        /// Command identifier tied to the pending restart bookkeeping.
537        command_id: String,
538        /// Deadline that triggered the abort escalation.
539        deadline_unix_nanos: u128,
540    },
541    /// Old attempt completed and a new generation may start under the pending restart request.
542    ChildRestartFenceReleased {
543        /// Child whose fence released.
544        child_id: ChildId,
545        /// Old generation that fully stopped.
546        old_generation: Generation,
547        /// Old attempt that fully stopped.
548        old_attempt: ChildStartCount,
549        /// Target generation allowed to start after this release.
550        target_generation: Generation,
551        /// Exit classification reported for the old attempt.
552        exit_kind: TaskExit,
553    },
554    /// Conflicting restart intent that was merged, rejected, or superseded by policy.
555    ChildRestartConflict {
556        /// Child identifier for the fencing scope.
557        child_id: ChildId,
558        /// Generation that was active or pinned when the conflict was classified.
559        current_generation: Option<Generation>,
560        /// Attempt counter that was active or pinned when the conflict was classified.
561        current_attempt: Option<ChildStartCount>,
562        /// Generation the caller wanted to reach, if applicable.
563        target_generation: Option<Generation>,
564        /// Command identifier supplied by the caller when present.
565        command_id: String,
566        /// Low-cardinality conflict classifier (`already_pending`, `rejected`, ...).
567        decision: String,
568        /// Human-readable reason for observability dumps.
569        reason: String,
570    },
571    /// Stale completion triple observed after authoritative state moved forward.
572    ChildAttemptStaleReport {
573        /// Child identifier tied to the completion triple.
574        child_id: ChildId,
575        /// Generation carried by the stale completion report.
576        reported_generation: Generation,
577        /// Attempt counter carried by the stale completion report.
578        reported_attempt: ChildStartCount,
579        /// Generation considered authoritative when the stale report arrived.
580        current_generation: Option<Generation>,
581        /// Attempt counter considered authoritative when the stale report arrived.
582        current_attempt: Option<ChildStartCount>,
583        /// Exit classification supplied by the stale report.
584        exit_kind: TaskExit,
585        /// Runtime-selected handling bucket for metrics and audits.
586        handled_as: StaleReportHandling,
587    },
588    /// Pending restart bookkeeping drained because the pinned old attempt exited.
589    ChildRestartFencePendingDrained {
590        /// Child whose pending restart advanced past the cooperative stop barrier.
591        child_id: ChildId,
592    },
593    /// Child control command delivered cancellation.
594    ChildControlCancelDelivered {
595        /// Child that received cancellation.
596        child_id: ChildId,
597        /// Generation that received cancellation.
598        generation: Generation,
599        /// Attempt that received cancellation.
600        attempt: ChildStartCount,
601        /// Control command name.
602        command: String,
603        /// Control command identifier.
604        command_id: String,
605    },
606    /// Child control stop completed.
607    ChildControlStopCompleted {
608        /// Child that completed stopping.
609        child_id: ChildId,
610        /// Generation that completed stopping.
611        generation: Generation,
612        /// Attempt that completed stopping.
613        attempt: ChildStartCount,
614        /// Child exit classification.
615        exit_kind: TaskExit,
616    },
617    /// Child control stop failed.
618    ChildControlStopFailed {
619        /// Child that failed to stop.
620        child_id: ChildId,
621        /// Generation that failed to stop.
622        generation: Generation,
623        /// Attempt that failed to stop.
624        attempt: ChildStartCount,
625        /// Current attempt status.
626        status: ChildAttemptStatus,
627        /// Current stop progress.
628        stop_state: ChildStopState,
629        /// Control failure phase.
630        phase: ChildControlFailurePhase,
631        /// Human-readable failure reason.
632        reason: String,
633        /// Whether callers can retry to recover.
634        recoverable: bool,
635    },
636    /// Child control operation changed.
637    ChildControlOperationChanged {
638        /// Child whose operation changed.
639        child_id: ChildId,
640        /// Previous operation.
641        from: ChildControlOperation,
642        /// New operation.
643        to: ChildControlOperation,
644        /// Control command name.
645        command: String,
646        /// Control command identifier.
647        command_id: String,
648    },
649    /// Child control command completed with a full outcome.
650    ChildControlCommandCompleted {
651        /// Child that the command targeted.
652        child_id: ChildId,
653        /// Stable control command name.
654        command: String,
655        /// Control command identifier.
656        command_id: String,
657        /// Caller that requested the command.
658        requested_by: String,
659        /// Operator-provided reason.
660        reason: String,
661        /// Low-cardinality command result.
662        result: String,
663        /// Full control outcome.
664        outcome: Box<ChildControlResult>,
665    },
666    /// Child restart limit accounting was refreshed.
667    ChildRuntimeRestartLimitUpdated {
668        /// Child whose restart limit accounting changed.
669        child_id: ChildId,
670        /// Updated restart limit state.
671        restart_limit: RestartLimitState,
672    },
673    /// Child runtime state record was removed.
674    ChildRuntimeStateRemoved {
675        /// Removed child.
676        child_id: ChildId,
677        /// Child path in the supervisor tree.
678        path: SupervisorPath,
679        /// Final attempt status.
680        final_status: Option<ChildAttemptStatus>,
681    },
682    /// Child heartbeat became stale.
683    ChildHeartbeatStale {
684        /// Child with a stale heartbeat.
685        child_id: ChildId,
686        /// Attempt with a stale heartbeat.
687        attempt: ChildStartCount,
688        /// Last heartbeat timestamp in Unix epoch nanoseconds.
689        since_unix_nanos: u128,
690    },
691    /// Control command was accepted.
692    CommandAccepted {
693        /// Command audit payload.
694        audit: CommandAudit,
695    },
696    /// Control command completed.
697    CommandCompleted {
698        /// Command audit payload.
699        audit: CommandAudit,
700    },
701    /// Runtime control loop started.
702    RuntimeControlLoopStarted {
703        /// Startup phase label.
704        phase: String,
705        /// Startup time in Unix epoch nanoseconds.
706        started_at_unix_nanos: u128,
707    },
708    /// Runtime control loop shutdown was requested.
709    RuntimeControlLoopShutdownRequested {
710        /// Stable command identifier.
711        command_id: String,
712        /// Caller that requested shutdown.
713        requested_by: String,
714        /// Operator-provided reason.
715        reason: String,
716    },
717    /// Runtime control loop completed normally.
718    RuntimeControlLoopCompleted {
719        /// Completion phase label.
720        phase: String,
721        /// Completion reason.
722        reason: String,
723        /// Completion time in Unix epoch nanoseconds.
724        completed_at_unix_nanos: u128,
725    },
726    /// Runtime control loop failed.
727    RuntimeControlLoopFailed {
728        /// Failure phase label.
729        phase: String,
730        /// Failure reason.
731        reason: String,
732        /// Whether failure came from panic.
733        panic: bool,
734        /// Whether a new supervisor can recover.
735        recoverable: bool,
736    },
737    /// Runtime control loop join completed.
738    RuntimeControlLoopJoinCompleted {
739        /// Stable command identifier.
740        command_id: String,
741        /// Caller that requested join.
742        requested_by: String,
743        /// Final state label.
744        state: String,
745        /// Final phase label.
746        phase: String,
747        /// Final reason.
748        reason: String,
749    },
750    /// Event subscriber lagged.
751    SubscriberLagged {
752        /// Number of missed events.
753        missed: u64,
754    },
755    /// Restart budget exhausted for a child.
756    BudgetExhausted {
757        /// Child whose budget ran out.
758        child_id: ChildId,
759        /// Nanoseconds to wait before retrying.
760        retry_after_ns: u128,
761        /// Source group that triggered the budget check (when applicable).
762        budget_source_group: Option<String>,
763    },
764    /// Group meltdown fuse was triggered.
765    GroupFuseTriggered {
766        /// Group that entered meltdown.
767        group_name: String,
768        /// Group from which the fuse propagated (when applicable).
769        propagated_from_group: Option<String>,
770    },
771    /// Escalation path bifurcation between critical and optional children.
772    EscalationBifurcated {
773        /// Severity classification for the escalation decision.
774        severity: String,
775        /// Budget verdict at the time of escalation (when available).
776        budget_verdict: Option<String>,
777        /// Meltdown outcome at the time of escalation (when available).
778        fuse_outcome: Option<String>,
779        /// Reason for tie-breaking (when applicable).
780        tie_break_reason: Option<String>,
781    },
782    /// Starvation alert emitted by the fairness probe (US1).
783    FairnessProbeStarvation {
784        /// The child that has been starved.
785        starved_child_id: ChildId,
786        /// How many scheduling opportunities were missed.
787        skip_count: u64,
788        /// Start of the probe window (Unix nanos).
789        probe_start_unix_nanos: u128,
790        /// End of the probe window (Unix nanos).
791        probe_end_unix_nanos: u128,
792    },
793    /// Restart budget denied by policy.
794    BudgetDenied {
795        /// Group associated with the budget check.
796        group: Option<String>,
797        /// Reason for the denial.
798        reason: String,
799        /// Remaining budget ratio.
800        budget_remaining: FiniteF64,
801    },
802    /// Generation fence engaged for child restart isolation.
803    GenerationFenced {
804        /// Old generation that was fenced.
805        old_generation: u64,
806        /// New generation that was allowed.
807        new_generation: u64,
808        /// Reason for the fence.
809        reason: String,
810    },
811    /// Health check passed for a child.
812    HealthCheckPassed {
813        /// Time since last check in milliseconds.
814        age_ms: u64,
815        /// Wall clock time when the child became healthy.
816        healthy_since_unix_nanos: u128,
817    },
818    /// Health check failed for a child.
819    HealthCheckFailed {
820        /// Failure reason.
821        reason: String,
822        /// Consecutive failure count.
823        consecutive_failures: u32,
824    },
825    /// Supervision paused for a child or group.
826    Paused {
827        /// Pause reason.
828        reason: String,
829        /// Caller that initiated the pause.
830        paused_by: String,
831    },
832    /// Supervision resumed for a child or group.
833    Resumed {
834        /// Resume reason.
835        reason: String,
836    },
837    /// Child or group was quarantined.
838    Quarantined {
839        /// Meltdown scope that triggered quarantine.
840        scope: MeltdownScope,
841        /// Quarantine reason.
842        reason: String,
843        /// Quarantine duration in seconds.
844        duration_secs: u64,
845    },
846    /// Backpressure alert emitted when subscriber buffer exceeds soft threshold.
847    BackpressureAlert {
848        /// Subscriber name or identifier.
849        subscriber: String,
850        /// Current buffer occupancy percentage.
851        buffer_pct: u8,
852        /// Threshold that triggered the alert.
853        threshold_pct: u8,
854    },
855    /// Backpressure degradation when subscriber buffer exceeds hard threshold.
856    BackpressureDegradation {
857        /// Subscriber name or identifier.
858        subscriber: String,
859        /// Active backpressure strategy.
860        strategy: String,
861        /// Current sampling ratio.
862        sample_ratio: FiniteF64,
863        /// Peak buffer occupancy during the degradation window.
864        buffer_peak_pct: u8,
865        /// Whether the subscriber has recovered.
866        recovered: bool,
867    },
868    /// Audit record for a command or lifecycle event.
869    AuditRecorded {
870        /// Command identifier.
871        command_id: String,
872        /// Event type being audited.
873        event_type: String,
874        /// Sampling ratio in effect when the audit was recorded.
875        sample_ratio: FiniteF64,
876        /// Correlation identifier linking this audit to the event chain.
877        correlation_id: CorrelationId,
878        /// Reason the audit was triggered.
879        trigger_reason: String,
880        /// Number of events discarded by sampling.
881        events_discarded: u64,
882    },
883    /// Child declaration was accepted and committed via add_child.
884    ChildDeclarationAccepted {
885        /// Transaction identifier for audit tracing.
886        transaction_id: Uuid,
887        /// Name of the accepted child.
888        child_name: String,
889        /// Runtime child identifier.
890        child_id: ChildId,
891        /// Supervisor spec hash after this operation.
892        spec_hash: String,
893    },
894    /// Child declaration was rejected via add_child.
895    ChildDeclarationRejected {
896        /// Transaction identifier for audit tracing.
897        transaction_id: Uuid,
898        /// Name of the rejected child.
899        child_name: String,
900        /// Human-readable rejection reason.
901        reason: String,
902        /// Optional JSON Pointer field path pointing to the error source.
903        field_path: Option<String>,
904    },
905}
906
907impl What {
908    /// Returns a low-cardinality event name.
909    ///
910    /// # Arguments
911    ///
912    /// This function has no arguments.
913    ///
914    /// # Returns
915    ///
916    /// Returns the stable event name.
917    ///
918    /// # Examples
919    ///
920    /// ```
921    /// let event = rust_supervisor::event::payload::What::ChildRunning {
922    ///     transition: None,
923    /// };
924    /// assert_eq!(event.name(), "ChildRunning");
925    /// ```
926    pub fn name(&self) -> &'static str {
927        match self {
928            Self::ChildStarting { .. } => "ChildStarting",
929            Self::ChildRunning { .. } => "ChildRunning",
930            Self::ChildReady { .. } => "ChildReady",
931            Self::ChildHeartbeat { .. } => "ChildHeartbeat",
932            Self::ChildFailed { .. } => "ChildFailed",
933            Self::ChildPanicked { .. } => "ChildPanicked",
934            Self::BackoffScheduled { .. } => "BackoffScheduled",
935            Self::ChildRestarting { .. } => "ChildRestarting",
936            Self::ChildRestarted { .. } => "ChildRestarted",
937            Self::ChildQuarantined { .. } => "ChildQuarantined",
938            Self::ChildStopped { .. } => "ChildStopped",
939            Self::ChildUnhealthy { .. } => "ChildUnhealthy",
940            Self::Meltdown { .. } => "Meltdown",
941            Self::ShutdownRequested { .. } => "ShutdownRequested",
942            Self::ShutdownPhaseChanged { .. } => "ShutdownPhaseChanged",
943            Self::ShutdownCompleted { .. } => "ShutdownCompleted",
944            Self::ChildShutdownCancelDelivered { .. } => "ChildShutdownCancelDelivered",
945            Self::ChildShutdownGraceful { .. } => "ChildShutdownGraceful",
946            Self::ChildShutdownAborted { .. } => "ChildShutdownAborted",
947            Self::ChildShutdownLateReport { .. } => "ChildShutdownLateReport",
948            Self::ChildRestartFenceEntered { .. } => "ChildRestartFenceEntered",
949            Self::ChildRestartFenceAbortRequested { .. } => "ChildRestartFenceAbortRequested",
950            Self::ChildRestartFenceReleased { .. } => "ChildRestartFenceReleased",
951            Self::ChildRestartConflict { .. } => "ChildRestartConflict",
952            Self::ChildAttemptStaleReport { .. } => "ChildAttemptStaleReport",
953            Self::ChildRestartFencePendingDrained { .. } => "ChildRestartFencePendingDrained",
954            Self::ChildControlCancelDelivered { .. } => "ChildControlCancelDelivered",
955            Self::ChildControlStopCompleted { .. } => "ChildControlStopCompleted",
956            Self::ChildControlStopFailed { .. } => "ChildControlStopFailed",
957            Self::ChildControlOperationChanged { .. } => "ChildControlOperationChanged",
958            Self::ChildControlCommandCompleted { .. } => "ChildControlCommandCompleted",
959            Self::ChildRuntimeRestartLimitUpdated { .. } => "ChildRuntimeRestartLimitUpdated",
960            Self::ChildRuntimeStateRemoved { .. } => "ChildRuntimeStateRemoved",
961            Self::ChildHeartbeatStale { .. } => "ChildHeartbeatStale",
962            Self::CommandAccepted { .. } => "CommandAccepted",
963            Self::CommandCompleted { .. } => "CommandCompleted",
964            Self::RuntimeControlLoopStarted { .. } => "RuntimeControlLoopStarted",
965            Self::RuntimeControlLoopShutdownRequested { .. } => {
966                "RuntimeControlLoopShutdownRequested"
967            }
968            Self::RuntimeControlLoopCompleted { .. } => "RuntimeControlLoopCompleted",
969            Self::RuntimeControlLoopFailed { .. } => "RuntimeControlLoopFailed",
970            Self::RuntimeControlLoopJoinCompleted { .. } => "RuntimeControlLoopJoinCompleted",
971            Self::SubscriberLagged { .. } => "SubscriberLagged",
972            Self::BudgetExhausted { .. } => "BudgetExhausted",
973            Self::GroupFuseTriggered { .. } => "GroupFuseTriggered",
974            Self::EscalationBifurcated { .. } => "EscalationBifurcated",
975            Self::FairnessProbeStarvation { .. } => "FairnessProbeStarvation",
976            Self::BudgetDenied { .. } => "BudgetDenied",
977            Self::GenerationFenced { .. } => "GenerationFenced",
978            Self::HealthCheckPassed { .. } => "HealthCheckPassed",
979            Self::HealthCheckFailed { .. } => "HealthCheckFailed",
980            Self::Paused { .. } => "Paused",
981            Self::Resumed { .. } => "Resumed",
982            Self::Quarantined { .. } => "Quarantined",
983            Self::BackpressureAlert { .. } => "BackpressureAlert",
984            Self::BackpressureDegradation { .. } => "BackpressureDegradation",
985            Self::AuditRecorded { .. } => "AuditRecorded",
986            Self::ChildDeclarationAccepted { .. } => "ChildDeclarationAccepted",
987            Self::ChildDeclarationRejected { .. } => "ChildDeclarationRejected",
988        }
989    }
990}
991
992/// Complete lifecycle event envelope.
993#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
994pub struct SupervisorEvent {
995    /// Schema version identifier, monotonically increasing.
996    pub schema_id: u64,
997    /// Time information for the lifecycle fact.
998    pub when: When,
999    /// Location information for the lifecycle fact.
1000    pub r#where: Where,
1001    /// Typed event payload.
1002    pub what: What,
1003    /// Optional policy decision related to the event.
1004    pub policy: Option<PolicyDecision>,
1005    /// Monotonic event sequence.
1006    pub sequence: EventSequence,
1007    /// Correlation identifier shared by related signals.
1008    pub correlation_id: CorrelationId,
1009    /// Configuration version that produced this fact.
1010    pub config_version: u64,
1011    /// List of meltdown scopes that reached or exceeded thresholds in this evaluation round.
1012    pub scopes_triggered: Vec<MeltdownScope>,
1013    /// The dominant attribution scope for the effective meltdown verdict.
1014    pub lead_scope: Option<MeltdownScope>,
1015    /// The effective protection action on the restrictiveness ladder.
1016    pub effective_protective_action: Option<ProtectionAction>,
1017    /// Reason for cold start budget triggering or exhaustion.
1018    pub cold_start_reason: ColdStartReason,
1019    /// Reason for hot loop detection triggering.
1020    pub hot_loop_reason: HotLoopReason,
1021    /// Ownership of the throttle gate that limited concurrent restarts.
1022    pub throttle_gate_owner: ThrottleGateOwner,
1023    /// Effective task role used by the policy decision.
1024    pub task_role: Option<TaskRole>,
1025    /// Whether fallback task role defaults were used.
1026    pub used_fallback_default: bool,
1027    /// Source that produced the effective policy.
1028    pub effective_policy_source: Option<PolicySource>,
1029}
1030
1031impl SupervisorEvent {
1032    /// Creates a supervisor lifecycle event.
1033    ///
1034    /// # Arguments
1035    ///
1036    /// - `when`: Event timing.
1037    /// - `r#where`: Event location.
1038    /// - `what`: Event payload.
1039    /// - `sequence`: Monotonic event sequence.
1040    /// - `correlation_id`: Correlation identifier for related signals.
1041    /// - `config_version`: Configuration version for this event.
1042    ///
1043    /// # Returns
1044    ///
1045    /// Returns a [`SupervisorEvent`].
1046    ///
1047    /// # Examples
1048    ///
1049    /// ```
1050    /// let event = rust_supervisor::event::payload::SupervisorEvent::new(
1051    ///     rust_supervisor::event::time::When::new(
1052    ///         rust_supervisor::event::time::EventTime::deterministic(
1053    ///             1,
1054    ///             1,
1055    ///             0,
1056    ///             rust_supervisor::id::types::Generation::initial(),
1057    ///             rust_supervisor::id::types::ChildStartCount::first(),
1058    ///         ),
1059    ///     ),
1060    ///     rust_supervisor::event::payload::Where::new(
1061    ///         rust_supervisor::id::types::SupervisorPath::root(),
1062    ///     ),
1063    ///     rust_supervisor::event::payload::What::ChildRunning { transition: None },
1064    ///     rust_supervisor::event::time::EventSequence::new(1),
1065    ///     rust_supervisor::event::time::CorrelationId::from_uuid(uuid::Uuid::nil()),
1066    ///     1,
1067    /// );
1068    /// assert_eq!(event.what.name(), "ChildRunning");
1069    /// ```
1070    pub fn new(
1071        when: When,
1072        r#where: Where,
1073        what: What,
1074        sequence: EventSequence,
1075        correlation_id: CorrelationId,
1076        config_version: u64,
1077    ) -> Self {
1078        Self {
1079            schema_id: 1,
1080            when,
1081            r#where,
1082            what,
1083            policy: None,
1084            sequence,
1085            correlation_id,
1086            config_version,
1087            scopes_triggered: Vec::new(),
1088            lead_scope: None,
1089            effective_protective_action: None,
1090            cold_start_reason: ColdStartReason::NotApplicable,
1091            hot_loop_reason: HotLoopReason::NotApplicable,
1092            throttle_gate_owner: ThrottleGateOwner::None,
1093            task_role: None,
1094            used_fallback_default: false,
1095            effective_policy_source: None,
1096        }
1097    }
1098
1099    /// Attaches a policy decision to an event.
1100    ///
1101    /// # Arguments
1102    ///
1103    /// - `policy`: Policy decision produced for this lifecycle fact.
1104    ///
1105    /// # Returns
1106    ///
1107    /// Returns the updated [`SupervisorEvent`].
1108    pub fn with_policy(mut self, policy: PolicyDecision) -> Self {
1109        self.policy = Some(policy);
1110        self
1111    }
1112}