Skip to main content

nexus_memory_agent/
identity.rs

1//! Cross-session identity resolution.
2//!
3//! Provides [`IdentityResolver`] for discovering related namespaces and sessions
4//! that belong to the same canonical agent family or project, enabling
5//! cross-namespace recall without merging separate agent silos.
6
7use nexus_storage::repository::NamespaceRepository;
8use tracing::debug;
9
10/// Resolves cross-session identity by mapping agent type strings to canonical
11/// forms and discovering related namespaces.
12pub struct IdentityResolver;
13
14impl IdentityResolver {
15    /// Find all namespace IDs whose name matches the canonical form of the
16    /// given agent type.
17    ///
18    /// This catches cases where older sessions stored memories under an alias
19    /// (e.g. "claude") while newer sessions use the canonical name ("claude-code").
20    pub async fn related_namespace_ids(
21        namespace_repo: &NamespaceRepository,
22        agent_type: &str,
23    ) -> Vec<i64> {
24        let canonical = nexus_core::canonicalize_agent_type(agent_type);
25        let mut ids = Vec::new();
26
27        if let Ok(all) = namespace_repo.list_all().await {
28            for ns in &all {
29                let ns_canonical = nexus_core::canonicalize_agent_type(&ns.name);
30                if ns_canonical == canonical {
31                    ids.push(ns.id);
32                }
33            }
34        }
35
36        debug!(
37            canonical_agent = %canonical,
38            related_count = ids.len(),
39            "Resolved related namespace IDs"
40        );
41        ids
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    #[test]
48    fn test_related_namespace_ids_uses_canonical_resolution() {
49        // Pure logic test — canonicalize produces consistent keys.
50        assert_eq!(
51            nexus_core::canonicalize_agent_type("claude"),
52            nexus_core::canonicalize_agent_type("claude-code")
53        );
54        assert_eq!(
55            nexus_core::canonicalize_agent_type("pi"),
56            nexus_core::canonicalize_agent_type("pi-mono")
57        );
58        assert_eq!(
59            nexus_core::canonicalize_agent_type("omp"),
60            nexus_core::canonicalize_agent_type("oh-my-pi")
61        );
62    }
63
64    #[test]
65    fn test_canonicalize_roundtrip_stability() {
66        // Canonicalizing an already-canonical string is idempotent.
67        let inputs = [
68            "claude-code",
69            "pi-mono",
70            "oh-my-pi",
71            "gemini",
72            "codex",
73            "amp",
74            "generic",
75        ];
76        for input in &inputs {
77            let once = nexus_core::canonicalize_agent_type(input);
78            let twice = nexus_core::canonicalize_agent_type(&once);
79            assert_eq!(once, twice, "canonicalize({}) not idempotent", input);
80        }
81    }
82
83    #[test]
84    fn test_normalize_project_path_variants_resolve_to_same() {
85        assert_eq!(
86            nexus_core::normalize_project_path("/home/user/project"),
87            nexus_core::normalize_project_path("/home/user/project/")
88        );
89        assert_eq!(
90            nexus_core::normalize_project_path("/home/user/project"),
91            nexus_core::normalize_project_path("/home/user/project//")
92        );
93        assert_eq!(
94            nexus_core::normalize_project_path("/a/b/c"),
95            nexus_core::normalize_project_path("/a//b///c")
96        );
97    }
98}