Skip to main content

kernel/
stall.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The sole stall criterion. Watchdog consumes the
7//! verdict and decides disposal; it never re-derives the criterion —
8//! "what counts as stalled" lives here and nowhere else.
9//! The part `crates/kernel/spec/Stall.lean` specifies this module.
10
11use crate::consts_policy::LOOP_REPEAT_THRESHOLD;
12use crate::locator::B3Hash;
13
14/// Fingerprint of one action's canonical bytes. The only content-hash
15/// producer is `B3Hash::digest`; this newtype names the purpose.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ActionFingerprint(B3Hash);
18
19impl ActionFingerprint {
20    pub fn derive(action_canonical: &[u8]) -> ActionFingerprint {
21        ActionFingerprint(B3Hash::digest(action_canonical))
22    }
23}
24
25/// Deliberately exhaustive verdict.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StallVerdict {
28    Ok,
29    Stall { repeats: u32 },
30}
31
32/// The sample is the recent fingerprints in time order. Only the *tail*
33/// run of identical prints counts: an older repetition broken by a newer
34/// different action already recovered itself. `repeats` saturates by
35/// contract — its only consumer compares against the threshold.
36pub fn observe(recent: &[ActionFingerprint]) -> StallVerdict {
37    let Some(last) = recent.last() else {
38        return StallVerdict::Ok;
39    };
40    let mut repeats: u32 = 0;
41    for print in recent.iter().rev() {
42        if print == last {
43            repeats = repeats.saturating_add(1);
44        } else {
45            break;
46        }
47    }
48    if repeats >= LOOP_REPEAT_THRESHOLD {
49        StallVerdict::Stall { repeats }
50    } else {
51        StallVerdict::Ok
52    }
53}
54
55#[cfg(test)]
56#[allow(
57    clippy::unwrap_used,
58    clippy::expect_used,
59    clippy::panic,
60    clippy::indexing_slicing,
61    reason = "test code"
62)]
63mod tests {
64    use super::*;
65    use proptest::prelude::*;
66
67    fn print(label: &str) -> ActionFingerprint {
68        ActionFingerprint::derive(label.as_bytes())
69    }
70
71    #[test]
72    fn empty_and_varied_histories_are_ok() {
73        assert_eq!(observe(&[]), StallVerdict::Ok);
74        assert_eq!(
75            observe(&[print("a"), print("b"), print("c")]),
76            StallVerdict::Ok
77        );
78    }
79
80    #[test]
81    fn three_identical_tail_actions_stall() {
82        let sample = [print("x"), print("loop"), print("loop"), print("loop")];
83        assert_eq!(observe(&sample), StallVerdict::Stall { repeats: 3 });
84    }
85
86    #[test]
87    fn a_broken_repetition_recovered_itself() {
88        // Two repeats, then something new, then two repeats again: the
89        // tail run is 2, under the threshold of 3.
90        let sample = [
91            print("loop"),
92            print("loop"),
93            print("fresh"),
94            print("loop"),
95            print("loop"),
96        ];
97        assert_eq!(observe(&sample), StallVerdict::Ok);
98    }
99
100    proptest! {
101        /// The verdict only ever reads the tail run length.
102        #[test]
103        fn stall_iff_tail_run_reaches_threshold(labels in proptest::collection::vec("[ab]", 1..12)) {
104            let prints: Vec<ActionFingerprint> =
105                labels.iter().map(|l| print(l)).collect();
106            let last = prints.last().unwrap();
107            let tail = prints.iter().rev().take_while(|p| *p == last).count();
108            let verdict = observe(&prints);
109            if tail >= usize::try_from(LOOP_REPEAT_THRESHOLD).unwrap() {
110                prop_assert_eq!(verdict, StallVerdict::Stall { repeats: u32::try_from(tail).unwrap() });
111            } else {
112                prop_assert_eq!(verdict, StallVerdict::Ok);
113            }
114        }
115    }
116}