1use 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#[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#[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
65pub(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
105pub 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
122 let derived_key = format!(
123 "derived-{}-{}",
124 sanitize_component(&canonical_agent),
125 sanitize_component(&fallback_scope)
126 );
127
128 if derived_key.len() <= 128 {
129 derived_key
130 } else {
131 let inline_hash = |input: &str| -> u64 {
133 let mut h: u64 = 0;
134 for chunk in input.as_bytes().chunks(8) {
135 let mut buf = [0u8; 8];
136 buf[..chunk.len()].copy_from_slice(chunk);
137 h = h
138 .wrapping_mul(0x517cc1b727220a95)
139 .wrapping_add(u64::from_le_bytes(buf));
140 }
141 h
142 };
143
144 let path_hash = inline_hash(&sanitize_component(&fallback_scope));
145 let agent = sanitize_component(&canonical_agent);
146 if 25 + agent.len() <= 128 {
148 format!("derived-{}-{:016x}", agent, path_hash)
149 } else {
150 let agent_hash = inline_hash(&agent);
152 format!("derived-{:016x}-{:016x}", agent_hash, path_hash)
153 }
154 }
155}
156
157pub(crate) fn sanitize_component(value: &str) -> String {
158 value
159 .chars()
160 .map(|ch| {
161 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
162 ch
163 } else {
164 '_'
165 }
166 })
167 .collect()
168}
169
170pub(crate) fn runtime_reason_label(reason: RuntimeShutdownReason) -> &'static str {
171 match reason {
172 RuntimeShutdownReason::SessionEnded => "session-ended",
173 RuntimeShutdownReason::IdleTimeout => "idle-timeout",
174 RuntimeShutdownReason::Manual => "manual",
175 }
176}
177
178pub(crate) async fn namespace_id_for(
179 agent_type: &str,
180 storage: &StorageManager,
181) -> Result<i64, AgentError> {
182 let canonical = nexus_core::canonicalize_agent_type(agent_type);
183 let namespace_repo = NamespaceRepository::new(storage.pool().clone());
184 namespace_repo
185 .get_or_create(&canonical, &canonical)
186 .await
187 .map(|namespace| namespace.id)
188 .map_err(|error| AgentError::Storage(error.to_string()))
189}
190
191pub(crate) async fn store_runtime_marker(
192 memory_repo: &MemoryRepository,
193 namespace_id: i64,
194 marker: RuntimeMarker<'_>,
195) -> Result<(), AgentError> {
196 let session_tag = derive_session_key(marker.agent_type, marker.session_key, marker.cwd);
197 let content = format!(
198 "Runtime {} for {} [session:{}] ({})",
199 marker.event.replace('_', " "),
200 marker.agent_type,
201 session_tag,
202 marker.detail
203 );
204 let metadata = json!({
205 "runtime": {
206 "event": marker.event,
207 "detail": marker.detail,
208 "session_key": marker.session_key,
209 "derived_session_key": session_tag,
210 "cwd": marker.cwd,
211 "agent_type": marker.agent_type,
212 "agent_namespace": marker.agent_namespace,
213 "captured_at": Utc::now(),
214 }
215 });
216 let mut cognitive = CognitiveMetadata::new(
217 CognitiveLevel::Explicit,
218 marker.agent_type,
219 marker.agent_type,
220 Some(session_tag.clone()),
221 "runtime_controller",
222 );
223 cognitive.confidence = Some(1.0);
224 let metadata = cognitive.merge_into(&metadata);
225
226 memory_repo
227 .store(StoreMemoryParams {
228 namespace_id,
229 content: &content,
230 category: &MemoryCategory::Session,
231 memory_lane_type: None,
232 labels: &[
233 "runtime".to_string(),
234 "session".to_string(),
235 marker.event.to_string(),
236 ],
237 metadata: &metadata,
238 embedding: None,
239 embedding_model: None,
240 })
241 .await
242 .map_err(|e| AgentError::Storage(e.to_string()))?;
243
244 Ok(())
245}