Skip to main content

lattice_agent/log/
buffer_names.rs

1//! Canonical synthetic buffer name for the per-process AI log
2//! family (AI-1b).
3//!
4//! One flavour: `*ai:<provider>:<index>*` -- one buffer per AI
5//! agent session, keyed by [`SessionKey`]. Mirrors
6//! `lattice-lsp`'s per-instance `*lsp:<server>:<workspace>*`
7//! naming, one level simpler (no trace-variant sibling).
8//!
9//! The inverse helper ([`parse_ai_log_name`]) extracts the
10//! originating [`SessionKey`] from a synthetic name so
11//! `AiLogMode::on_activate` can derive its identity from its
12//! buffer's name alone -- no buffer-local seeding required.
13//!
14//! ## Invariant
15//!
16//! Provider names never contain `:` (`opencode`, `claude`,
17//! `gemini`). The parser splits on the first colon after `*ai:`;
18//! everything after that up to the trailing `*` is the index.
19
20use std::sync::Arc;
21
22use super::ai_log::SessionKey;
23
24/// Build the synthetic name for the per-session AI log buffer
25/// owned by [`super::modes::AiLogMode`].
26///
27/// Format: `*ai:<provider>:<index>*`.
28pub fn ai_log_name(session: &SessionKey) -> String {
29    format!("*ai:{}:{}*", session.provider, session.index)
30}
31
32/// Recover the [`SessionKey`] from a per-session log buffer
33/// name. Returns `None` when `name` does not match the
34/// canonical `*ai:<provider>:<index>*` shape.
35pub fn parse_ai_log_name(name: &str) -> Option<SessionKey> {
36    let body = name.strip_prefix("*ai:")?.strip_suffix('*')?;
37    if body.is_empty() {
38        return None;
39    }
40    let (provider, index_str) = body.split_once(':')?;
41    if provider.is_empty() {
42        return None;
43    }
44    let index = index_str.parse::<u32>().ok()?;
45    Some(SessionKey::new(Arc::<str>::from(provider), index))
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    fn key(provider: &str, index: u32) -> SessionKey {
53        SessionKey::new(Arc::<str>::from(provider), index)
54    }
55
56    #[test]
57    fn name_round_trips() {
58        let k = key("opencode", 1);
59        let name = ai_log_name(&k);
60        assert_eq!(name, "*ai:opencode:1*");
61        let parsed = parse_ai_log_name(&name).expect("parse");
62        assert_eq!(parsed, k);
63    }
64
65    #[test]
66    fn name_round_trips_second_index() {
67        let k = key("opencode", 2);
68        let name = ai_log_name(&k);
69        assert_eq!(name, "*ai:opencode:2*");
70        let parsed = parse_ai_log_name(&name).expect("parse");
71        assert_eq!(parsed, k);
72    }
73
74    #[test]
75    fn parser_rejects_garbage_names() {
76        assert!(parse_ai_log_name("*foo*").is_none());
77        assert!(parse_ai_log_name("*ai:*").is_none());
78        assert!(parse_ai_log_name("*ai::1*").is_none());
79        assert!(parse_ai_log_name("*ai:opencode:*").is_none());
80        assert!(parse_ai_log_name("*ai:opencode:x*").is_none());
81        assert!(parse_ai_log_name("not-ai").is_none());
82    }
83}