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