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}