Skip to main content

nexus_memory_agent/
runtime_state.rs

1//! Runtime state persistence, session key derivation, and helper types.
2
3use std::path::{Path, PathBuf};
4
5use chrono::{DateTime, Utc};
6use nexus_core::{CognitiveLevel, CognitiveMetadata, MemoryCategory};
7use nexus_storage::repository::{MemoryRepository, NamespaceRepository, StoreMemoryParams};
8use nexus_storage::StorageManager;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11
12use crate::error::AgentError;
13
14// ── Public enums ──────────────────────────────────────────────────────
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum RuntimeMode {
18    SessionScoped,
19    Persistent,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RuntimeShutdownReason {
24    SessionEnded,
25    IdleTimeout,
26    Manual,
27}
28
29// ── Internal serialisation types ──────────────────────────────────────
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub(crate) struct RuntimeState {
33    pub agent_type: String,
34    pub session_key: String,
35    pub mode: RuntimeModeSerde,
36    pub started_at: DateTime<Utc>,
37    pub updated_at: DateTime<Utc>,
38}
39
40pub(crate) struct RuntimeMarker<'a> {
41    pub agent_type: &'a str,
42    pub session_key: Option<&'a str>,
43    pub cwd: Option<&'a str>,
44    pub event: &'a str,
45    pub detail: &'a str,
46    pub agent_namespace: &'a str,
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "snake_case")]
51pub(crate) enum RuntimeModeSerde {
52    SessionScoped,
53    Persistent,
54}
55
56impl From<RuntimeMode> for RuntimeModeSerde {
57    fn from(value: RuntimeMode) -> Self {
58        match value {
59            RuntimeMode::SessionScoped => Self::SessionScoped,
60            RuntimeMode::Persistent => Self::Persistent,
61        }
62    }
63}
64
65// ── State persistence helpers ─────────────────────────────────────────
66
67pub(crate) fn state_root() -> PathBuf {
68    if let Some(dir) = dirs::state_dir() {
69        dir.join("nexus-memory-system").join("runtime")
70    } else {
71        std::env::var("HOME")
72            .map(|h| PathBuf::from(h).join(".local/state/nexus-memory-system/runtime"))
73            .unwrap_or_else(|_| PathBuf::from(".nexus-runtime"))
74    }
75}
76
77pub(crate) fn state_file_path(agent_type: &str, session_key: &str) -> Result<PathBuf, AgentError> {
78    let root = state_root().join("sessions");
79    std::fs::create_dir_all(&root)?;
80    Ok(root.join(format!(
81        "{}__{}.json",
82        sanitize_component(agent_type),
83        sanitize_component(session_key)
84    )))
85}
86
87pub(crate) fn read_runtime_state(path: &Path) -> Result<Option<RuntimeState>, AgentError> {
88    if !path.exists() {
89        return Ok(None);
90    }
91
92    let contents = std::fs::read_to_string(path)?;
93    let state =
94        serde_json::from_str(&contents).map_err(|e| AgentError::Supervisor(e.to_string()))?;
95    Ok(Some(state))
96}
97
98pub(crate) fn write_runtime_state(path: &Path, state: &RuntimeState) -> Result<(), AgentError> {
99    let contents =
100        serde_json::to_string_pretty(state).map_err(|e| AgentError::Supervisor(e.to_string()))?;
101    std::fs::write(path, contents)?;
102    Ok(())
103}
104
105// ── Session key derivation ────────────────────────────────────────────
106
107pub fn derive_session_key(
108    agent_type: &str,
109    session_key: Option<&str>,
110    cwd: Option<&str>,
111) -> String {
112    if let Some(value) = session_key.filter(|value| !value.trim().is_empty()) {
113        return value.to_string();
114    }
115
116    let canonical_agent = nexus_core::canonicalize_agent_type(agent_type);
117    let fallback_scope = cwd
118        .filter(|value| !value.trim().is_empty())
119        .map(nexus_core::normalize_project_path)
120        .unwrap_or_else(|| "unknown-cwd".to_string());
121    format!(
122        "derived-{}-{}",
123        sanitize_component(&canonical_agent),
124        sanitize_component(&fallback_scope)
125    )
126}
127
128pub(crate) fn sanitize_component(value: &str) -> String {
129    value
130        .chars()
131        .map(|ch| {
132            if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
133                ch
134            } else {
135                '_'
136            }
137        })
138        .collect()
139}
140
141pub(crate) fn runtime_reason_label(reason: RuntimeShutdownReason) -> &'static str {
142    match reason {
143        RuntimeShutdownReason::SessionEnded => "session-ended",
144        RuntimeShutdownReason::IdleTimeout => "idle-timeout",
145        RuntimeShutdownReason::Manual => "manual",
146    }
147}
148
149pub(crate) async fn namespace_id_for(
150    agent_type: &str,
151    storage: &StorageManager,
152) -> Result<i64, AgentError> {
153    let canonical = nexus_core::canonicalize_agent_type(agent_type);
154    let namespace_repo = NamespaceRepository::new(storage.pool().clone());
155    namespace_repo
156        .get_or_create(&canonical, &canonical)
157        .await
158        .map(|namespace| namespace.id)
159        .map_err(|error| AgentError::Storage(error.to_string()))
160}
161
162pub(crate) async fn store_runtime_marker(
163    memory_repo: &MemoryRepository,
164    namespace_id: i64,
165    marker: RuntimeMarker<'_>,
166) -> Result<(), AgentError> {
167    let session_tag = derive_session_key(marker.agent_type, marker.session_key, marker.cwd);
168    let content = format!(
169        "Runtime {} for {} [session:{}] ({})",
170        marker.event.replace('_', " "),
171        marker.agent_type,
172        session_tag,
173        marker.detail
174    );
175    let metadata = json!({
176        "runtime": {
177            "event": marker.event,
178            "detail": marker.detail,
179            "session_key": marker.session_key,
180            "derived_session_key": session_tag,
181            "cwd": marker.cwd,
182            "agent_type": marker.agent_type,
183            "agent_namespace": marker.agent_namespace,
184            "captured_at": Utc::now(),
185        }
186    });
187    let mut cognitive = CognitiveMetadata::new(
188        CognitiveLevel::Explicit,
189        marker.agent_type,
190        marker.agent_type,
191        Some(session_tag.clone()),
192        "runtime_controller",
193    );
194    cognitive.confidence = Some(1.0);
195    let metadata = cognitive.merge_into(&metadata);
196
197    memory_repo
198        .store(StoreMemoryParams {
199            namespace_id,
200            content: &content,
201            category: &MemoryCategory::Session,
202            memory_lane_type: None,
203            labels: &[
204                "runtime".to_string(),
205                "session".to_string(),
206                marker.event.to_string(),
207            ],
208            metadata: &metadata,
209            embedding: None,
210            embedding_model: None,
211        })
212        .await
213        .map_err(|e| AgentError::Storage(e.to_string()))?;
214
215    Ok(())
216}