zeroclaw_api/session_keys.rs
1//! Session key normalization shared across infra and memory backends.
2//!
3//! Channel orchestration uses two identifiers derived from a `ChannelMessage`:
4//! one ends up as a JSONL filename (via `SessionStore::session_path`) and as
5//! an in-memory HashMap key for the conversation history cache, while the
6//! same identifier is also passed to `Memory::store`/`Memory::recall` as the
7//! `session_id` filter. Because filesystem-safe sanitization is applied when
8//! writing the JSONL file, every other layer must use the same sanitized form
9//! to keep lookups consistent across daemon restarts and persisted backends.
10
11/// Replace every character outside `[A-Za-z0-9_-]` with `_`. Idempotent.
12///
13/// Callers building session keys must pre-apply this so the runtime HashMap
14/// key, the on-disk JSONL filename, and the `session_id` column in memory
15/// backends all agree.
16pub fn sanitize_session_key(key: &str) -> String {
17 key.chars()
18 .map(|c| {
19 if c.is_alphanumeric() || c == '_' || c == '-' {
20 c
21 } else {
22 '_'
23 }
24 })
25 .collect()
26}
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 #[test]
33 fn replaces_special_characters_with_underscore() {
34 assert_eq!(
35 sanitize_session_key("slack_C123_1.2_user one"),
36 "slack_C123_1_2_user_one"
37 );
38 }
39
40 #[test]
41 fn preserves_alphanumeric_underscore_and_hyphen() {
42 let key = "abc-DEF_123";
43 assert_eq!(sanitize_session_key(key), key);
44 }
45
46 #[test]
47 fn is_idempotent() {
48 let once = sanitize_session_key("whatsapp_123@g.us_alice");
49 let twice = sanitize_session_key(&once);
50 assert_eq!(once, twice);
51 }
52
53 #[test]
54 fn handles_empty_string() {
55 assert_eq!(sanitize_session_key(""), "");
56 }
57
58 #[test]
59 fn preserves_unicode_alphanumeric() {
60 // is_alphanumeric() treats unicode letters/digits as alphanumeric.
61 assert_eq!(sanitize_session_key("user_Алиса"), "user_Алиса");
62 }
63}