use std::fmt; use std::str::FromStr; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::ids::{AgentId, MessageId, SessionId}; /// The deep-linkable chat session key from spec ยง12: /// `agent:{agentId}-claw-{shard}:session:{sessionId}:{messageId}`. /// /// This codec is the single source of truth for the format; the frontend /// mirrors it in `frontend/src/lib/url/session-key.ts` and a contract test /// keeps the two in lockstep. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SessionKey { pub agent_id: AgentId, /// Deterministic hash bucket of the agent id, reserved for future /// partitioning. Carried verbatim through parse/format. pub shard: u16, pub session_id: SessionId, pub message_id: MessageId, } #[derive(Debug, thiserror::Error)] pub enum SessionKeyError { #[error("malformed session key: {0}")] Malformed(String), #[error("invalid shard: {0}")] InvalidShard(String), #[error("invalid id in session key: {0}")] InvalidId(#[from] uuid::Error), } impl fmt::Display for SessionKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "agent:{}-claw-{}:session:{}:{}", self.agent_id, self.shard, self.session_id, self.message_id ) } } impl FromStr for SessionKey { type Err = SessionKeyError; fn from_str(s: &str) -> Result { let malformed = || SessionKeyError::Malformed(s.to_owned()); let rest = s.strip_prefix("agent:").ok_or_else(malformed)?; let (agent_part, rest) = rest.split_once(":session:").ok_or_else(malformed)?; // `-claw-` cannot occur inside a UUID (hex digits and dashes only), // so the rightmost occurrence cleanly separates id from shard. let (agent_str, shard_str) = agent_part.rsplit_once("-claw-").ok_or_else(malformed)?; let agent_id = AgentId::from_str(agent_str)?; let shard: u16 = shard_str .parse() .map_err(|_| SessionKeyError::InvalidShard(shard_str.to_owned()))?; let mut tail = rest.split(':'); let session_str = tail.next().ok_or_else(malformed)?; let message_str = tail.next().ok_or_else(malformed)?; if tail.next().is_some() { return Err(malformed()); } Ok(SessionKey { agent_id, shard, session_id: SessionId::from_str(session_str)?, message_id: MessageId::from_str(message_str)?, }) } } impl Serialize for SessionKey { fn serialize(&self, serializer: S) -> Result { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for SessionKey { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; SessionKey::from_str(&s).map_err(D::Error::custom) } }