rlmesh_proto/lib.rs
1//! Generated RLMesh protobuf bindings and protocol-level constants.
2#![deny(rustdoc::broken_intra_doc_links)]
3
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7/// Identity of the frozen wire substrate: the `core` handshake plus the `spaces`
8/// value transport the runtime relays between env and model. NOT the package
9/// version, NOT the product semver, and NOT a per-service version — it names the
10/// one shared byte contract every component is built against, decoupled from all
11/// of them.
12///
13/// Compatibility is plain equality: a peer is compatible iff its token equals
14/// this. There is no support window and no range negotiation. The wire grows
15/// additively forever under this single token; workflow *behavior* rides on
16/// editions and optional *features* on capabilities, so neither forces a bump.
17///
18/// This is a failsafe, not a version counter: in the normal course it is never
19/// bumped. Bumping it to `rlmesh-wire-v2` is a deliberate, public hard pivot — a
20/// build that intentionally will not interoperate with `rlmesh-wire-v1` — reserved
21/// for a wire break that additive growth, editions, and capabilities genuinely
22/// cannot absorb.
23pub const PROTOCOL_GENERATION: &str = "rlmesh-wire-v1";
24
25/// Current workflow semantics edition.
26///
27/// Stable releases use a bare sealed `YYYY.MM` label. Prerelease and local
28/// source builds use a suffixed cohort (`YYYY.MM-<semver-prerelease>` or
29/// `YYYY.MM-dev.<git-token>`) so moving builds only interoperate with the same
30/// cohort unless both sides explicitly advertise a sealed fallback edition.
31pub const CURRENT_WORKFLOW_EDITION: &str = env!("RLMESH_CURRENT_WORKFLOW_EDITION");
32
33/// Bare `YYYY.MM` workflow edition base for this build.
34pub const WORKFLOW_EDITION_BASE: &str = env!("RLMESH_WORKFLOW_EDITION_BASE");
35
36/// Build cohort used to spell [`CURRENT_WORKFLOW_EDITION`].
37pub const BUILD_COHORT: &str = env!("RLMESH_BUILD_COHORT");
38
39/// Source of the build cohort: `release`, `package`, or `git`.
40pub const BUILD_SOURCE: &str = env!("RLMESH_BUILD_SOURCE");
41
42/// Workflow editions this crate can operate under, generated by `build.rs` from
43/// `rlmesh.toml`'s `[workflow] supported_editions` with
44/// [`CURRENT_WORKFLOW_EDITION`] first. Each edition names an immutable
45/// behavioral contract documented in `docs/editions/<base>.md` (the spec file is
46/// keyed by the bare `YYYY.MM` base, never the suffixed cohort), so a sealed
47/// edition is retained here under its bare name for as long as the crate can
48/// still drive it. A published crate ships no `rlmesh.toml`; it reads the same
49/// list from its shipped `supported_editions.txt`.
50pub const SUPPORTED_WORKFLOW_EDITIONS: &[&str] = &SUPPORTED_WORKFLOW_EDITION_ARRAY;
51
52const SUPPORTED_WORKFLOW_EDITION_LIST: &str = env!("RLMESH_SUPPORTED_WORKFLOW_EDITIONS");
53
54const SUPPORTED_WORKFLOW_EDITION_ARRAY: [&str; edition_count(SUPPORTED_WORKFLOW_EDITION_LIST)] =
55 split_editions(SUPPORTED_WORKFLOW_EDITION_LIST);
56
57/// Number of comma-separated editions in the build-time list (never zero: the
58/// list always carries at least [`CURRENT_WORKFLOW_EDITION`]).
59const fn edition_count(list: &str) -> usize {
60 let bytes = list.as_bytes();
61 let mut count = 1;
62 let mut index = 0;
63 while index < bytes.len() {
64 if bytes[index] == b',' {
65 count += 1;
66 }
67 index += 1;
68 }
69 count
70}
71
72/// Split the build-time list on commas. Edition names are `YYYY.MM` bases with
73/// SemVer/git cohort suffixes, so no name can contain the separator.
74const fn split_editions<const N: usize>(list: &'static str) -> [&'static str; N] {
75 let bytes = list.as_bytes();
76 let mut editions = [""; N];
77 let mut start = 0;
78 let mut index = 0;
79 let mut slot = 0;
80 while index < bytes.len() {
81 if bytes[index] == b',' {
82 editions[slot] = edition_at(bytes, start, index);
83 slot += 1;
84 start = index + 1;
85 }
86 index += 1;
87 }
88 editions[slot] = edition_at(bytes, start, bytes.len());
89 editions
90}
91
92const fn edition_at(bytes: &'static [u8], start: usize, end: usize) -> &'static str {
93 let (_, tail) = bytes.split_at(start);
94 let (edition, _) = tail.split_at(end - start);
95 match std::str::from_utf8(edition) {
96 Ok(edition) => edition,
97 Err(_) => panic!("RLMESH_SUPPORTED_WORKFLOW_EDITIONS is not UTF-8"),
98 }
99}
100
101/// A workflow semantics edition this build implements.
102///
103/// An edition is a sticky behavioral declaration (the NixOS `stateVersion`
104/// model), so this enum is deliberately exhaustive and its arms are append-only:
105/// a future default change adds an arm here plus a row in [`defaults`] and
106/// leaves every sealed arm — and every code path reading it — untouched. A name
107/// no arm implements is refused at the boundary by [`Edition::parse`], never
108/// folded into a neighbouring arm.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum Edition {
111 /// `2026.06`, sealed at v0.1.0. Contract: `docs/editions/2026.06.md`.
112 E2026_06,
113}
114
115impl Edition {
116 /// Every arm, in declaration order; append-only, like the enum itself.
117 ///
118 /// Rust cannot enumerate variants, so this list is written by hand and
119 /// guarded by `every_arm_is_listed_and_has_a_row`: its exhaustive match means
120 /// a new arm does not compile until it is named there, and the test fails
121 /// unless every name this build offers ([`SUPPORTED_WORKFLOW_EDITIONS`]) and
122 /// every arm's own base resolve through this list.
123 const ALL: &'static [Edition] = &[Edition::E2026_06];
124
125 /// The bare `YYYY.MM` base this edition's contract document is keyed by.
126 pub const fn base(&self) -> &'static str {
127 match self {
128 Edition::E2026_06 => "2026.06",
129 }
130 }
131
132 /// The arm this build's own [`CURRENT_WORKFLOW_EDITION`] spells: what a
133 /// participant runs at when nothing pins it elsewhere.
134 pub fn current() -> Edition {
135 Edition::parse(CURRENT_WORKFLOW_EDITION)
136 .expect("CURRENT_WORKFLOW_EDITION names an arm (every_arm_is_listed_and_has_a_row)")
137 }
138
139 /// Resolve a wire edition name to the arm it names.
140 ///
141 /// The name is split at its first `-` exactly as [`edition_sort_key`] splits
142 /// it: the `YYYY.MM` base names the contract, and a cohort suffix only
143 /// identifies a moving build of that SAME base (`YYYY.MM-<semver-prerelease>`
144 /// or `YYYY.MM-dev.<git-token>`). So every cohort spelling of a known base
145 /// resolves to that base's arm — this build's own
146 /// ([`CURRENT_WORKFLOW_EDITION`], e.g. `2026.06-0.1.0-rc.12`) and equally a
147 /// retained older base's cohort, which this build never spells itself.
148 /// Whitespace is trimmed; a base no arm implements is an [`UnknownEdition`].
149 ///
150 /// This is the ONLY place an edition name is compared as a string. Every
151 /// guard and default downstream branches on the typed value.
152 pub fn parse(edition: &str) -> Result<Edition, UnknownEdition> {
153 let name = edition.trim();
154 let (base, _, _) = edition_sort_key(name);
155 Edition::ALL
156 .iter()
157 .copied()
158 .find(|edition| edition.base() == base)
159 .ok_or_else(|| UnknownEdition(name.to_string()))
160 }
161}
162
163impl std::fmt::Display for Edition {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.write_str(self.base())
166 }
167}
168
169/// A wire edition name no [`Edition`] arm implements.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct UnknownEdition(String);
172
173impl UnknownEdition {
174 /// The rejected name, trimmed, exactly as it arrived on the wire.
175 pub fn name(&self) -> &str {
176 &self.0
177 }
178}
179
180impl std::fmt::Display for UnknownEdition {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 write!(f, "unknown workflow edition {:?}", self.0)
183 }
184}
185
186impl std::error::Error for UnknownEdition {}
187
188/// The edition-governed defaults one [`Edition`] promises: the values a
189/// participant would otherwise hardcode, lifted here so a future default change
190/// is a new row rather than an edit to a code path a sealed edition already
191/// depends on. One row per arm, returned by [`defaults`]; **a sealed row is
192/// never edited.**
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct EditionDefaults {
195 /// Per-episode step bound applied when the session sets no explicit
196 /// `max_episode_steps` and the runtime owns resets, so a broken termination
197 /// condition surfaces as a truncation instead of hanging the run.
198 ///
199 /// Not named in the prose of `docs/editions/2026.06.md`, but observable in
200 /// the episode ledger the edition's "Episode Accounting" section governs
201 /// ("When a sub-environment reports terminated or truncated, its episode
202 /// completes"), and pinned by the `05-default-truncation-bound` fingerprint.
203 ///
204 /// Mirrored by `_MAX_STEPS_PER_EPISODE` in
205 /// `python/rlmesh/src/rlmesh/_models/_eval.py`, which bounds the Python
206 /// `Session` loop identically. The two loops must agree, so an edition that
207 /// changes this row changes that constant in the same commit.
208 pub default_max_episode_steps: i64,
209
210 /// The single reserved `ResetRequest.options` key this edition defines.
211 ///
212 /// `docs/editions/2026.06.md`, Reset: "This edition reserves exactly one,
213 /// `trial_index`: the 0-based ordinal of the episode being started, an
214 /// integer for a single lane or a per-lane list aligned to the lanes being
215 /// reset."
216 pub trial_index_option_key: &'static str,
217
218 /// The autoreset modes under which the RUNTIME owns lane restarts (it issues
219 /// the `Reset`s), as opposed to the env rolling its own lanes.
220 ///
221 /// `docs/editions/2026.06.md`, Episode Accounting: "`AUTORESET_MODE_DISABLED`:
222 /// only an explicit `Reset` restarts a lane." and "`AUTORESET_MODE_UNSPECIFIED`:
223 /// treated as `DISABLED`."
224 pub driver_owned_reset_modes: &'static [core::v1::AutoresetMode],
225
226 /// The `final_info` keys this edition reads an episode's task outcome from,
227 /// in priority order: the first key present decides, and a numeric value
228 /// coerces by truthiness (`1`/`1.0` → true).
229 ///
230 /// Which keys count is an edition-governed promise — an env that reports its
231 /// outcome under one of these names has it surface on the episode summary
232 /// and the `EpisodeCompleted` event; one that uses any other name does not.
233 /// Gymnasium's `is_success` leads; `success` and `task_success` follow as the
234 /// two spellings the ecosystem also ships.
235 ///
236 /// The Python `Session` loop reads the outcome from the same `info` with
237 /// `bool(info[key])` (`_success_from_info` in
238 /// `python/rlmesh/src/rlmesh/_models/_eval.py`); an edition that changes this
239 /// row revisits that sweep in the same commit.
240 pub success_info_keys: &'static [&'static str],
241
242 /// The reserved info-map key a served env reports value-conformance
243 /// warnings under.
244 ///
245 /// `docs/editions/2026.06.md`, Value Conformance: "A conformance warning is
246 /// reported in-band under the reserved `rlmesh.conformance.warning` key in
247 /// the info map returned by `reset` and `step`, at most once per (deviation
248 /// kind, value path) per session."
249 pub conformance_warning_info_key: &'static str,
250}
251
252/// Edition `2026.06`, sealed at v0.1.0. Never edited: a default change mints a
253/// new edition with its own row.
254static DEFAULTS_2026_06: EditionDefaults = EditionDefaults {
255 default_max_episode_steps: 100_000,
256 trial_index_option_key: "trial_index",
257 driver_owned_reset_modes: &[
258 core::v1::AutoresetMode::Disabled,
259 core::v1::AutoresetMode::Unspecified,
260 ],
261 success_info_keys: &["is_success", "success", "task_success"],
262 conformance_warning_info_key: "rlmesh.conformance.warning",
263};
264
265/// The defaults row governing a session running at `edition`.
266///
267/// Exhaustive by construction: a new [`Edition`] arm does not compile until it
268/// has a row here.
269pub const fn defaults(edition: Edition) -> &'static EditionDefaults {
270 match edition {
271 Edition::E2026_06 => &DEFAULTS_2026_06,
272 }
273}
274
275/// Whether this build retains `edition`: it has an arm for it AND
276/// [`SUPPORTED_WORKFLOW_EDITIONS`] (generated from `rlmesh.toml`) still names a
277/// spelling of it.
278///
279/// This is the authority for enforcing a runtime-pinned edition. Membership, not
280/// equality with [`CURRENT_WORKFLOW_EDITION`]: a retained older edition is a
281/// legitimate session floor, and rejecting it would refuse a route this build
282/// can actually drive.
283pub fn is_retained_edition(edition: Edition) -> bool {
284 SUPPORTED_WORKFLOW_EDITIONS
285 .iter()
286 .any(|retained| Edition::parse(retained) == Ok(edition))
287}
288
289/// Resolve a wire edition name to the arm this build both implements AND
290/// retains, or say why it cannot.
291///
292/// The boundary every string-carrying caller goes through — the env client's
293/// handshake, the runtime spec, a language binding — so a name this build was not
294/// built to drive is refused where it arrives, in one wording, rather than at a
295/// later guard that only ever sees the typed value. The message names the string
296/// exactly as it arrived (trimmed) and the retained list, so an operator can see
297/// both halves of the mismatch.
298pub fn parse_retained_edition(edition: &str) -> Result<Edition, String> {
299 Edition::parse(edition)
300 .ok()
301 .filter(|edition| is_retained_edition(*edition))
302 .ok_or_else(|| {
303 format!(
304 "runtime cannot drive workflow edition {:?}; this build implements {:?}",
305 edition.trim(),
306 SUPPORTED_WORKFLOW_EDITIONS
307 )
308 })
309}
310
311/// Resolve a **declared** edition — a participant's WANT — to the arm it names,
312/// or say why this build cannot declare it.
313///
314/// Stricter than [`parse_retained_edition`], which answers a different question:
315/// whether this build can be *pinned* to a value the runtime already selected out
316/// of an intersection of CAN sets. A declaration is a ceiling instead
317/// ([`want_admits`]), so it is only usable if it admits something this build
318/// offers. The bare `YYYY.MM` base of a retained edition always does — it admits
319/// every cohort spelling of that base, which is what makes it the value to write
320/// down. A cohort spelling that sorts below everything offered (an older
321/// prerelease of this build's own base, say) would refuse every session the
322/// build takes part in — including a purely local run — so it is refused here,
323/// where it was typed.
324pub fn parse_declared_edition(edition: &str) -> Result<Edition, String> {
325 let parsed = parse_retained_edition(edition)?;
326 let want = edition.trim();
327 if SUPPORTED_WORKFLOW_EDITIONS
328 .iter()
329 .any(|can| want_admits(want, can))
330 {
331 return Ok(parsed);
332 }
333 Err(format!(
334 "workflow edition {want:?} admits none of the editions this build offers \
335 ({SUPPORTED_WORKFLOW_EDITIONS:?}), so declaring it would refuse every session; \
336 declare one of those, or the bare {:?} base, instead",
337 parsed.base()
338 ))
339}
340
341/// Stable capability names exchanged during handshake.
342///
343/// Capabilities are advisory. A present key means the named optional feature is
344/// available; an absent key means it is not. They cover optional features that
345/// preserve interaction semantics. A feature that changes meaning belongs in an
346/// edition or generation. House rule: when an older peer would mishandle an
347/// absent field, the emitter checks a capability before sending it; if absence
348/// changes semantics, use an edition.
349pub mod capabilities {
350 /// A served model endpoint processes Join-stream requests concurrently
351 /// (pipelined predict): responses arrive in completion order rather than
352 /// strict arrival order, while per-route lifecycle ordering is preserved.
353 ///
354 /// Advisory only; this is not an edition change. The wire messages are
355 /// identical: every response still mirrors its `request_id`. A client uses
356 /// it to decide whether overlapping multiple predicts on one connection will
357 /// actually pipeline (capability present) or serialize behind the handler
358 /// (capability absent). See `docs/editions/2026.06.md`.
359 pub const MODEL_CONCURRENT_PREDICT_V1: &str = "rlmesh.model.concurrent_predict.v1";
360
361 /// A served model endpoint understands observation history: it answers
362 /// `ResolveAdapterResponse.history` when the runtime offers
363 /// `delivers_history`, ingests `PredictRequest.history` rows, and holds the
364 /// producer to consecutive `step`s. Advisory: a runtime that never offers
365 /// history sees no wire difference.
366 pub const MODEL_OBSERVATION_HISTORY_V1: &str = "rlmesh.model.observation_history.v1";
367
368 /// A served env endpoint steps and resets lanes individually: a `Reset` or
369 /// `Step` naming `env_indices` is honored (and answered partial-width)
370 /// instead of rejected with `UNSUPPORTED`, and lane-scoped requests on one
371 /// Join stream may be processed concurrently, answered in completion order.
372 ///
373 /// The wire spelling is the bare `subset_step` shipped peers send, so it
374 /// carries no `rlmesh.*` prefix. See `docs/editions/2026.06.md`.
375 pub const ENV_SUBSET_STEP: &str = "subset_step";
376}
377
378/// Whether the given protocol generation is the one this build speaks. Plain
379/// equality — there is no support window. Whitespace is trimmed so a padded wire
380/// value still matches.
381pub fn is_protocol_generation_supported(generation: &str) -> bool {
382 generation.trim() == PROTOCOL_GENERATION
383}
384
385/// Ordering key for a workflow edition name: `(base, cohort?, suffix)`.
386///
387/// The name is split at its **first** `-` into a `YYYY.MM` base and an optional
388/// cohort suffix:
389/// - `base` compares lexicographically — the zero-padded fixed-width `YYYY.MM`
390/// makes that chronological, so a newer date always outranks an older one.
391/// - `cohort?` is `true` for a suffixed prerelease/dev cohort and `false` for a
392/// bare sealed fallback, so an exact matching moving cohort wins over its
393/// sealed fallback when both peers support it.
394/// - `suffix` is the full cohort as a deterministic third tiebreak; two
395/// same-date cohorts are ordered by suffix rather than by iteration order.
396///
397/// Applies to workflow editions only. Protocol generations are compared by plain
398/// equality ([`is_protocol_generation_supported`]), never ordered — there is no
399/// generation window to pick a highest from.
400pub fn edition_sort_key(edition: &str) -> (&str, bool, &str) {
401 match edition.split_once('-') {
402 Some((base, suffix)) => (base, true, suffix),
403 None => (edition, false, ""),
404 }
405}
406
407/// Whether a declared WANT admits `edition` as the session's edition.
408///
409/// A declaration names the contract a participant was authored against, so a
410/// bare `YYYY.MM` WANT is a **base-level** ceiling: it admits every spelling of
411/// that base or an older one — on a prerelease or dev build, whose CAN set is
412/// only its cohort, `2026.06` selects that build's `2026.06-<cohort>`. A WANT
413/// that carries a cohort pins to that exact moving build and keeps the full
414/// [`edition_sort_key`] order, so a differing cohort of the same base is still
415/// excluded.
416pub fn want_admits(want: &str, edition: &str) -> bool {
417 match edition_sort_key(want) {
418 (base, false, _) => edition_sort_key(edition).0 <= base,
419 want => edition_sort_key(edition) <= want,
420 }
421}
422
423/// One participant's bind-time offer: the two sets it brings to negotiation.
424///
425/// Built for the env, the model, AND this runtime; [`negotiate_session_floor`]
426/// reconciles all three (the runtime is a participant because it re-frames
427/// env<->model traffic, so the session runs at an edition all three speak). When
428/// the model and runtime are the same build (in-process / `run_local`), the floor
429/// degenerates to `env ∩ self`. Protocol generation is NOT part of the offer — it
430/// is gated by plain equality at each pairwise handshake, so a session that reaches
431/// edition negotiation already shares one generation. Capabilities are advisory and
432/// pairwise (each peer reads the other's advertised map directly), not negotiated here.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct SessionOffer {
435 /// CAN: every workflow edition this participant can operate under — its
436 /// retained sealed list, carried on `supported_workflow_editions`.
437 pub editions: Vec<String>,
438 /// WANT: the one edition this participant declares, carried on
439 /// `preferred_workflow_edition`. `None` (an empty wire value) means
440 /// undeclared, and negotiation then reads it as `max(editions)` — which is
441 /// what every peer built before the field existed means.
442 pub preferred: Option<String>,
443}
444
445impl SessionOffer {
446 /// Build an undeclared offer (no WANT) from edition string slices.
447 /// Whitespace is trimmed.
448 pub fn new(editions: &[&str]) -> Self {
449 Self {
450 editions: editions.iter().map(|e| e.trim().to_string()).collect(),
451 preferred: None,
452 }
453 }
454
455 /// This build's own offer: the retained list it CAN drive, plus the edition
456 /// it declares — `declared`, or [`CURRENT_WORKFLOW_EDITION`] when the
457 /// participant pins nothing.
458 pub fn this_build(declared: Option<&str>) -> Self {
459 Self {
460 editions: supported_workflow_editions(),
461 preferred: Some(declared_workflow_edition(declared).to_string()),
462 }
463 }
464
465 /// The CAN set, trimmed, with empty entries dropped (they never match).
466 fn can(&self) -> impl Iterator<Item = &str> {
467 self.editions
468 .iter()
469 .map(|edition| edition.trim())
470 .filter(|edition| !edition.is_empty())
471 }
472
473 /// The declared WANT, trimmed; `None` when undeclared.
474 fn want(&self) -> Option<&str> {
475 self.preferred
476 .as_deref()
477 .map(str::trim)
478 .filter(|want| !want.is_empty())
479 }
480}
481
482/// The edition a participant declares (its WANT): the explicit declaration, or
483/// [`CURRENT_WORKFLOW_EDITION`] when it declares none. Whitespace is trimmed and
484/// an empty declaration reads as absent.
485pub fn declared_workflow_edition(declared: Option<&str>) -> &str {
486 declared
487 .map(str::trim)
488 .filter(|declared| !declared.is_empty())
489 .unwrap_or(CURRENT_WORKFLOW_EDITION)
490}
491
492/// One tier's position in a refused negotiation, for the diagnostic.
493#[derive(Debug, Clone, PartialEq, Eq)]
494pub struct TierOffer {
495 /// Which participant this is: `env`, `model`, or `runtime`.
496 pub tier: String,
497 /// The tier's CAN set as it arrived.
498 pub can: Vec<String>,
499 /// The tier's declared WANT, or `None` when it declared none.
500 pub want: Option<String>,
501}
502
503/// No edition satisfies every participant — the negotiation's clean refusal.
504///
505/// Carries each tier's WANT and CAN so an operator can see which side is the
506/// blocker without reading logs from three processes. Displays as one line per
507/// the [`Display`](std::fmt::Display) impl; callers wrap it in their own context.
508#[derive(Debug, Clone, PartialEq, Eq)]
509pub struct EditionRefusal {
510 /// Every participant, in negotiation order.
511 pub tiers: Vec<TierOffer>,
512}
513
514impl std::fmt::Display for EditionRefusal {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 for (index, tier) in self.tiers.iter().enumerate() {
517 if index > 0 {
518 f.write_str("; ")?;
519 }
520 match &tier.want {
521 Some(want) => write!(f, "{} wants {want:?} and can {:?}", tier.tier, tier.can)?,
522 None => write!(
523 f,
524 "{} declares no edition and can {:?}",
525 tier.tier, tier.can
526 )?,
527 }
528 }
529 Ok(())
530 }
531}
532
533impl std::error::Error for EditionRefusal {}
534
535/// Select the one edition a whole session runs at, from every participant's
536/// WANT and CAN — the single rule both [`negotiate_workflow_edition`] (two
537/// parties) and [`negotiate_session_floor`] (three) apply.
538///
539/// ```text
540/// E = max { e ∈ ⋂ can(P) : ∀P want(P) admits e } ordered by edition_sort_key
541/// ```
542///
543/// where a WANT admits an edition per [`want_admits`]: a bare base admits every
544/// spelling of that base or older, a cohort spelling admits what sorts at or
545/// below it. A participant that declares no WANT is read as wanting `max(can)`
546/// — which is what every peer built before the field existed means — and every
547/// member of the intersection is already ≤ its own max, so with nothing
548/// declared anywhere the ceiling is vacuous and this reduces exactly to the
549/// highest mutual edition.
550///
551/// A WANT is a **ceiling, not an exact demand**: a pin the intersection does not
552/// contain still selects the highest mutual edition it admits, and a pin above
553/// what another participant can drive never lifts the session above the floor,
554/// because the ceiling is applied *after* intersecting the CAN sets.
555///
556/// `Err` means no edition satisfies everyone; the caller must fail the session
557/// before any Join stream opens.
558fn select_workflow_edition(tiers: &[(&str, &SessionOffer)]) -> Result<String, EditionRefusal> {
559 let refuse = || EditionRefusal {
560 tiers: tiers
561 .iter()
562 .map(|(tier, offer)| TierOffer {
563 tier: (*tier).to_string(),
564 can: offer.editions.clone(),
565 want: offer.want().map(str::to_string),
566 })
567 .collect(),
568 };
569 let (_, first) = tiers.first().ok_or_else(&refuse)?;
570
571 first
572 .can()
573 .filter(|edition| {
574 tiers
575 .iter()
576 .all(|(_, offer)| offer.can().any(|other| other == *edition))
577 })
578 .filter(|edition| {
579 tiers
580 .iter()
581 .all(|(_, offer)| offer.want().is_none_or(|want| want_admits(want, edition)))
582 })
583 .max_by_key(|edition| edition_sort_key(edition))
584 .map(str::to_string)
585 .ok_or_else(&refuse)
586}
587
588/// Select the workflow edition governing a session between this runtime and one
589/// peer — the co-located floor (`env ∩ self`) the in-process / `run_local` path
590/// runs at, with the model and runtime being the same build.
591///
592/// Applies `select_workflow_edition`'s rule to the two offers. Editions the
593/// peer offers that this build does not retain are ignored, never accepted on
594/// the assumption they are compatible.
595///
596/// This is the **env-leg** negotiation only: `peer` is named `env` in the
597/// [`EditionRefusal`] diagnostic, which is correct for the one caller (the env
598/// client's handshake). A model-leg two-party negotiation would need its own
599/// tier label, and a three-party route uses [`negotiate_session_floor`].
600pub fn negotiate_workflow_edition(
601 peer: &SessionOffer,
602 runtime: &SessionOffer,
603) -> Result<String, EditionRefusal> {
604 select_workflow_edition(&[("env", peer), ("runtime", runtime)])
605}
606
607/// Why the runtime capped a session below what its peers would have run at.
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum RuntimeCap {
610 /// The runtime's CAN set, not its declaration, is what holds the session
611 /// down: undeclared it would still not reach the peers' edition. Upgrading
612 /// the runtime unlocks it.
613 Capability,
614 /// The runtime's declared WANT holds the session below what its own CAN set
615 /// would otherwise have reached. This is the sticky model working as
616 /// intended, not a defect.
617 Declaration,
618}
619
620/// The reconciled workflow edition produced by [`negotiate_session_floor`].
621#[derive(Debug, Clone, PartialEq, Eq)]
622pub struct SessionFloor {
623 /// The edition the session runs at: `select_workflow_edition` across env,
624 /// model, AND this runtime.
625 pub selected_workflow_edition: String,
626 /// The edition env + model alone would have used (always >= `selected`). When
627 /// it differs from `selected`, the runtime is the limiting tier — see
628 /// [`runtime_cap`](Self::runtime_cap).
629 pub desired_workflow_edition: String,
630 /// Why the runtime capped the session, or `None` when it did not.
631 pub runtime_cap: Option<RuntimeCap>,
632}
633
634impl SessionFloor {
635 /// Whether the runtime capped the session below what env + model would have
636 /// used on their own. The run is still safe at `selected_workflow_edition`
637 /// (all three speak it); see [`runtime_cap`](Self::runtime_cap) for whether
638 /// that is a limitation or a declaration.
639 pub fn runtime_limited(&self) -> bool {
640 self.runtime_cap.is_some()
641 }
642}
643
644/// Reconcile the route's workflow edition across env, model, and this runtime.
645///
646/// The runtime decode-rebuilds env<->model envelopes (prost drops fields it does
647/// not know), so a session runs at an edition all three support — never the
648/// env<->model pairwise max — or an edition-gated field would be silently stripped
649/// crossing an older runtime. The result also carries the env+model `desired`
650/// edition, and why the runtime capped it, so the caller can tell a runtime that
651/// *cannot* keep up from one that deliberately declares an older edition.
652/// Protocol generation is not reconciled here — it is gated by equality at each
653/// pairwise handshake, so all three already share it.
654///
655/// `Err` when the three share no edition; the caller must then fail the session
656/// before any Join stream opens.
657pub fn negotiate_session_floor(
658 env: &SessionOffer,
659 model: &SessionOffer,
660 runtime: &SessionOffer,
661) -> Result<SessionFloor, EditionRefusal> {
662 let selected_workflow_edition =
663 select_workflow_edition(&[("env", env), ("model", model), ("runtime", runtime)])?;
664 // env+model alone always admit at least `selected` (it clears both their CAN
665 // sets and both their ceilings), so this is Ok; fall back defensively.
666 let desired_workflow_edition = select_workflow_edition(&[("env", env), ("model", model)])
667 .unwrap_or_else(|_| selected_workflow_edition.clone());
668 let runtime_cap = (desired_workflow_edition != selected_workflow_edition).then(|| {
669 // Attribute the cap to the runtime's declaration only when dropping that
670 // declaration would actually lift the session: what its CAN set alone
671 // admits. Asking whether its CAN set contains `desired` would blame the
672 // CAN set for a runtime that is both older AND declared below its own max,
673 // telling the operator to upgrade when the declaration is the real pin.
674 let undeclared = SessionOffer {
675 editions: runtime.editions.clone(),
676 preferred: None,
677 };
678 // Relaxing one ceiling never refuses what `selected` already cleared.
679 let capable =
680 select_workflow_edition(&[("env", env), ("model", model), ("runtime", &undeclared)])
681 .unwrap_or_else(|_| selected_workflow_edition.clone());
682 if edition_sort_key(&selected_workflow_edition) < edition_sort_key(&capable) {
683 RuntimeCap::Declaration
684 } else {
685 RuntimeCap::Capability
686 }
687 });
688 Ok(SessionFloor {
689 selected_workflow_edition,
690 desired_workflow_edition,
691 runtime_cap,
692 })
693}
694
695/// Whether a client's handshake offer is compatible with this server: the only
696/// handshake-level decision is protocol generation (a hard, full-restart break).
697///
698/// The workflow **edition** is NOT decided here — only the runtime sees every
699/// participant, so it is the sole edition authority (via [`negotiate_session_floor`],
700/// which degenerates to `env ∩ self` when the model and runtime are the same build).
701/// A generation-compatible peer that shares no edition handshakes fine and then
702/// fails at the runtime's floor, with a clearer all-tiers diagnostic. Env and model
703/// servers use this same function, so the verdict cannot drift between services.
704pub fn evaluate_handshake(client_protocol_generation: &str) -> bool {
705 is_protocol_generation_supported(client_protocol_generation)
706}
707
708/// The core handshake request this build sends as a gRPC client: its protocol
709/// generation, the editions it CAN drive, the edition it declares (WANT), its
710/// advertised capabilities, and PeerInfo. Shared by the env and model clients
711/// (each wraps it in its service-specific request) so the request shape cannot
712/// drift between services.
713///
714/// `declared` is the caller's pinned edition; `None` declares
715/// [`CURRENT_WORKFLOW_EDITION`], which is `max(can)` for this build and therefore
716/// leaves negotiation exactly where it was before the field existed.
717pub fn core_handshake_request(
718 component: &str,
719 capabilities: &[&str],
720 declared: Option<&str>,
721) -> core::v1::HandshakeRequest {
722 core::v1::HandshakeRequest {
723 protocol_generation: PROTOCOL_GENERATION.to_string(),
724 peer_info: Some(peer_info(component)),
725 capabilities: capability_map(capabilities),
726 supported_workflow_editions: supported_workflow_editions(),
727 preferred_workflow_edition: declared_workflow_edition(declared).to_string(),
728 }
729}
730
731/// The human-facing rejection message when a client's protocol generation does not
732/// match this server. Generation is the ONLY handshake-level rejection; an edition
733/// mismatch is decided — and diagnosed — by the runtime's floor, not here. Shared by
734/// the env and model servers so the rejection prose cannot drift.
735pub fn generation_mismatch_message(client_protocol_generation: &str) -> String {
736 format!(
737 "protocol generation {client_protocol_generation} not compatible with server \
738 {PROTOCOL_GENERATION}"
739 )
740}
741
742/// Return supported workflow editions as owned strings for protobuf messages.
743pub fn supported_workflow_editions() -> Vec<String> {
744 SUPPORTED_WORKFLOW_EDITIONS
745 .iter()
746 .map(|edition| (*edition).to_string())
747 .collect()
748}
749
750/// Return a handshake capability map for the given capability names.
751///
752/// # Value grammar
753///
754/// A handshake capability map is keyed by capability name; the value is the
755/// literal string `"true"` and nothing else is defined by this protocol
756/// generation. Every RLMesh emitter goes through this function, so every value
757/// RLMesh puts on the wire is `"true"`, and [`has_capability`] is the matching
758/// reader: a key whose value is anything else — `"1"`, `"yes"`, `"TRUE"`, or
759/// the empty string — reads as NOT advertised, exactly like an absent key. A
760/// third-party peer that wants a capability honored must therefore send
761/// `"true"` verbatim.
762///
763/// Capabilities are advisory, so this strictness is safe in both directions:
764/// the worst case of an unrecognized value is the feature staying off.
765pub fn capability_map(names: &[&str]) -> HashMap<String, String> {
766 names
767 .iter()
768 .map(|name| ((*name).to_string(), "true".to_string()))
769 .collect()
770}
771
772/// Whether a peer's handshake capability map advertises the named capability.
773///
774/// Present means the key is mapped to the literal `"true"` (see
775/// [`capability_map`] for the value grammar). An absent key, an empty value, or
776/// any other value is read as absent — never as present — so an advisory
777/// feature a peer spelled differently stays off rather than being negotiated on
778/// a guess.
779pub fn has_capability(map: &HashMap<String, String>, name: &str) -> bool {
780 map.get(name).is_some_and(|value| value == "true")
781}
782
783pub mod core {
784 pub mod v1 {
785 tonic::include_proto!("rlmesh.core.v1");
786 }
787}
788
789/// Advisory runtime identity supplied by a non-Rust host (e.g. the Python SDK)
790/// to enrich the handshake [`PeerInfo`](core::v1::PeerInfo).
791///
792/// Every field is optional and best-effort. When set process-wide via
793/// [`set_peer_info_override`], [`peer_info`] merges these values over the
794/// Rust-detected defaults: a non-empty override field wins, an empty/absent one
795/// falls back to the Rust-detected value (`os`/`arch`/`package_version`). The
796/// `component` passed to [`peer_info`] is always honored; an override
797/// `component` is ignored so each call site keeps naming itself.
798///
799/// This is purely additive diagnostics: PeerInfo never gates compatibility.
800#[derive(Debug, Clone, Default, PartialEq, Eq)]
801pub struct PeerInfoOverride {
802 /// Implementation language, e.g. `"python"`. Empty leaves the Rust default.
803 pub language: String,
804 /// Language runtime version, e.g. `"3.11.4"`.
805 pub language_version: String,
806 /// Package/build version of the host SDK. Empty falls back to this crate's.
807 pub package_version: String,
808 /// Operating system, e.g. `"linux"`, `"macos"`. Empty falls back to the
809 /// Rust-detected [`std::env::consts::OS`].
810 pub os: String,
811 /// OS version/release.
812 pub os_version: String,
813 /// CPU architecture, e.g. `"x86_64"`. Empty falls back to the Rust-detected
814 /// [`std::env::consts::ARCH`].
815 pub arch: String,
816 /// High-value framework versions for debugging (e.g. `{"numpy":"1.26.4"}`).
817 pub framework_versions: HashMap<String, String>,
818 /// Additional advisory key/value diagnostics.
819 pub extra: HashMap<String, String>,
820}
821
822/// Process-wide host identity override consulted by [`peer_info`].
823///
824/// `None` (the default) means no override: pure-Rust peers handshake exactly as
825/// before. A Python-hosted process sets this once at import; the value applies
826/// to every handshake the process performs (it is the only host).
827static PEER_INFO_OVERRIDE: RwLock<Option<PeerInfoOverride>> = RwLock::new(None);
828
829/// Install (or replace) the process-wide [`PeerInfoOverride`] consulted by
830/// [`peer_info`]. Intended for non-Rust hosts (the Python SDK) to report their
831/// real runtime. Idempotent and thread-safe; passing the value again overwrites.
832pub fn set_peer_info_override(info: PeerInfoOverride) {
833 if let Ok(mut guard) = PEER_INFO_OVERRIDE.write() {
834 *guard = Some(info);
835 }
836}
837
838/// Build advisory [`PeerInfo`](core::v1::PeerInfo) diagnostics for a handshake.
839///
840/// `component` names the emitting participant (e.g. `"rlmesh-runtime"`,
841/// `"rlmesh-env"`, `"rlmesh-model"`). The build version, language, OS and arch
842/// default to this crate's compile environment (`language="rust"`, empty
843/// `language_version`/`os_version`/`framework_versions`).
844///
845/// When a process-wide [`PeerInfoOverride`] has been installed via
846/// [`set_peer_info_override`] (e.g. by the Python SDK), its non-empty fields win
847/// over the Rust defaults, falling back to the Rust-detected
848/// `os`/`arch`/`package_version` for any empty override field. The `component`
849/// argument is always preserved so each call site keeps naming itself. PeerInfo
850/// is advisory only and never gates compatibility.
851pub fn peer_info(component: &str) -> core::v1::PeerInfo {
852 let mut extra = HashMap::new();
853 extra.insert(
854 "rlmesh.workflow.base".to_string(),
855 WORKFLOW_EDITION_BASE.to_string(),
856 );
857 extra.insert(
858 "rlmesh.workflow.edition".to_string(),
859 CURRENT_WORKFLOW_EDITION.to_string(),
860 );
861 extra.insert("rlmesh.build.cohort".to_string(), BUILD_COHORT.to_string());
862 extra.insert("rlmesh.build.source".to_string(), BUILD_SOURCE.to_string());
863
864 let mut info = core::v1::PeerInfo {
865 component: component.to_string(),
866 package_version: env!("CARGO_PKG_VERSION").to_string(),
867 language: "rust".to_string(),
868 language_version: String::new(),
869 os: std::env::consts::OS.to_string(),
870 os_version: String::new(),
871 arch: std::env::consts::ARCH.to_string(),
872 framework_versions: HashMap::new(),
873 extra,
874 };
875
876 if let Ok(guard) = PEER_INFO_OVERRIDE.read()
877 && let Some(over) = guard.as_ref()
878 {
879 // Python (or other host) values win when present; empty fields keep the
880 // Rust-detected fallback. `component` is never overridden.
881 if !over.language.is_empty() {
882 info.language = over.language.clone();
883 }
884 if !over.language_version.is_empty() {
885 info.language_version = over.language_version.clone();
886 }
887 if !over.package_version.is_empty() {
888 info.package_version = over.package_version.clone();
889 }
890 if !over.os.is_empty() {
891 info.os = over.os.clone();
892 }
893 if !over.os_version.is_empty() {
894 info.os_version = over.os_version.clone();
895 }
896 if !over.arch.is_empty() {
897 info.arch = over.arch.clone();
898 }
899 if !over.framework_versions.is_empty() {
900 info.framework_versions = over.framework_versions.clone();
901 }
902 if !over.extra.is_empty() {
903 info.extra.extend(over.extra.clone());
904 }
905 }
906
907 info
908}
909
910pub mod env {
911 pub mod v1 {
912 tonic::include_proto!("rlmesh.env.v1");
913 }
914}
915
916pub mod spaces {
917 pub mod v1 {
918 tonic::include_proto!("rlmesh.spaces.v1");
919 }
920}
921
922pub mod model {
923 pub mod v1 {
924 tonic::include_proto!("rlmesh.model.v1");
925 }
926}
927
928/// The endpoint-local split of `JoinResponse.endpoint_total_ns` (ns), plus the
929/// pre-handler wait and slot depth a pipelining model endpoint reports.
930///
931/// A zero duration is "not measured": a peer built before these fields existed
932/// sends none of them, and a reader sees this default. The gauges are `Option`
933/// because for them a measured zero is a real sample (an even vector has zero
934/// skew; an engine that just evicted holds zero episodes) that must stay
935/// distinguishable from a peer that never measures. `in_flight` is model-only;
936/// `lane_skew_ns` is env-only.
937#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
938pub struct EndpointPhases {
939 /// Decoding the request off the wire.
940 pub decode_ns: u64,
941 /// The env or model implementation's own work, user code included.
942 pub user_ns: u64,
943 /// Encoding the response onto the wire.
944 pub encode_ns: u64,
945 /// Wait before the handler ran. Model: concurrency permit, route gate,
946 /// handler lock. Env: the env lock, which serializes ops on one env.
947 pub queue_ns: u64,
948 /// Adapter work inside `user_ns` (observation assembly + action apply) —
949 /// a sub-span, not additive: the model's own forward is `user_ns -
950 /// adapter_ns`. Model-only, and zero for a spec-less route.
951 pub adapter_ns: u64,
952 /// Episodes whose frame-stack windows the endpoint's adapter engine held
953 /// when the response was stamped, across all routes. Model-only; `None`
954 /// from a handler that keeps no such accounting.
955 pub held_episodes: Option<u32>,
956 /// Bytes those held frame-stack windows occupy. Model-only; `None` from a
957 /// handler that keeps no such accounting.
958 pub held_state_bytes: Option<u64>,
959 /// Requests holding a concurrency slot when this request was dispatched to
960 /// its handler (>= 1): slot occupancy, not parallelism.
961 pub in_flight: u32,
962 /// Straggler skew across a vector env's lanes for this op — see
963 /// [`lane_skew_ns`]. Env-only; `None` from an env that does not time its
964 /// lanes, `Some(0)` from one whose lanes were even.
965 pub lane_skew_ns: Option<u64>,
966}
967
968impl EndpointPhases {
969 /// Wire form of a measurement: an unmeasured (zero) phase is left off the
970 /// message rather than sent as a `0` a reader cannot tell from silence.
971 pub fn reported(ns: u64) -> Option<u64> {
972 (ns != 0).then_some(ns)
973 }
974
975 /// Build a split from measured phase durations, saturating at `u64::MAX` ns.
976 pub fn from_durations(
977 decode: std::time::Duration,
978 user: std::time::Duration,
979 encode: std::time::Duration,
980 ) -> Self {
981 fn ns(duration: std::time::Duration) -> u64 {
982 duration.as_nanos().min(u128::from(u64::MAX)) as u64
983 }
984 Self {
985 decode_ns: ns(decode),
986 user_ns: ns(user),
987 encode_ns: ns(encode),
988 ..Self::default()
989 }
990 }
991
992 /// Fold an inner layer's own split into this layer's decode/call/encode
993 /// measurement. The inner layer's decode and encode accumulate into the
994 /// outer ones, and `user_ns` keeps the rest of the call — the inner user
995 /// work plus whatever it cost to reach it (GIL wait, adapter overhead) —
996 /// so the phases still sum to this layer's whole measurement. An inner
997 /// layer that measures nothing, or whose wire share does not even fit
998 /// inside the measured call, leaves the whole call as user time.
999 pub fn nest(decode_ns: u64, call_ns: u64, encode_ns: u64, inner: Self) -> Self {
1000 // Lane skew is the inner env's alone; an outer layer never measures it.
1001 let inner_wire = inner.decode_ns.saturating_add(inner.encode_ns);
1002 if inner_wire > call_ns {
1003 return Self {
1004 decode_ns,
1005 user_ns: call_ns,
1006 encode_ns,
1007 lane_skew_ns: inner.lane_skew_ns,
1008 ..Self::default()
1009 };
1010 }
1011 Self {
1012 decode_ns: decode_ns.saturating_add(inner.decode_ns),
1013 user_ns: call_ns - inner_wire,
1014 encode_ns: encode_ns.saturating_add(inner.encode_ns),
1015 lane_skew_ns: inner.lane_skew_ns,
1016 ..Self::default()
1017 }
1018 }
1019
1020 /// Read the split an env peer stamped; all-zero for a peer that sends none.
1021 pub fn from_env_response(response: &env::v1::JoinResponse) -> Self {
1022 Self {
1023 decode_ns: response.decode_ns.unwrap_or(0),
1024 user_ns: response.user_ns.unwrap_or(0),
1025 encode_ns: response.encode_ns.unwrap_or(0),
1026 queue_ns: response.queue_ns.unwrap_or(0),
1027 lane_skew_ns: response.lane_skew_ns,
1028 ..Self::default()
1029 }
1030 }
1031
1032 /// Read the split a model peer stamped; all-zero for a peer that sends none.
1033 pub fn from_model_response(response: &model::v1::JoinResponse) -> Self {
1034 Self {
1035 decode_ns: response.decode_ns.unwrap_or(0),
1036 user_ns: response.user_ns.unwrap_or(0),
1037 encode_ns: response.encode_ns.unwrap_or(0),
1038 queue_ns: response.queue_ns.unwrap_or(0),
1039 in_flight: response.in_flight.unwrap_or(0),
1040 adapter_ns: response.adapter_ns.unwrap_or(0),
1041 held_episodes: response.held_episodes,
1042 held_state_bytes: response.held_state_bytes,
1043 ..Self::default()
1044 }
1045 }
1046}
1047
1048/// Nanoseconds elapsed since `started_at`, saturating instead of wrapping.
1049pub fn elapsed_ns(started_at: std::time::Instant) -> u64 {
1050 started_at.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64
1051}
1052
1053/// Straggler skew across a vector env's lanes for one op: how much longer the
1054/// slowest lane took than the median lane, in nanoseconds.
1055///
1056/// One scalar per op, so a 32-lane env costs one metric rather than 32 series,
1057/// and the aggregator's p50/p95/p99 over it answer how often and how badly a
1058/// single lane gates the batch the other lanes ride in. A shifted-but-even
1059/// vector reads zero; one bad lane among healthy ones reads its full excess.
1060///
1061/// `lane_ns` is reordered in place and never grown — pass a scratch buffer the
1062/// env keeps across steps. Fewer than two lanes have nothing to compare and
1063/// read zero. The median is the lower one for an even lane count, so a
1064/// two-lane vector still reports its straggler.
1065pub fn lane_skew_ns(lane_ns: &mut [u64]) -> u64 {
1066 if lane_ns.len() < 2 {
1067 return 0;
1068 }
1069 let (_, median, slower) = lane_ns.select_nth_unstable((lane_ns.len() - 1) / 2);
1070 let median = *median;
1071 slower
1072 .iter()
1073 .copied()
1074 .max()
1075 .unwrap_or(median)
1076 .saturating_sub(median)
1077}
1078
1079/// A completing lane's final info from a step's `infos`, `lane` being its
1080/// position among the `width` lanes the step covered. An explicit
1081/// `final_info` entry wins (masked by `_final_info`). Otherwise a single-lane
1082/// step's info IS the final info, and a vector step's lane is sliced out of the
1083/// Gymnasium vector-info layout. `None` when the lane has nothing.
1084pub fn lane_final_info(
1085 info: Option<&spaces::v1::MetaMap>,
1086 lane: usize,
1087 width: usize,
1088) -> Option<spaces::v1::MetaMap> {
1089 use spaces::v1::meta_value::Kind;
1090 let info = info?;
1091 let Some(final_info) = info.entries.get("final_info") else {
1092 if width == 1 {
1093 return Some(info.clone());
1094 }
1095 let sliced = vector_info_lane(info, lane, width);
1096 return (!sliced.entries.is_empty()).then_some(sliced);
1097 };
1098 let is_present = match info.entries.get("_final_info") {
1099 Some(mask) => meta_bool_at(mask, lane).unwrap_or(false),
1100 None => width == 1,
1101 };
1102 if !is_present {
1103 return None;
1104 }
1105 match &final_info.kind {
1106 Some(Kind::Map(map)) => Some(map.clone()),
1107 Some(Kind::List(list)) => match &list.items.get(lane)?.kind {
1108 Some(Kind::Map(map)) => Some(map.clone()),
1109 _ => None,
1110 },
1111 _ => None,
1112 }
1113}
1114
1115/// One lane of a Gymnasium vector info: each `key` holds a per-lane array or a
1116/// nested info map, and its `_key` mask marks the lanes that actually set it.
1117fn vector_info_lane(info: &spaces::v1::MetaMap, lane: usize, width: usize) -> spaces::v1::MetaMap {
1118 use spaces::v1::meta_value::Kind;
1119 let entries = info
1120 .entries
1121 .iter()
1122 .filter(|(key, _)| {
1123 !key.strip_prefix('_')
1124 .is_some_and(|masked| info.entries.contains_key(masked))
1125 })
1126 .filter(|(key, _)| {
1127 info.entries
1128 .get(&format!("_{key}"))
1129 .is_none_or(|mask| meta_bool_at(mask, lane) == Some(true))
1130 })
1131 .filter_map(|(key, value)| {
1132 let lane_value = match &value.kind {
1133 Some(Kind::Map(map)) => spaces::v1::MetaValue {
1134 kind: Some(Kind::Map(vector_info_lane(map, lane, width))),
1135 },
1136 Some(Kind::List(list)) if list.items.len() == width => list.items[lane].clone(),
1137 _ => return None,
1138 };
1139 Some((key.clone(), lane_value))
1140 })
1141 .collect();
1142 spaces::v1::MetaMap { entries }
1143}
1144
1145fn meta_bool_at(value: &spaces::v1::MetaValue, lane: usize) -> Option<bool> {
1146 use spaces::v1::meta_value::Kind;
1147 match &value.kind {
1148 Some(Kind::List(list)) => match list.items.get(lane)?.kind {
1149 Some(Kind::Bool(flag)) => Some(flag),
1150 _ => None,
1151 },
1152 _ => None,
1153 }
1154}
1155
1156/// The `rlmesh.toml` array scan `build.rs` reads the retained edition list
1157/// with, compiled into the test build so `cargo test` covers the real parser —
1158/// a `#[test]` inside a build script never runs.
1159#[cfg(test)]
1160#[path = "../build_manifest.rs"]
1161mod build_manifest;
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::{
1166 CURRENT_WORKFLOW_EDITION, PROTOCOL_GENERATION, SUPPORTED_WORKFLOW_EDITIONS, SessionOffer,
1167 evaluate_handshake, is_protocol_generation_supported, negotiate_session_floor,
1168 negotiate_workflow_edition, supported_workflow_editions,
1169 };
1170
1171 /// One tier of a negotiation case: what it CAN drive, and what it WANTs
1172 /// (`None` = undeclared, which negotiation reads as `max(can)`).
1173 fn tier(can: &[&str], want: Option<&str>) -> SessionOffer {
1174 SessionOffer {
1175 editions: offer(can),
1176 preferred: want.map(str::to_string),
1177 }
1178 }
1179
1180 #[test]
1181 fn lane_final_info_reads_each_gymnasium_info_layout() {
1182 use super::lane_final_info;
1183 use super::spaces::v1::meta_value::Kind;
1184 use super::spaces::v1::{MetaList, MetaMap, MetaValue};
1185
1186 let value = |kind: Kind| MetaValue { kind: Some(kind) };
1187 let list = |items: Vec<MetaValue>| value(Kind::List(MetaList { items }));
1188 let map = |entries: Vec<(&str, MetaValue)>| MetaMap {
1189 entries: entries
1190 .into_iter()
1191 .map(|(key, value)| (key.to_string(), value))
1192 .collect(),
1193 };
1194 let success = |flag: bool| map(vec![("is_success", value(Kind::Bool(flag)))]);
1195
1196 let scalar = success(false);
1197 assert_eq!(lane_final_info(Some(&scalar), 0, 1), Some(success(false)));
1198 assert_eq!(lane_final_info(None, 0, 2), None);
1199
1200 let explicit = map(vec![
1201 (
1202 "final_info",
1203 list(vec![
1204 value(Kind::Map(success(true))),
1205 value(Kind::Map(MetaMap::default())),
1206 ]),
1207 ),
1208 (
1209 "_final_info",
1210 list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1211 ),
1212 ]);
1213 assert_eq!(lane_final_info(Some(&explicit), 0, 2), Some(success(true)));
1214 assert_eq!(lane_final_info(Some(&explicit), 1, 2), None);
1215
1216 // No masks: a lane-wide array is still per-lane.
1217 let unmasked = map(vec![(
1218 "is_success",
1219 list(vec![value(Kind::Bool(false)), value(Kind::Bool(true))]),
1220 )]);
1221 assert_eq!(lane_final_info(Some(&unmasked), 1, 2), Some(success(true)));
1222
1223 let masked_out = map(vec![
1224 (
1225 "is_success",
1226 list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1227 ),
1228 (
1229 "_is_success",
1230 list(vec![value(Kind::Bool(true)), value(Kind::Bool(false))]),
1231 ),
1232 ]);
1233 assert_eq!(lane_final_info(Some(&masked_out), 1, 2), None);
1234 }
1235
1236 #[test]
1237 fn phases_from_a_peer_that_stamps_nothing_read_back_as_zero() {
1238 use super::{EndpointPhases, env, model};
1239
1240 // An older peer sends neither the split nor the queue scalars.
1241 let env_response = env::v1::JoinResponse {
1242 endpoint_total_ns: Some(1_000),
1243 ..Default::default()
1244 };
1245 assert_eq!(
1246 EndpointPhases::from_env_response(&env_response),
1247 EndpointPhases::default()
1248 );
1249
1250 let model_response = model::v1::JoinResponse {
1251 endpoint_total_ns: Some(1_000),
1252 ..Default::default()
1253 };
1254 assert_eq!(
1255 EndpointPhases::from_model_response(&model_response),
1256 EndpointPhases::default()
1257 );
1258
1259 // A peer that does stamp them reads back verbatim.
1260 let stamped = model::v1::JoinResponse {
1261 endpoint_total_ns: Some(1_000),
1262 decode_ns: Some(10),
1263 user_ns: Some(20),
1264 encode_ns: Some(30),
1265 queue_ns: Some(40),
1266 in_flight: Some(3),
1267 adapter_ns: Some(15),
1268 held_episodes: Some(5),
1269 held_state_bytes: Some(6_000),
1270 ..Default::default()
1271 };
1272 assert_eq!(
1273 EndpointPhases::from_model_response(&stamped),
1274 EndpointPhases {
1275 decode_ns: 10,
1276 user_ns: 20,
1277 encode_ns: 30,
1278 queue_ns: 40,
1279 in_flight: 3,
1280 adapter_ns: 15,
1281 held_episodes: Some(5),
1282 held_state_bytes: Some(6_000),
1283 lane_skew_ns: None,
1284 }
1285 );
1286
1287 // Lane skew rides the env response only.
1288 let skewed = env::v1::JoinResponse {
1289 endpoint_total_ns: Some(1_000),
1290 lane_skew_ns: Some(880),
1291 ..Default::default()
1292 };
1293 assert_eq!(
1294 EndpointPhases::from_env_response(&skewed).lane_skew_ns,
1295 Some(880)
1296 );
1297 // A measured zero (an even vector) is a sample, not an absence.
1298 let even = env::v1::JoinResponse {
1299 lane_skew_ns: Some(0),
1300 ..Default::default()
1301 };
1302 assert_eq!(
1303 EndpointPhases::from_env_response(&even).lane_skew_ns,
1304 Some(0)
1305 );
1306
1307 // An unmeasured phase is left off the wire rather than sent as a zero.
1308 assert_eq!(EndpointPhases::reported(0), None);
1309 assert_eq!(EndpointPhases::reported(7), Some(7));
1310 }
1311
1312 #[test]
1313 fn nesting_folds_an_inner_split_into_the_outer_one() {
1314 use super::EndpointPhases;
1315
1316 // An inner layer that measures nothing leaves the whole call as user time.
1317 assert_eq!(
1318 EndpointPhases::nest(1, 10, 2, EndpointPhases::default()),
1319 EndpointPhases {
1320 decode_ns: 1,
1321 user_ns: 10,
1322 encode_ns: 2,
1323 ..EndpointPhases::default()
1324 }
1325 );
1326
1327 // One that does splits the call: its own decode/encode join this layer's,
1328 // and user keeps the remainder of the call.
1329 let inner = EndpointPhases {
1330 decode_ns: 3,
1331 user_ns: 5,
1332 encode_ns: 2,
1333 ..EndpointPhases::default()
1334 };
1335 assert_eq!(
1336 EndpointPhases::nest(1, 10, 2, inner),
1337 EndpointPhases {
1338 decode_ns: 4,
1339 user_ns: 5,
1340 encode_ns: 4,
1341 ..EndpointPhases::default()
1342 }
1343 );
1344
1345 // A call longer than the inner layer's own accounting (GIL wait, adapter
1346 // overhead) keeps the residual in user, so the phases still sum to this
1347 // layer's whole measurement: 1 + 14 + 2 == 4 + 9 + 4.
1348 assert_eq!(
1349 EndpointPhases::nest(1, 14, 2, inner),
1350 EndpointPhases {
1351 decode_ns: 4,
1352 user_ns: 9,
1353 encode_ns: 4,
1354 ..EndpointPhases::default()
1355 }
1356 );
1357
1358 // An inner split whose wire share does not fit inside the measured call
1359 // is inconsistent; folding it would break the sum, so none of it folds.
1360 assert_eq!(
1361 EndpointPhases::nest(1, 4, 2, inner),
1362 EndpointPhases {
1363 decode_ns: 1,
1364 user_ns: 4,
1365 encode_ns: 2,
1366 ..EndpointPhases::default()
1367 }
1368 );
1369 }
1370
1371 #[test]
1372 fn lane_skew_is_the_slowest_lane_over_the_median_one() {
1373 use super::lane_skew_ns;
1374
1375 // One straggler among healthy lanes: its full excess over a typical lane.
1376 assert_eq!(lane_skew_ns(&mut [10, 10, 10, 10, 10, 10, 10, 900]), 890);
1377 // An evenly slow vector is not a straggler — the whole distribution moved,
1378 // which `endpoint.user` already shows.
1379 assert_eq!(lane_skew_ns(&mut [900; 8]), 0);
1380 // Two lanes compare against the faster one, so the straggler still reads.
1381 assert_eq!(lane_skew_ns(&mut [10, 100]), 90);
1382 // A lane faster than its peers is not skew.
1383 assert_eq!(lane_skew_ns(&mut [1, 100, 100, 100]), 0);
1384 // Nothing to compare.
1385 assert_eq!(lane_skew_ns(&mut [42]), 0);
1386 assert_eq!(lane_skew_ns(&mut []), 0);
1387 }
1388
1389 #[test]
1390 fn nesting_keeps_the_inner_envs_lane_skew() {
1391 use super::EndpointPhases;
1392
1393 let measured = EndpointPhases {
1394 user_ns: 5,
1395 lane_skew_ns: Some(77),
1396 ..EndpointPhases::default()
1397 };
1398 assert_eq!(
1399 EndpointPhases::nest(1, 10, 2, measured).lane_skew_ns,
1400 Some(77)
1401 );
1402
1403 // An env that times its lanes but reports no split of its own still gets
1404 // the skew through.
1405 let skew_only = EndpointPhases {
1406 lane_skew_ns: Some(77),
1407 ..EndpointPhases::default()
1408 };
1409 assert_eq!(
1410 EndpointPhases::nest(1, 10, 2, skew_only).lane_skew_ns,
1411 Some(77)
1412 );
1413 }
1414
1415 fn offer(editions: &[&str]) -> Vec<String> {
1416 editions.iter().map(|edition| edition.to_string()).collect()
1417 }
1418
1419 #[test]
1420 fn peer_info_default_then_override_merges_python_with_rust_fallback() {
1421 use super::{PeerInfoOverride, peer_info, set_peer_info_override};
1422 use std::collections::HashMap;
1423
1424 // No override installed yet: a pure-Rust peer reports the Rust defaults.
1425 let rust_info = peer_info("rlmesh-env");
1426 assert_eq!(rust_info.component, "rlmesh-env");
1427 assert_eq!(rust_info.language, "rust");
1428 assert!(rust_info.language_version.is_empty());
1429 assert!(rust_info.framework_versions.is_empty());
1430 let detected_os = rust_info.os.clone();
1431 let detected_arch = rust_info.arch.clone();
1432 let detected_pkg = rust_info.package_version.clone();
1433
1434 // Install a Python-style override with `os`/`package_version` left empty
1435 // so the Rust-detected fallbacks fill them.
1436 let mut frameworks = HashMap::new();
1437 frameworks.insert("numpy".to_string(), "1.26.4".to_string());
1438 set_peer_info_override(PeerInfoOverride {
1439 language: "python".to_string(),
1440 language_version: "3.11.4".to_string(),
1441 package_version: String::new(),
1442 os: String::new(),
1443 os_version: "ubuntu-22.04".to_string(),
1444 arch: "aarch64".to_string(),
1445 framework_versions: frameworks,
1446 extra: HashMap::from([("rlmesh.startup.listen_ms".to_string(), "4200".to_string())]),
1447 });
1448
1449 let py_info = peer_info("rlmesh-env");
1450 // Host-supplied extras ride beside the build keys, never replace them.
1451 assert_eq!(
1452 py_info
1453 .extra
1454 .get("rlmesh.startup.listen_ms")
1455 .map(String::as_str),
1456 Some("4200")
1457 );
1458 assert!(py_info.extra.contains_key("rlmesh.build.cohort"));
1459 // component still names this call site; not taken from the override.
1460 assert_eq!(py_info.component, "rlmesh-env");
1461 // Python values win.
1462 assert_eq!(py_info.language, "python");
1463 assert_eq!(py_info.language_version, "3.11.4");
1464 assert_eq!(py_info.os_version, "ubuntu-22.04");
1465 assert_eq!(py_info.arch, "aarch64");
1466 assert_eq!(
1467 py_info.framework_versions.get("numpy").map(String::as_str),
1468 Some("1.26.4")
1469 );
1470 // Empty override fields fall back to the Rust-detected values.
1471 assert_eq!(py_info.os, detected_os);
1472 assert_eq!(py_info.package_version, detected_pkg);
1473 assert_eq!(
1474 py_info
1475 .extra
1476 .get("rlmesh.workflow.edition")
1477 .map(String::as_str),
1478 Some(CURRENT_WORKFLOW_EDITION)
1479 );
1480 // `arch` was overridden, so it differs from the detected value here.
1481 let _ = detected_arch;
1482 }
1483
1484 #[test]
1485 fn has_capability_reads_advertised_features() {
1486 use super::{capabilities, capability_map, has_capability};
1487 let map = capability_map(&[capabilities::MODEL_CONCURRENT_PREDICT_V1]);
1488 assert!(has_capability(
1489 &map,
1490 capabilities::MODEL_CONCURRENT_PREDICT_V1
1491 ));
1492 assert!(!has_capability(&map, "rlmesh.not.advertised.v1"));
1493 // The documented value grammar: emitters write the literal "true", and
1494 // any other spelling reads as absent rather than as a guessed "on".
1495 assert_eq!(map[capabilities::MODEL_CONCURRENT_PREDICT_V1], "true");
1496 for value in ["1", "yes", "TRUE", ""] {
1497 let odd =
1498 std::collections::HashMap::from([("rlmesh.odd.v1".to_string(), value.to_string())]);
1499 assert!(!has_capability(&odd, "rlmesh.odd.v1"), "{value:?}");
1500 }
1501 }
1502
1503 #[test]
1504 fn env_subset_step_keeps_its_shipped_wire_spelling() {
1505 // Peers built against 0.1.0 advertise the bare `subset_step` key; the
1506 // constant renames the site, never the string on the wire.
1507 assert_eq!(super::capabilities::ENV_SUBSET_STEP, "subset_step");
1508 }
1509
1510 #[test]
1511 fn protocol_generation_is_plain_equality() {
1512 // The only generation check is equality with this build's generation —
1513 // there is no support window. Whitespace is trimmed; anything else is a
1514 // hard mismatch (a deliberate major break).
1515 assert!(is_protocol_generation_supported(PROTOCOL_GENERATION));
1516 assert!(is_protocol_generation_supported(&format!(
1517 " {PROTOCOL_GENERATION} "
1518 )));
1519 assert!(!is_protocol_generation_supported("rlmesh-wire-v2"));
1520 assert!(!is_protocol_generation_supported(""));
1521 assert!(!is_protocol_generation_supported("0.1.0"));
1522 }
1523
1524 #[test]
1525 fn split_editions_recovers_every_supported_edition() {
1526 // The build-time list is comma-separated; today's tree generates one
1527 // entry, so cover the retained-fallback shape here rather than waiting
1528 // for a second edition to be sealed.
1529 const LIST: &str = "2026.09-0.2.0-rc.1,2026.06";
1530 const EDITIONS: [&str; super::edition_count(LIST)] = super::split_editions(LIST);
1531 assert_eq!(EDITIONS, ["2026.09-0.2.0-rc.1", "2026.06"]);
1532 assert_eq!(super::edition_count("2026.06"), 1);
1533 }
1534
1535 #[test]
1536 fn manifest_string_list_reads_both_array_spellings() {
1537 use super::build_manifest::manifest_string_list;
1538
1539 // `bump_version.py` writes the single-line spelling, but a hand-edited
1540 // manifest may spread the array over lines: both must offer the same
1541 // editions, or a retained edition would silently vanish from the build.
1542 let single = "[workflow]\nsupported_editions = [\"2026.09-0.2.0-rc.1\", \"2026.06\"]\n";
1543 let multi = "[workflow]\nsupported_editions = [\n \"2026.09-0.2.0-rc.1\", # cohort\n \
1544 \"2026.06\",\n]\n";
1545 let want = ["2026.09-0.2.0-rc.1".to_string(), "2026.06".to_string()];
1546 assert_eq!(manifest_string_list(single, "supported_editions"), want);
1547 assert_eq!(manifest_string_list(multi, "supported_editions"), want);
1548 assert!(manifest_string_list(single, "current_edition").is_empty());
1549 }
1550
1551 #[test]
1552 fn supported_workflow_editions_lead_with_current() {
1553 // The list is generated from `rlmesh.toml`; whatever it retains, this
1554 // build's own cohort is first, nothing repeats, and the owned-string
1555 // form handed to protobuf mirrors it exactly.
1556 assert!(!SUPPORTED_WORKFLOW_EDITIONS.is_empty());
1557 assert_eq!(SUPPORTED_WORKFLOW_EDITIONS[0], CURRENT_WORKFLOW_EDITION);
1558 let unique: std::collections::BTreeSet<&&str> =
1559 SUPPORTED_WORKFLOW_EDITIONS.iter().collect();
1560 assert_eq!(unique.len(), SUPPORTED_WORKFLOW_EDITIONS.len());
1561 assert!(
1562 SUPPORTED_WORKFLOW_EDITIONS
1563 .iter()
1564 .all(|edition| !edition.trim().is_empty())
1565 );
1566 assert_eq!(
1567 supported_workflow_editions(),
1568 SUPPORTED_WORKFLOW_EDITIONS
1569 .iter()
1570 .map(|edition| (*edition).to_string())
1571 .collect::<Vec<_>>()
1572 );
1573 }
1574
1575 #[test]
1576 fn negotiation_selects_mutual_edition() {
1577 let runtime = SessionOffer::this_build(None);
1578 assert_eq!(
1579 negotiate_workflow_edition(&SessionOffer::new(&[CURRENT_WORKFLOW_EDITION]), &runtime),
1580 Ok(CURRENT_WORKFLOW_EDITION.to_string())
1581 );
1582 assert_eq!(
1583 negotiate_workflow_edition(
1584 &SessionOffer::new(&["2025.01", CURRENT_WORKFLOW_EDITION, "2031.12"]),
1585 &runtime
1586 ),
1587 Ok(CURRENT_WORKFLOW_EDITION.to_string())
1588 );
1589 }
1590
1591 #[test]
1592 fn negotiation_trims_offered_editions() {
1593 let padded = SessionOffer {
1594 editions: vec![format!(" {CURRENT_WORKFLOW_EDITION} ")],
1595 preferred: None,
1596 };
1597 assert_eq!(
1598 negotiate_workflow_edition(&padded, &SessionOffer::this_build(None)),
1599 Ok(CURRENT_WORKFLOW_EDITION.to_string())
1600 );
1601 }
1602
1603 #[test]
1604 fn negotiation_rejects_unknown_or_empty_offers() {
1605 let runtime = SessionOffer::this_build(None);
1606 for offered in [
1607 &[][..],
1608 &[""][..],
1609 &["2026"][..],
1610 &["next"][..],
1611 &["2026.11", "2027.01"][..],
1612 ] {
1613 let refusal = negotiate_workflow_edition(&SessionOffer::new(offered), &runtime)
1614 .expect_err("no mutual edition");
1615 // The refusal names both tiers and both of their sets.
1616 let message = refusal.to_string();
1617 assert!(message.contains("env"), "{message}");
1618 assert!(message.contains("runtime"), "{message}");
1619 assert!(message.contains(CURRENT_WORKFLOW_EDITION), "{message}");
1620 }
1621 }
1622
1623 #[test]
1624 fn evaluate_handshake_gates_generation_only() {
1625 // The handshake decides ONE thing: protocol generation. Editions are the
1626 // runtime's call (the floor), so a generation-ok peer is compatible
1627 // regardless of editions — even with no mutual edition (it fails later at
1628 // the floor, with a clearer all-tiers message).
1629 assert!(evaluate_handshake(PROTOCOL_GENERATION));
1630
1631 // A protocol mismatch is never compatible.
1632 assert!(!evaluate_handshake("rlmesh-wire-v2"));
1633 }
1634
1635 #[test]
1636 fn is_retained_edition_matches_the_window() {
1637 use super::{Edition, is_retained_edition};
1638 let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1639 assert!(is_retained_edition(current));
1640 // Trimmed, and this build's cohort spelling names the same arm as its base.
1641 assert_eq!(
1642 Edition::parse(&format!(" {CURRENT_WORKFLOW_EDITION} ")),
1643 Ok(current)
1644 );
1645 assert_eq!(Edition::parse(super::WORKFLOW_EDITION_BASE), Ok(current));
1646 // An edition no arm implements never resolves, so it is never retained.
1647 assert!(Edition::parse("2099.01").is_err());
1648 assert!(Edition::parse("").is_err());
1649 }
1650
1651 /// The base names the contract; a cohort suffix only identifies a moving
1652 /// build of it. So EVERY cohort spelling of a known base resolves to that
1653 /// base's arm -- including one this build never spells itself, which is what a
1654 /// policy-legal manifest retaining an older base's cohort would carry.
1655 #[test]
1656 fn parse_resolves_any_cohort_spelling_of_a_known_base() {
1657 use super::{Edition, WORKFLOW_EDITION_BASE, parse_retained_edition};
1658
1659 let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1660 assert_eq!(Edition::parse(WORKFLOW_EDITION_BASE), Ok(current));
1661 assert_eq!(Edition::parse(CURRENT_WORKFLOW_EDITION), Ok(current));
1662 for foreign in [
1663 format!("{WORKFLOW_EDITION_BASE}-0.0.1-rc.1"),
1664 format!("{WORKFLOW_EDITION_BASE}-dev.deadbeef"),
1665 ] {
1666 assert_eq!(
1667 Edition::parse(&foreign),
1668 Ok(current),
1669 "{foreign} is a cohort of a known base"
1670 );
1671 }
1672 // A base no arm implements is refused, bare or suffixed.
1673 assert!(Edition::parse("2099.01").is_err());
1674 assert!(Edition::parse("2099.01-0.1.0-rc.12").is_err());
1675
1676 // The shared boundary refuses the same names, naming the arrived string
1677 // and the retained list.
1678 assert_eq!(
1679 parse_retained_edition(CURRENT_WORKFLOW_EDITION),
1680 Ok(current)
1681 );
1682 let error = parse_retained_edition(" 2099.01 ").expect_err("no arm implements it");
1683 assert!(
1684 error.contains("\"2099.01\"") && error.contains(CURRENT_WORKFLOW_EDITION),
1685 "expected the arrived name and the retained list, got: {error}"
1686 );
1687 }
1688
1689 /// A declaration is a ceiling, so it is only usable when it admits something
1690 /// this build offers. Every name this build offers is declarable by
1691 /// construction, and so is the bare base on EVERY build — it admits every
1692 /// cohort of itself, which is what makes it the value to write down. A
1693 /// cohort that would leave the ceiling below the whole CAN set is refused
1694 /// here rather than deadlocking negotiation.
1695 #[test]
1696 fn a_declaration_must_leave_this_build_something_to_run() {
1697 use super::{Edition, WORKFLOW_EDITION_BASE, parse_declared_edition};
1698
1699 let current = Edition::parse(CURRENT_WORKFLOW_EDITION).expect("the current edition parses");
1700 for offered in SUPPORTED_WORKFLOW_EDITIONS {
1701 assert_eq!(
1702 parse_declared_edition(offered),
1703 Ok(current),
1704 "{offered} is offered, so it is declarable"
1705 );
1706 }
1707 assert_eq!(parse_declared_edition(WORKFLOW_EDITION_BASE), Ok(current));
1708 assert_eq!(
1709 parse_declared_edition(&format!(" {WORKFLOW_EDITION_BASE} ")),
1710 Ok(current)
1711 );
1712 // A cohort of this base that sorts below everything offered: on a
1713 // sealed build the bare base is offered and admitted, on a prerelease
1714 // or dev build only a higher cohort is, so it is refused naming both
1715 // halves and the base to declare instead.
1716 let stale_cohort = format!("{WORKFLOW_EDITION_BASE}-0.0.0");
1717 match parse_declared_edition(&stale_cohort) {
1718 Ok(edition) => {
1719 assert_eq!(edition, current);
1720 assert!(SUPPORTED_WORKFLOW_EDITIONS.contains(&WORKFLOW_EDITION_BASE));
1721 }
1722 Err(error) => {
1723 assert!(
1724 error.contains(&stale_cohort)
1725 && error.contains(CURRENT_WORKFLOW_EDITION)
1726 && error.contains(&format!("{WORKFLOW_EDITION_BASE:?} base")),
1727 "expected both halves of the mismatch, got: {error}"
1728 );
1729 }
1730 }
1731 // An unimplemented base still fails on the retained check, unchanged.
1732 assert!(parse_declared_edition("2099.01").is_err());
1733 }
1734
1735 /// The two shapes of a WANT: a bare base is a base-level ceiling, a cohort
1736 /// spelling keeps the exact order.
1737 #[test]
1738 fn want_admits_by_base_when_bare_and_by_key_when_suffixed() {
1739 use super::want_admits;
1740
1741 assert!(want_admits("2026.06", "2026.06"));
1742 assert!(want_admits("2026.06", "2026.06-dev.aaa"));
1743 assert!(want_admits("2026.06", "2026.06-0.1.0-rc.12"));
1744 assert!(want_admits("2026.06", "2026.01"));
1745 assert!(!want_admits("2026.06", "2026.08"));
1746 assert!(!want_admits("2026.06", "2026.08-dev.aaa"));
1747
1748 assert!(want_admits("2026.06-dev.bbb", "2026.06-dev.bbb"));
1749 assert!(want_admits("2026.06-dev.bbb", "2026.06-dev.aaa"));
1750 assert!(want_admits("2026.06-dev.bbb", "2026.06"));
1751 assert!(!want_admits("2026.06-dev.bbb", "2026.06-dev.ccc"));
1752 assert!(!want_admits("2026.06-dev.bbb", "2026.08"));
1753 }
1754
1755 /// Every arm has a defaults row, and every name this build offers or runs
1756 /// under resolves back to an arm. `defaults` is an exhaustive match, so the
1757 /// "has a row" half is enforced by the compiler; this pins the rest.
1758 #[test]
1759 fn every_arm_is_listed_and_has_a_row() {
1760 use super::{Edition, defaults, is_retained_edition};
1761 for edition in Edition::ALL {
1762 // Exhaustive: a new arm does not compile until it is named here, so
1763 // one cannot be added without visiting this test. `Edition::ALL`
1764 // itself is held complete by the retained-list loop below, which
1765 // parses every name this build offers back through it.
1766 match edition {
1767 Edition::E2026_06 => {
1768 assert!(Edition::ALL.contains(&Edition::E2026_06));
1769 }
1770 }
1771 let row = defaults(*edition);
1772 assert!(!row.trial_index_option_key.is_empty());
1773 assert!(!row.conformance_warning_info_key.is_empty());
1774 assert_eq!(Edition::parse(edition.base()), Ok(*edition));
1775 assert_eq!(edition.to_string(), edition.base());
1776 }
1777 assert_eq!(
1778 Edition::parse(CURRENT_WORKFLOW_EDITION).map(is_retained_edition),
1779 Ok(true)
1780 );
1781 assert_eq!(
1782 Edition::parse(CURRENT_WORKFLOW_EDITION),
1783 Ok(Edition::current())
1784 );
1785 for retained in SUPPORTED_WORKFLOW_EDITIONS {
1786 let edition = Edition::parse(retained)
1787 .unwrap_or_else(|err| panic!("retained edition {retained:?} has no arm: {err}"));
1788 assert!(is_retained_edition(edition));
1789 }
1790 }
1791
1792 #[test]
1793 fn session_floor_picks_highest_all_three_support() {
1794 // Highest edition all three share wins (ranked by edition_sort_key);
1795 // whitespace is trimmed so a padded edition still matches. When all three
1796 // reach the same top edition, the runtime is not limiting.
1797 let env = SessionOffer::new(&["2026.01", " 2026.06 "]);
1798 let model = SessionOffer::new(&["2026.06", "2026.01"]);
1799 let runtime = SessionOffer::new(&["2026.06"]);
1800 let floor = negotiate_session_floor(&env, &model, &runtime).expect("a floor");
1801 assert_eq!(floor.selected_workflow_edition, "2026.06");
1802 assert_eq!(floor.desired_workflow_edition, "2026.06");
1803 assert!(!floor.runtime_limited());
1804 }
1805
1806 #[test]
1807 fn session_floor_flags_runtime_as_limiting_tier() {
1808 // env+model both reach 2026.08, but the runtime only speaks 2026.06, so the
1809 // floor drops to 2026.06 (safe — all three speak it) and the runtime is
1810 // flagged as the tier holding the session back.
1811 let env = SessionOffer::new(&["2026.06", "2026.08"]);
1812 let model = SessionOffer::new(&["2026.06", "2026.08"]);
1813 let runtime = SessionOffer::new(&["2026.06"]);
1814 let floor = negotiate_session_floor(&env, &model, &runtime).expect("a floor");
1815 assert_eq!(floor.selected_workflow_edition, "2026.06");
1816 assert_eq!(floor.desired_workflow_edition, "2026.08");
1817 assert!(floor.runtime_limited());
1818 }
1819
1820 #[test]
1821 fn session_floor_is_none_when_no_common_edition() {
1822 // env+model agree on 2026.08 but the runtime can't speak it, and they can't
1823 // speak the runtime's 2026.06 → no edition all three share → None.
1824 let env = SessionOffer::new(&["2026.08"]);
1825 let model = SessionOffer::new(&["2026.08"]);
1826 let runtime = SessionOffer::new(&["2026.06"]);
1827 assert!(negotiate_session_floor(&env, &model, &runtime).is_err());
1828
1829 // Empty strings never match, so an offer of only "" has no mutual value.
1830 let empty = SessionOffer::new(&[""]);
1831 let ok = SessionOffer::new(&["2026.06"]);
1832 assert!(negotiate_session_floor(&empty, &ok, &ok).is_err());
1833 }
1834
1835 #[test]
1836 fn edition_ordering_prefers_exact_cohort_then_newer_date() {
1837 use super::edition_sort_key;
1838
1839 // Exact moving cohorts beat their own sealed fallback. This lets two
1840 // matching prerelease/dev peers use the newest cohort while still allowing
1841 // fallback to the sealed edition when the moving cohorts differ.
1842 assert!(edition_sort_key("2026.06-0.1.0-rc.1") > edition_sort_key("2026.06"));
1843
1844 // newer-date-wins: a newer date outranks an older one regardless of
1845 // cohort status.
1846 assert!(edition_sort_key("2026.09-0.2.0-beta.1") > edition_sort_key("2026.06"));
1847 assert!(edition_sort_key("2026.09") > edition_sort_key("2026.06-0.1.0-rc.1"));
1848
1849 // deterministic suffix tiebreak: two same-date cohorts order by
1850 // their full suffix, never by iteration order, so two honest builds
1851 // never disagree on the winner.
1852 assert!(edition_sort_key("2026.06-0.1.0-rc.2") > edition_sort_key("2026.06-0.1.0-rc.1"));
1853
1854 // negotiate_workflow_edition applies the same key: offered against a
1855 // hypothetical multi-edition supported set, the highest by key wins. With
1856 // the single supported edition this build ships, the current edition is
1857 // selected when offered alongside older/newer noise.
1858 assert_eq!(
1859 negotiate_workflow_edition(
1860 &SessionOffer::new(&["2025.01", CURRENT_WORKFLOW_EDITION, "2099.12"]),
1861 &SessionOffer::this_build(None)
1862 ),
1863 Ok(CURRENT_WORKFLOW_EDITION.to_string())
1864 );
1865 }
1866
1867 #[test]
1868 fn session_floor_prefers_exact_edition_cohort_over_sealed_fallback() {
1869 // The floor uses edition_sort_key: when all three offer the exact moving
1870 // cohort and its sealed fallback, the exact cohort is selected.
1871 let editions = &["2026.06", "2026.06-0.1.0-rc.1"];
1872 let offer = SessionOffer::new(editions);
1873 let floor = negotiate_session_floor(&offer, &offer, &offer).expect("a floor");
1874 assert_eq!(floor.selected_workflow_edition, "2026.06-0.1.0-rc.1");
1875 }
1876
1877 /// The WANT/CAN rule, end to end: two- and three-party, declared and not.
1878 ///
1879 /// Each case is `(name, [(tier, CAN, WANT)], expected selection)`; `None`
1880 /// expects a refusal that names every tier. Plain strings throughout —
1881 /// negotiation is string-level, and `Edition` typing happens at the boundary
1882 /// the selected name is then parsed through.
1883 #[test]
1884 fn want_can_selection_table() {
1885 type Tier<'a> = (&'a str, &'a [&'a str], Option<&'a str>);
1886 let cases: &[(&str, &[Tier<'_>], Option<&str>)] = &[
1887 // --- nothing declared: exactly the pre-WANT highest-mutual rule ---
1888 (
1889 "two-party, undeclared: highest mutual",
1890 &[
1891 ("env", &["2026.01", "2026.06"], None),
1892 ("runtime", &["2026.06"], None),
1893 ],
1894 Some("2026.06"),
1895 ),
1896 (
1897 "three-party, undeclared: highest all three share",
1898 &[
1899 ("env", &["2026.01", " 2026.06 "], None),
1900 ("model", &["2026.06", "2026.01"], None),
1901 ("runtime", &["2026.06"], None),
1902 ],
1903 Some("2026.06"),
1904 ),
1905 (
1906 "three-party, undeclared: the runtime's CAN set caps the floor",
1907 &[
1908 ("env", &["2026.06", "2026.08"], None),
1909 ("model", &["2026.06", "2026.08"], None),
1910 ("runtime", &["2026.06"], None),
1911 ],
1912 Some("2026.06"),
1913 ),
1914 (
1915 "three-party, undeclared: empty intersection refuses",
1916 &[
1917 ("env", &["2026.08"], None),
1918 ("model", &["2026.08"], None),
1919 ("runtime", &["2026.06"], None),
1920 ],
1921 None,
1922 ),
1923 // --- a declared WANT is a ceiling ---
1924 (
1925 "one peer declares below the mutual max: the declared one wins",
1926 &[
1927 ("env", &["2026.06", "2026.08"], Some("2026.06")),
1928 ("model", &["2026.06", "2026.08"], None),
1929 ("runtime", &["2026.06", "2026.08"], None),
1930 ],
1931 Some("2026.06"),
1932 ),
1933 (
1934 "a WANT above what another peer CAN never lifts the floor",
1935 &[
1936 ("env", &["2026.06", "2026.08", "2026.10"], Some("2026.10")),
1937 ("model", &["2026.06", "2026.08"], None),
1938 ("runtime", &["2026.06", "2026.08", "2026.10"], None),
1939 ],
1940 Some("2026.08"),
1941 ),
1942 (
1943 "a WANT the intersection does not contain still selects the \
1944 highest mutual below it: a pin is a ceiling, not an exact demand",
1945 &[
1946 ("env", &["2026.06", "2026.08"], Some("2026.07")),
1947 ("model", &["2026.06", "2026.08"], None),
1948 ("runtime", &["2026.06", "2026.08"], None),
1949 ],
1950 Some("2026.06"),
1951 ),
1952 (
1953 "a WANT below everything mutual refuses rather than running higher",
1954 &[
1955 ("env", &["2026.06"], Some("2025.01")),
1956 ("model", &["2026.06"], None),
1957 ("runtime", &["2026.06"], None),
1958 ],
1959 None,
1960 ),
1961 (
1962 "the lowest WANT wins when several are declared",
1963 &[
1964 ("env", &["2026.06", "2026.08", "2026.10"], Some("2026.10")),
1965 ("model", &["2026.06", "2026.08", "2026.10"], Some("2026.08")),
1966 (
1967 "runtime",
1968 &["2026.06", "2026.08", "2026.10"],
1969 Some("2026.10"),
1970 ),
1971 ],
1972 Some("2026.08"),
1973 ),
1974 // --- dev cohorts vs their sealed base ---
1975 (
1976 "undeclared, matching dev cohorts: the exact cohort beats its base",
1977 &[
1978 ("env", &["2026.06", "2026.06-dev.aaa"], None),
1979 ("model", &["2026.06", "2026.06-dev.aaa"], None),
1980 ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1981 ],
1982 Some("2026.06-dev.aaa"),
1983 ),
1984 (
1985 "differing dev cohorts do not match: both fall back to the sealed base",
1986 &[
1987 ("env", &["2026.06", "2026.06-dev.aaa"], None),
1988 ("model", &["2026.06", "2026.06-dev.bbb"], None),
1989 ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1990 ],
1991 Some("2026.06"),
1992 ),
1993 (
1994 "a WANT of the bare base admits every cohort of it: the exact cohort still wins",
1995 &[
1996 ("env", &["2026.06", "2026.06-dev.aaa"], Some("2026.06")),
1997 ("model", &["2026.06", "2026.06-dev.aaa"], None),
1998 ("runtime", &["2026.06", "2026.06-dev.aaa"], None),
1999 ],
2000 Some("2026.06-dev.aaa"),
2001 ),
2002 // --- a bare-base WANT against cohort-only builds (the stateVersion value) ---
2003 (
2004 "bare base WANT on both sides, cohort-only CANs: selects the cohort",
2005 &[
2006 ("env", &["2026.06-dev.aaa"], Some("2026.06")),
2007 ("runtime", &["2026.06-dev.aaa"], Some("2026.06")),
2008 ],
2009 Some("2026.06-dev.aaa"),
2010 ),
2011 (
2012 "bare base WANT on one side, three cohort-only tiers: selects the cohort",
2013 &[
2014 ("env", &["2026.06-dev.aaa"], Some("2026.06")),
2015 ("model", &["2026.06-dev.aaa"], None),
2016 ("runtime", &["2026.06-dev.aaa"], None),
2017 ],
2018 Some("2026.06-dev.aaa"),
2019 ),
2020 (
2021 "bare base WANT admits its own cohorts but not a newer base's",
2022 &[
2023 (
2024 "env",
2025 &["2026.06-dev.aaa", "2026.08-dev.aaa"],
2026 Some("2026.06"),
2027 ),
2028 ("runtime", &["2026.06-dev.aaa", "2026.08-dev.aaa"], None),
2029 ],
2030 Some("2026.06-dev.aaa"),
2031 ),
2032 (
2033 "bare base WANT against a newer base only refuses",
2034 &[
2035 ("env", &["2026.08"], Some("2026.06")),
2036 ("model", &["2026.08"], None),
2037 ("runtime", &["2026.08", "2026.08-dev.aaa"], None),
2038 ],
2039 None,
2040 ),
2041 (
2042 "cohort WANT against a differing cohort of the same base refuses",
2043 &[
2044 ("env", &["2026.06-dev.aaa"], Some("2026.06-dev.aaa")),
2045 ("runtime", &["2026.06-dev.bbb"], None),
2046 ],
2047 None,
2048 ),
2049 (
2050 "cohort WANT keeps the exact order: a higher cohort in the intersection is excluded",
2051 &[
2052 ("env", &["2026.06-dev.bbb"], Some("2026.06-dev.aaa")),
2053 ("runtime", &["2026.06-dev.bbb"], None),
2054 ],
2055 None,
2056 ),
2057 // --- a 0.1.0-shaped peer against a build that retains more ---
2058 (
2059 "a 0.1.0-shaped peer (one CAN, no WANT) pins a newer build to 2026.06",
2060 &[
2061 ("env", &["2026.06"], None),
2062 ("runtime", &["2026.06", "2099.01"], None),
2063 ],
2064 Some("2026.06"),
2065 ),
2066 (
2067 "the same peer against a runtime that declares the newer edition",
2068 &[
2069 ("env", &["2026.06"], None),
2070 ("model", &["2026.06"], None),
2071 ("runtime", &["2026.06", "2099.01"], Some("2099.01")),
2072 ],
2073 Some("2026.06"),
2074 ),
2075 // --- degenerate sets ---
2076 (
2077 "a tier that CAN nothing refuses",
2078 &[("env", &[], None), ("runtime", &["2026.06"], None)],
2079 None,
2080 ),
2081 ];
2082
2083 for (name, tiers, expected) in cases {
2084 let offers: Vec<(&str, SessionOffer)> = tiers
2085 .iter()
2086 .map(|(tier_name, can, want)| (*tier_name, tier(can, *want)))
2087 .collect();
2088 let borrowed: Vec<(&str, &SessionOffer)> = offers
2089 .iter()
2090 .map(|(tier_name, o)| (*tier_name, o))
2091 .collect();
2092 let selected = super::select_workflow_edition(&borrowed);
2093 match expected {
2094 Some(edition) => assert_eq!(selected.as_deref(), Ok(*edition), "{name}"),
2095 None => {
2096 let refusal = selected.expect_err(name).to_string();
2097 for (tier_name, can, _) in tiers.iter() {
2098 assert!(refusal.contains(tier_name), "{name}: {refusal}");
2099 for edition in can.iter() {
2100 assert!(refusal.contains(edition), "{name}: {refusal}");
2101 }
2102 }
2103 }
2104 }
2105 }
2106 }
2107
2108 /// With nothing declared anywhere, selection is bit-for-bit the pre-WANT
2109 /// rule: the WANT ceiling is `min(max(can))`, and every member of the
2110 /// intersection is already <= each tier's own max, so the filter is vacuous.
2111 ///
2112 /// `highest_mutual` below is the function this unit replaced, copied
2113 /// verbatim from `git show HEAD:crates/rlmesh-proto/src/lib.rs` (minus its
2114 /// generic `key`, which every caller instantiated with `edition_sort_key`).
2115 #[test]
2116 fn undeclared_everywhere_is_the_old_highest_mutual() {
2117 fn highest_mutual(sets: &[&[String]]) -> Option<String> {
2118 let (first, rest) = sets.split_first()?;
2119 first
2120 .iter()
2121 .map(|value| value.trim())
2122 .filter(|value| !value.is_empty())
2123 .filter(|value| {
2124 rest.iter()
2125 .all(|set| set.iter().any(|other| other.trim() == *value))
2126 })
2127 .max_by_key(|value| super::edition_sort_key(value))
2128 .map(|value| value.to_string())
2129 }
2130
2131 let shapes: &[&[&[&str]]] = &[
2132 &[&["2026.06"], &["2026.06"]],
2133 &[&["2026.01", " 2026.06 "], &["2026.06", "2026.01"]],
2134 &[&["2025.01", "2026.06", "2031.12"], &["2026.06"]],
2135 &[&["2026.11", "2027.01"], &["2026.06"]],
2136 &[&[""], &["2026.06"]],
2137 &[&[], &["2026.06"]],
2138 &[
2139 &["2026.06", "2026.08"],
2140 &["2026.06", "2026.08"],
2141 &["2026.06"],
2142 ],
2143 &[&["2026.08"], &["2026.08"], &["2026.06"]],
2144 &[
2145 &["2026.06", "2026.06-0.1.0-rc.1"],
2146 &["2026.06", "2026.06-0.1.0-rc.1"],
2147 &["2026.06", "2026.06-0.1.0-rc.1"],
2148 ],
2149 &[
2150 &["2026.06", "2026.06-dev.aaa"],
2151 &["2026.06", "2026.06-dev.bbb"],
2152 &["2026.06", "2026.06-dev.aaa"],
2153 ],
2154 &[&["2026.06"], &["2026.06", "2099.01"]],
2155 ];
2156
2157 for shape in shapes {
2158 let offers: Vec<SessionOffer> = shape.iter().map(|can| tier(can, None)).collect();
2159 let owned: Vec<Vec<String>> = shape.iter().map(|can| offer(can)).collect();
2160 let sets: Vec<&[String]> = owned.iter().map(Vec::as_slice).collect();
2161 let borrowed: Vec<(&str, &SessionOffer)> = offers.iter().map(|o| ("tier", o)).collect();
2162 assert_eq!(
2163 super::select_workflow_edition(&borrowed).ok(),
2164 highest_mutual(&sets),
2165 "{shape:?}"
2166 );
2167 }
2168 }
2169
2170 /// The runtime holding the floor down is diagnosed by cause (plan E.3): a
2171 /// CAN-set limit is a defect the operator can fix by upgrading; a declared
2172 /// WANT is the sticky model working as intended.
2173 #[test]
2174 fn session_floor_reports_why_the_runtime_capped_the_session() {
2175 use super::RuntimeCap;
2176
2177 // The runtime cannot drive 2026.08 at all.
2178 let peers = tier(&["2026.06", "2026.08"], None);
2179 let floor =
2180 negotiate_session_floor(&peers, &peers, &tier(&["2026.06"], None)).expect("a floor");
2181 assert_eq!(floor.runtime_cap, Some(RuntimeCap::Capability));
2182
2183 // The runtime can drive 2026.08 and declares 2026.06 instead.
2184 let runtime = tier(&["2026.06", "2026.08"], Some("2026.06"));
2185 let floor = negotiate_session_floor(&peers, &peers, &runtime).expect("a floor");
2186 assert_eq!(floor.selected_workflow_edition, "2026.06");
2187 assert_eq!(floor.desired_workflow_edition, "2026.08");
2188 assert_eq!(floor.runtime_cap, Some(RuntimeCap::Declaration));
2189
2190 // Both older AND declared below its own max: the declaration is what
2191 // pins the session (undeclared it would have run at 2026.08), so this is
2192 // not an upgrade-the-runtime diagnostic even though 2026.10 is out of
2193 // the runtime's reach entirely.
2194 let wide = tier(&["2026.06", "2026.08", "2026.10"], None);
2195 let runtime = tier(&["2026.06", "2026.08"], Some("2026.06"));
2196 let floor = negotiate_session_floor(&wide, &wide, &runtime).expect("a floor");
2197 assert_eq!(floor.selected_workflow_edition, "2026.06");
2198 assert_eq!(floor.desired_workflow_edition, "2026.10");
2199 assert_eq!(floor.runtime_cap, Some(RuntimeCap::Declaration));
2200
2201 // A peer, not the runtime, holding the floor down is not a runtime cap.
2202 let pinned = tier(&["2026.06", "2026.08"], Some("2026.06"));
2203 let floor = negotiate_session_floor(&pinned, &peers, &peers).expect("a floor");
2204 assert_eq!(floor.selected_workflow_edition, "2026.06");
2205 assert_eq!(floor.desired_workflow_edition, "2026.06");
2206 assert_eq!(floor.runtime_cap, None);
2207 assert!(!floor.runtime_limited());
2208 }
2209
2210 /// An undeclared participant declares this build's current edition, which is
2211 /// `max(can)` for this build — so this build's own offer never caps a peer.
2212 #[test]
2213 fn this_build_declares_its_current_edition_by_default() {
2214 use super::{core_handshake_request, declared_workflow_edition, edition_sort_key};
2215
2216 // The no-op argument rests on this: the default WANT is `max(can)`, so a
2217 // retained edition sorting above the current one would make this build's
2218 // own offer cap its peers.
2219 assert_eq!(
2220 SUPPORTED_WORKFLOW_EDITIONS
2221 .iter()
2222 .max_by_key(|edition| edition_sort_key(edition))
2223 .copied(),
2224 Some(CURRENT_WORKFLOW_EDITION)
2225 );
2226
2227 assert_eq!(declared_workflow_edition(None), CURRENT_WORKFLOW_EDITION);
2228 assert_eq!(
2229 declared_workflow_edition(Some(" ")),
2230 CURRENT_WORKFLOW_EDITION
2231 );
2232 assert_eq!(declared_workflow_edition(Some(" 2026.06 ")), "2026.06");
2233
2234 let offer = SessionOffer::this_build(None);
2235 assert_eq!(offer.editions, supported_workflow_editions());
2236 assert_eq!(
2237 offer.preferred.as_deref(),
2238 Some(CURRENT_WORKFLOW_EDITION),
2239 "an undeclared build wants its current edition"
2240 );
2241
2242 let request = core_handshake_request("rlmesh-env", &[], None);
2243 assert_eq!(request.preferred_workflow_edition, CURRENT_WORKFLOW_EDITION);
2244 assert_eq!(
2245 core_handshake_request("rlmesh-env", &[], Some("2026.06")).preferred_workflow_edition,
2246 "2026.06"
2247 );
2248 }
2249}