Skip to main content

cortexfs_channels/
route.rs

1use crate::{ChannelError, InboundMessage, MessageTarget};
2
3/// Deterministic mapping from a remote conversation to an agent session name.
4#[derive(Clone, Debug, Eq, PartialEq)]
5pub struct ChannelSessionRoute {
6    agent: String,
7    prefix: String,
8}
9
10impl ChannelSessionRoute {
11    pub fn new(agent: impl Into<String>, prefix: impl Into<String>) -> Result<Self, ChannelError> {
12        let agent = agent.into();
13        let prefix = prefix.into();
14        if !valid_name(&agent) || !valid_name(&prefix) {
15            return Err(ChannelError::InvalidValue(format!("{agent}/{prefix}")));
16        }
17        Ok(Self { agent, prefix })
18    }
19
20    #[must_use]
21    pub fn agent(&self) -> &str {
22        &self.agent
23    }
24
25    /// Returns a stable, filesystem-safe session name for one conversation thread.
26    #[must_use]
27    pub fn session_for(&self, target: &MessageTarget) -> String {
28        let key = conversation_key(target);
29        format!("{}-{:016x}", self.prefix, fnv1a(key.as_bytes()))
30    }
31
32    /// Returns a deterministic idempotency key for one inbound platform message.
33    #[must_use]
34    pub fn request_id_for(&self, message: &InboundMessage) -> String {
35        let mut key = conversation_key(&message.target);
36        key.push('\0');
37        key.push_str(&message.id);
38        format!("im-{}-{:016x}", self.prefix, fnv1a(key.as_bytes()))
39    }
40}
41
42fn conversation_key(target: &MessageTarget) -> String {
43    let mut key = String::with_capacity(target.conversation.as_str().len() + 64);
44    key.push_str(target.channel.as_str());
45    key.push('\0');
46    key.push_str(target.conversation.as_str());
47    if let Some(thread) = target.thread.as_deref() {
48        key.push('\0');
49        key.push_str(thread);
50    }
51    key
52}
53
54fn valid_name(value: &str) -> bool {
55    !value.is_empty()
56        && value.len() <= 64
57        && value
58            .bytes()
59            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
60}
61
62fn fnv1a(bytes: &[u8]) -> u64 {
63    bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
64        (hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
65    })
66}