use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
pub const RELENTLESS_TOOL_ROUND_TARGET: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Tenacity {
#[default]
Normal,
Relentless,
}
impl Tenacity {
#[must_use]
pub fn project_tool_round_limit(self, configured: usize) -> usize {
if self == Self::Relentless {
configured.max(RELENTLESS_TOOL_ROUND_TARGET)
} else {
configured
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Relentless => "relentless",
}
}
#[must_use]
pub fn describe(self) -> String {
match self {
Self::Normal => {
"stop at the first plausible finish; fresh progress may renew the initial allowance".to_string()
}
Self::Relentless => format!(
"lift the initial tool-round allowance to at least {RELENTLESS_TOOL_ROUND_TARGET} \
(an explicit initial allowance still wins; configured progress renewal remains active)"
),
}
}
#[must_use]
pub fn all() -> [Self; 2] {
[Self::Normal, Self::Relentless]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RecordedTenacity {
Current(Tenacity),
Legacy(LegacyTenacity),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LegacyTenacity {
Relaxed,
Standard,
Insistent,
}
impl RecordedTenacity {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Current(t) => t.label(),
Self::Legacy(LegacyTenacity::Relaxed) => "relaxed",
Self::Legacy(LegacyTenacity::Standard) => "standard",
Self::Legacy(LegacyTenacity::Insistent) => "insistent",
}
}
}
impl From<Tenacity> for RecordedTenacity {
fn from(t: Tenacity) -> Self {
Self::Current(t)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolRoundLimitSource {
Config,
Tenacity,
Override,
}
impl ToolRoundLimitSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Config => "config",
Self::Tenacity => "tenacity",
Self::Override => "override",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolRoundLimit {
pub rounds: usize,
pub source: ToolRoundLimitSource,
pub configured: usize,
pub tenacity: Option<RecordedTenacity>,
}
impl ToolRoundLimit {
#[must_use]
pub fn is_escalated(&self) -> bool {
self.rounds != self.configured
}
}
#[must_use]
pub fn resolve_tool_round_limit(
configured: usize,
explicit_tenacity: Option<Tenacity>,
explicit_rounds: Option<usize>,
) -> ToolRoundLimit {
if let Some(rounds) = explicit_rounds {
return ToolRoundLimit {
rounds,
source: ToolRoundLimitSource::Override,
configured,
tenacity: explicit_tenacity.map(RecordedTenacity::from),
};
}
if let Some(level) = explicit_tenacity {
let projected = level.project_tool_round_limit(configured);
if projected != configured {
return ToolRoundLimit {
rounds: projected,
source: ToolRoundLimitSource::Tenacity,
configured,
tenacity: Some(level.into()),
};
}
}
ToolRoundLimit {
rounds: configured,
source: ToolRoundLimitSource::Config,
configured,
tenacity: explicit_tenacity.map(RecordedTenacity::from),
}
}
impl fmt::Display for Tenacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
impl FromStr for Tenacity {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().to_ascii_lowercase();
if s == "default" {
return Ok(Self::default());
}
if let Some(level) = Self::all().into_iter().find(|l| l.label() == s) {
return Ok(level);
}
if let Some(new) = crate::psyche_import::split_tenacity(&s) {
return Err(format!(
"tenacity '{s}' was split: its read-before-acting half is now \
`--initiative {new}`; tenacity is normal|relentless"
));
}
Err(format!("unknown tenacity '{s}' (normal|relentless)"))
}
}
static CLI_TENACITY: std::sync::Mutex<Option<Tenacity>> = std::sync::Mutex::new(None);
static SESSION_TOOL_ROUNDS: std::sync::Mutex<Option<usize>> = std::sync::Mutex::new(None);
static CONFIGURED_TOOL_ROUNDS: std::sync::Mutex<Option<usize>> = std::sync::Mutex::new(None);
std::thread_local! {
static EFFECTIVE_TENACITY_OVERRIDE: std::cell::Cell<Option<Tenacity>> =
const { std::cell::Cell::new(None) };
}
#[must_use]
pub struct ScopedEffectiveTenacity {
previous: Option<Tenacity>,
_thread_bound: std::marker::PhantomData<std::rc::Rc<()>>,
}
impl Drop for ScopedEffectiveTenacity {
fn drop(&mut self) {
let _ = EFFECTIVE_TENACITY_OVERRIDE.try_with(|slot| slot.set(self.previous));
}
}
pub fn scoped_effective_tenacity(level: Tenacity) -> ScopedEffectiveTenacity {
let previous = EFFECTIVE_TENACITY_OVERRIDE.with(|slot| slot.replace(Some(level)));
ScopedEffectiveTenacity {
previous,
_thread_bound: std::marker::PhantomData,
}
}
pub fn set_cli_tenacity(level: Tenacity) {
if let Ok(mut slot) = CLI_TENACITY.lock() {
*slot = Some(level);
}
}
pub fn clear_cli_tenacity() {
if let Ok(mut slot) = CLI_TENACITY.lock() {
*slot = None;
}
}
#[must_use]
pub fn cli_tenacity() -> Option<Tenacity> {
CLI_TENACITY.lock().ok().and_then(|s| *s)
}
#[must_use]
pub fn effective_tenacity() -> Tenacity {
if let Some(level) = EFFECTIVE_TENACITY_OVERRIDE.with(std::cell::Cell::get) {
return level;
}
cli_tenacity().unwrap_or_default()
}
#[doc(hidden)]
pub struct TenacityRuntimeSnapshot {
cli: Option<Tenacity>,
session_rounds: Option<usize>,
configured_rounds: Option<usize>,
}
pub fn set_session_tool_rounds(rounds: Option<usize>) {
if let Ok(mut slot) = SESSION_TOOL_ROUNDS.lock() {
*slot = rounds;
}
}
#[must_use]
pub fn session_tool_rounds() -> Option<usize> {
SESSION_TOOL_ROUNDS.lock().ok().and_then(|s| *s)
}
pub fn set_configured_tool_rounds(rounds: Option<usize>) {
if let Ok(mut slot) = CONFIGURED_TOOL_ROUNDS.lock() {
*slot = rounds;
}
}
#[must_use]
pub fn configured_tool_rounds() -> Option<usize> {
CONFIGURED_TOOL_ROUNDS.lock().ok().and_then(|s| *s)
}
#[must_use]
pub fn session_tool_round_limit() -> Option<ToolRoundLimit> {
Some(resolve_tool_round_limit(
configured_tool_rounds()?,
cli_tenacity(),
session_tool_rounds(),
))
}
#[doc(hidden)]
#[must_use]
pub fn snapshot_runtime_state() -> TenacityRuntimeSnapshot {
TenacityRuntimeSnapshot {
cli: cli_tenacity(),
session_rounds: session_tool_rounds(),
configured_rounds: configured_tool_rounds(),
}
}
#[doc(hidden)]
pub fn restore_runtime_state(snapshot: TenacityRuntimeSnapshot) {
if let Ok(mut s) = CLI_TENACITY.lock() {
*s = snapshot.cli;
}
if let Ok(mut s) = SESSION_TOOL_ROUNDS.lock() {
*s = snapshot.session_rounds;
}
if let Ok(mut s) = CONFIGURED_TOOL_ROUNDS.lock() {
*s = snapshot.configured_rounds;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_session_override_and_its_baseline_round_trip() {
let _g = crate::test_guard::GlobalSettingsGuard::acquire();
set_session_tool_rounds(None);
assert_eq!(session_tool_rounds(), None, "no override by default");
set_session_tool_rounds(Some(320));
assert_eq!(session_tool_rounds(), Some(320));
set_session_tool_rounds(None);
assert_eq!(session_tool_rounds(), None, "the override releases");
set_configured_tool_rounds(Some(40));
assert_eq!(configured_tool_rounds(), Some(40));
}
#[test]
fn the_derivation_is_computable_from_the_globals() {
let _g = crate::test_guard::GlobalSettingsGuard::acquire();
set_configured_tool_rounds(Some(40));
set_cli_tenacity(Tenacity::Relentless);
set_session_tool_rounds(Some(320));
let limit = session_tool_round_limit().expect("a baseline is installed");
assert_eq!(limit.rounds, 320);
assert_eq!(limit.source, ToolRoundLimitSource::Override);
assert_eq!(limit.configured, 40);
assert_eq!(limit.tenacity, Some(Tenacity::Relentless.into()));
assert!(limit.is_escalated(), "320 over a configured 40");
set_session_tool_rounds(None);
let limit = session_tool_round_limit().expect("a baseline is installed");
assert_eq!(limit.source, ToolRoundLimitSource::Tenacity);
assert_eq!(limit.configured, 40);
}
#[test]
fn no_baseline_means_no_derivation_not_a_default() {
let _g = crate::test_guard::GlobalSettingsGuard::acquire();
set_configured_tool_rounds(None);
assert_eq!(configured_tool_rounds(), None);
assert!(
session_tool_round_limit().is_none(),
"a derivation without a baseline would be a confident invention"
);
}
#[test]
fn normal_is_the_default_and_leaves_the_cap_alone() {
assert_eq!(Tenacity::default(), Tenacity::Normal);
assert_eq!(Tenacity::Normal.project_tool_round_limit(40), 40);
}
#[test]
fn scoped_override_is_nested_thread_local_and_restores_the_global_resolution() {
use crate::test_guard::GlobalSettingsGuard;
let _settings = GlobalSettingsGuard::acquire();
clear_cli_tenacity();
assert_eq!(effective_tenacity(), Tenacity::Normal);
let outer = scoped_effective_tenacity(Tenacity::Relentless);
assert_eq!(effective_tenacity(), Tenacity::Relentless);
{
let _inner = scoped_effective_tenacity(Tenacity::Normal);
assert_eq!(effective_tenacity(), Tenacity::Normal);
}
assert_eq!(effective_tenacity(), Tenacity::Relentless);
let other_thread = std::thread::spawn(effective_tenacity)
.join()
.expect("tenacity probe thread");
assert_eq!(
other_thread,
Tenacity::Normal,
"the override must stay local to the driven turn thread"
);
drop(outer);
set_cli_tenacity(Tenacity::Relentless);
assert_eq!(effective_tenacity(), Tenacity::Relentless);
}
#[test]
fn relentless_can_project_an_operator_selected_round_target() {
assert_eq!(Tenacity::Normal.project_tool_round_limit(40), 40);
assert_eq!(
Tenacity::Relentless.project_tool_round_limit(40),
RELENTLESS_TOOL_ROUND_TARGET
);
assert_eq!(
Tenacity::Relentless.project_tool_round_limit(20_000),
20_000,
"the posture must not lower an already larger configured budget"
);
let overridden = resolve_tool_round_limit(40, Some(Tenacity::Relentless), Some(7));
assert_eq!(
(overridden.rounds, overridden.source),
(7, ToolRoundLimitSource::Override),
"a direct round limit remains the outermost operator choice"
);
}
#[test]
fn parse_is_case_insensitive_and_round_trips_the_label() {
for t in Tenacity::all() {
assert_eq!(t.label().parse::<Tenacity>().unwrap(), t);
assert_eq!(t.to_string(), t.label());
}
assert_eq!(
" RELENTLESS ".parse::<Tenacity>().unwrap(),
Tenacity::Relentless
);
assert_eq!("default".parse::<Tenacity>().unwrap(), Tenacity::Normal);
assert!("banana".parse::<Tenacity>().is_err());
}
#[test]
fn an_old_level_is_refused_with_its_initiative_replacement() {
for (old, new) in [
("relaxed", "patient"),
("standard", "measured"),
("insistent", "decisive"),
] {
let err = old.parse::<Tenacity>().unwrap_err();
assert!(err.contains(&format!("--initiative {new}")), "{err}");
}
}
#[test]
fn serde_uses_the_lowercase_label() {
let json = serde_json::to_string(&Tenacity::Relentless).unwrap();
assert_eq!(json, "\"relentless\"");
let back: Tenacity = serde_json::from_str("\"normal\"").unwrap();
assert_eq!(back, Tenacity::Normal);
assert!(serde_json::from_str::<Tenacity>("\"standard\"").is_err());
}
#[test]
fn a_recorded_level_round_trips_old_and_new_labels_verbatim() {
for label in ["relaxed", "standard", "insistent", "normal", "relentless"] {
let json = format!("\"{label}\"");
let r: RecordedTenacity = serde_json::from_str(&json).unwrap();
assert_eq!(serde_json::to_string(&r).unwrap(), json);
assert_eq!(r.label(), label);
}
assert_eq!(
serde_json::from_str::<RecordedTenacity>("\"relentless\"").unwrap(),
RecordedTenacity::Current(Tenacity::Relentless)
);
}
#[test]
fn snapshot_restore_round_trips_every_tenacity_global() {
use crate::test_guard::GlobalSettingsGuard;
let _g = GlobalSettingsGuard::acquire();
clear_cli_tenacity();
set_session_tool_rounds(None);
set_configured_tool_rounds(None);
let snap = snapshot_runtime_state();
set_cli_tenacity(Tenacity::Relentless);
set_session_tool_rounds(Some(320));
set_configured_tool_rounds(Some(40));
assert_eq!(cli_tenacity(), Some(Tenacity::Relentless));
restore_runtime_state(snap);
assert_eq!(cli_tenacity(), None, "CLI tenacity restored");
assert_eq!(
session_tool_rounds(),
None,
"the /rounds override restored — a leaked 320 would escalate the next test"
);
assert_eq!(configured_tool_rounds(), None);
}
#[test]
fn guarded_state_is_restored_even_when_a_test_panics() {
use crate::test_guard::GlobalSettingsGuard;
let result = std::panic::catch_unwind(|| {
let _g = GlobalSettingsGuard::acquire();
clear_cli_tenacity();
set_session_tool_rounds(Some(4242));
panic!("intentional panic inside a guarded test");
});
assert!(result.is_err(), "the guarded closure panicked as intended");
let _g = GlobalSettingsGuard::acquire();
assert_ne!(
session_tool_rounds(),
Some(4242),
"GlobalSettingsGuard::drop restored the round override during unwind"
);
}
}