Skip to main content

mlua_swarm/core/
config.rs

1//! EngineCfg + LongHoldConfig.
2
3use std::time::Duration;
4
5fn random_token_secret() -> Vec<u8> {
6    let mut buf = vec![0u8; 32];
7    getrandom::fill(&mut buf).expect("OS RNG unavailable");
8    buf
9}
10
11/// Lock acquisition + max-hold guard configuration.
12#[derive(Debug, Clone)]
13pub struct EngineCfg {
14    /// When `true`, `Engine::with_state` never retries on a busy lock — it
15    /// fails fast with `EngineError::LockBusy` on the very first
16    /// `try_lock` miss instead of backing off and retrying.
17    pub try_only: bool,
18    /// Max number of `try_lock` retries before giving up with
19    /// `EngineError::LockBusyAfterRetry` (ignored when `try_only = true`).
20    pub max_retry: u32,
21    /// Linear backoff step (ms) between retries; the sleep duration for
22    /// attempt `n` is `backoff_ms_step * (n + 1)`.
23    pub backoff_ms_step: u64,
24    /// R4 guard threshold: if a single `with_state` closure holds the lock
25    /// longer than this (ms), `with_state` reports a long operation having
26    /// leaked inside the lock in violation of the R3 discipline — it emits
27    /// a `tracing::warn!` with `op` / `elapsed_ms` / `max_hold_ms` and
28    /// continues, unless [`Self::max_hold_panic`] escalates the overrun to
29    /// a panic.
30    pub max_hold_ms: u128,
31    /// When `true`, an R4 overrun panics (after the warn) instead of
32    /// continuing. Off by default in every build: the guard measures
33    /// wall-clock hold time, which on a loaded shared runner includes
34    /// scheduler preemption, so a threshold panic is structurally flaky in
35    /// CI — a trivial closure was observed held for 186ms > 50ms on a
36    /// GitHub macOS runner, and the panic killed the server under test.
37    /// Enable locally when hunting a suspected R3 violation (a long op /
38    /// blocking call leaked inside the lock).
39    pub max_hold_panic: bool,
40    /// HMAC secret used by `TokenSigner` to sign/verify `CapToken`s.
41    ///
42    /// `Default` generates this fresh (32 random bytes from the OS RNG) on
43    /// every call when the caller does not supply one — set it explicitly
44    /// for tokens to stay valid across restarts, or when multiple
45    /// independently-constructed engines/signers must accept each other's
46    /// tokens.
47    pub token_secret: Vec<u8>,
48    /// Long-hold session tuning (idle keepalive, heartbeat cadence).
49    pub long_hold: LongHoldConfig,
50    /// Worker recursive spawn depth ceiling (guards against unbounded spawn).
51    ///
52    /// When `Ctx.meta.runtime["spawn_depth"]` has already reached this value
53    /// and a Worker token tries to call `start_task`, the engine raises
54    /// `EngineError::SpawnDepthExceeded`. `0` = root (a task launched
55    /// directly by an Operator); `4` = default, which allows four levels of
56    /// nested sub-tasks.
57    pub max_spawn_depth: u32,
58    /// GH #31: threshold/mode/storage tuning for delivering an oversized
59    /// baked `system_prompt` by reference (`WorkerPayload.system_ref`)
60    /// instead of inline (`WorkerPayload.system`). See
61    /// [`SystemRefConfig`].
62    pub system_ref: SystemRefConfig,
63    /// Policy that governs how submit-time projection sinks react when a
64    /// fail-open condition is encountered (missing `work_dir`/
65    /// `project_root`, `OutputStore` write error, adapter materialize
66    /// error, state lookup error). Default [`CheckPolicy::Warn`]
67    /// preserves the pre-existing warn-and-continue behaviour of every
68    /// call site — see [`CheckPolicy`] for the semantics of the other
69    /// two modes and [`apply_check_policy`](crate::core::engine::apply_check_policy)
70    /// for the shared decision helper. Per-run override is threaded via
71    /// `POST /v1/tasks` (subtask-1c).
72    pub check_policy: CheckPolicy,
73}
74
75// `CheckPolicy` is the Swarm IF SoT type; it lives in the
76// `mlua-swarm-schema` crate (enum relocation) and is
77// re-exported here so every existing path
78// (`crate::core::config::CheckPolicy`, `EngineCfg.check_policy`,
79// `TaskSpec.check_policy`, `apply_check_policy`) keeps its type path
80// unchanged. There is exactly ONE definition — no duplicate enum / `From`
81// bridge — and the wire form (`"silent"` / `"warn"` / `"strict"`) is
82// byte-identical to the pre-relocation form. See the schema crate for the
83// full mode semantics.
84pub use mlua_swarm_schema::CheckPolicy;
85
86impl EngineCfg {
87    /// Strict variant: `try_only = true` (no retry/backoff) and a tight
88    /// `max_hold_ms = 10`. Useful for tests that want lock contention to
89    /// fail fast rather than silently wait.
90    pub fn strict() -> Self {
91        Self {
92            try_only: true,
93            max_retry: 0,
94            backoff_ms_step: 0,
95            max_hold_ms: 10,
96            ..Self::default()
97        }
98    }
99
100    /// Relaxed variant: higher `max_retry` / `backoff_ms_step` /
101    /// `max_hold_ms` than the default, for environments where lock
102    /// contention is expected to be more frequent or operations slower.
103    pub fn relaxed() -> Self {
104        Self {
105            try_only: false,
106            max_retry: 10,
107            backoff_ms_step: 50,
108            max_hold_ms: 200,
109            ..Self::default()
110        }
111    }
112}
113
114impl Default for EngineCfg {
115    /// Baseline configuration: bounded retry with backoff, a generous
116    /// (but non-zero) `max_hold_ms`, and `max_spawn_depth = 4`.
117    /// `token_secret` is generated fresh per call — see the field doc.
118    fn default() -> Self {
119        Self {
120            try_only: false,
121            max_retry: 3,
122            backoff_ms_step: 10,
123            max_hold_ms: 50,
124            max_hold_panic: false,
125            token_secret: random_token_secret(),
126            long_hold: LongHoldConfig::default(),
127            max_spawn_depth: 4,
128            system_ref: SystemRefConfig::default(),
129            check_policy: CheckPolicy::default(),
130        }
131    }
132}
133
134/// GH #31: server-side config for how a baked `system_prompt` too large
135/// to inline is delivered instead — see `Engine::fetch_worker_payload`'s
136/// threshold branch (`crate::core::engine`) for where this is consumed.
137/// Single server-side setting, not per-request: every fetch of a given
138/// `(task_id, attempt)` sees the same `mode` and `threshold_bytes`.
139#[derive(Debug, Clone)]
140pub struct SystemRefConfig {
141    /// Byte length of the baked `system` string above which
142    /// `Engine::fetch_worker_payload{,_trusted}` switches from inlining
143    /// the value (`WorkerPayload.system`) to a reference
144    /// (`WorkerPayload.system_ref`). Default (`25 * 1024`, 25 KiB)
145    /// matches `bp_doctor`'s existing WARN threshold
146    /// (`AGENT_MD_DEFAULT_WARN_BYTES` in
147    /// `crates/mlua-swarm-cli/src/mcp.rs`) — the same SubAgent
148    /// context-window headroom rationale that threshold documents
149    /// applies here to inline-vs-reference delivery.
150    pub threshold_bytes: usize,
151    /// Which [`crate::types::SystemRefMode`] an over-threshold response
152    /// uses to deliver its content.
153    pub mode: crate::types::SystemRefMode,
154    /// Directory `SystemRefMode::File` writes rendered `system` bodies
155    /// into (`<store_dir>/<task_id>-<attempt>.md`). No eviction policy —
156    /// files accumulate for the process lifetime (explicitly out of
157    /// scope; see the risk note on `Engine::fetch_worker_payload`).
158    pub store_dir: std::path::PathBuf,
159}
160
161impl Default for SystemRefConfig {
162    /// `threshold_bytes = 25 * 1024`, `mode = SystemRefMode::File`,
163    /// `store_dir = std::env::temp_dir().join("mse-system-ref")`.
164    fn default() -> Self {
165        Self {
166            threshold_bytes: 25 * 1024,
167            mode: crate::types::SystemRefMode::File,
168            store_dir: std::env::temp_dir().join("mse-system-ref"),
169        }
170    }
171}
172
173/// Tuning for long-running (suspend/resume-capable) sessions and tasks —
174/// how long a poll/suspend may hold, how often heartbeats are expected,
175/// and whether idle tasks are kept alive across a detach.
176#[derive(Debug, Clone)]
177pub struct LongHoldConfig {
178    /// Default wait duration used by long-poll style waits when the
179    /// caller does not specify one explicitly.
180    pub default_hold: Duration,
181    /// Upper bound on how long a single suspend/poll wait may block.
182    pub max_hold: Duration,
183    /// Expected cadence of `Engine::heartbeat` calls from an attached
184    /// session; consumed by `Engine::start_detach_loop`.
185    pub heartbeat_interval: Duration,
186    /// Number of missed heartbeat intervals tolerated before the detach
187    /// loop flips a session's `attached` flag to `false`.
188    pub heartbeat_miss_threshold: u32,
189    /// When `true`, a task survives (its state is retained) across a
190    /// session detach, so a later reattach can resume it in place.
191    pub keepalive_on_idle: bool,
192}
193
194impl Default for LongHoldConfig {
195    fn default() -> Self {
196        Self {
197            default_hold: Duration::from_secs(3600),
198            max_hold: Duration::from_secs(48 * 3600),
199            heartbeat_interval: Duration::from_secs(300),
200            heartbeat_miss_threshold: 3,
201            keepalive_on_idle: true,
202        }
203    }
204}