diff --git a/crates/maestro-protocol/src/lib.rs b/crates/maestro-protocol/src/lib.rs index 80f3e79..390c245 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -163,6 +163,13 @@ impl<'de> Deserialize<'de> for ActivityState { /// for a fixed TTL, then resumes inference. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ReportActivityRequest { + /// Parsed strictly, unlike [`ActivityState`]'s own lenient `Deserialize` + /// (#110). Leniency exists so a client reading a *newer* daemon's snapshot + /// degrades an unrecognised state to `Idle` instead of dropping the whole + /// session. A report travelling the other way has no such excuse: an agent + /// posting a state this daemon does not know is a bug in the agent, and + /// silently recording it as `Idle` would hide that behind a 204. + #[serde(deserialize_with = "strict_activity_state")] pub state: ActivityState, /// Short human string shown next to the state (e.g. an error message). /// The daemon caps its length server-side. @@ -170,6 +177,26 @@ pub struct ReportActivityRequest { pub detail: Option, } +/// Reject an unknown [`ActivityState`] spelling instead of degrading it to +/// `Idle`. Used only by [`ReportActivityRequest`]; see the note there. +fn strict_activity_state<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error as _; + let s = String::deserialize(deserializer)?; + match s.as_str() { + "Active" => Ok(ActivityState::Active), + "Idle" => Ok(ActivityState::Idle), + "AwaitingInput" => Ok(ActivityState::AwaitingInput), + "Error" => Ok(ActivityState::Error), + other => Err(D::Error::unknown_variant( + other, + &["Active", "Idle", "AwaitingInput", "Error"], + )), + } +} + /// Optional worktree request attached to a `CreateSessionRequest`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct WorktreeSpec { diff --git a/docs/SIDEBAR.md b/docs/SIDEBAR.md index cab0444..f1d5a23 100644 --- a/docs/SIDEBAR.md +++ b/docs/SIDEBAR.md @@ -108,3 +108,9 @@ If a client receives an `ActivityState` variant it does not know, it must not fail rendering: treat it as `Idle` (neutral/dim, line-2 rules apply as usual) until the daemon sends a variant it does know. Rust clients deserialize activity leniently for the same reason. + +Leniency runs one way only. It covers a client *reading* a newer daemon, where +dropping the whole session would be worse than dimming one card. A report +arriving at `POST /api/sessions/:id/activity` is parsed strictly and an unknown +state is a 4xx: an agent posting a state the daemon does not know is a bug in +the agent, and answering 204 would hide it.