1use super::*;
2
3#[derive(Debug, serde::Deserialize, ts_rs::TS)]
5#[ts(export, export_to = "../../../frontend/types/rpc/")]
6pub struct MemoryListReq {
7 pub agent_id: String,
8}
9
10#[derive(Debug, serde::Deserialize, ts_rs::TS)]
12#[ts(export, export_to = "../../../frontend/types/rpc/")]
13pub struct MemoryReadReq {
14 pub agent_id: String,
15 pub filename: String,
16}
17
18#[derive(Debug, serde::Deserialize, ts_rs::TS)]
20#[ts(export, export_to = "../../../frontend/types/rpc/")]
21pub struct MemoryWriteReq {
22 pub agent_id: String,
23 pub filename: String,
24 pub content: String,
25}
26
27pub fn register(engine: &Arc<WshRpcEngine>, state: &AppState) {
28 register_memory_list(engine, state);
29 register_memory_read(engine, state);
30 register_memory_write(engine, state);
31}
32
33fn register_memory_list(engine: &Arc<WshRpcEngine>, state: &AppState) {
34 let state = state.clone();
35 engine.register_typed(COMMAND_MEMORY_LIST, move |req: MemoryListReq, ctx| {
36 let state = state.clone();
37 async move {
38 check_s1(&state.mstore, &ctx, &req.agent_id)?;
39 memory_list_impl(&state, &req.agent_id)
40 }
41 });
42}
43
44fn register_memory_read(engine: &Arc<WshRpcEngine>, state: &AppState) {
45 let state = state.clone();
46 engine.register_typed(COMMAND_MEMORY_READ, move |req: MemoryReadReq, ctx| {
47 let state = state.clone();
48 async move {
49 check_s1(&state.mstore, &ctx, &req.agent_id)?;
50 memory_read_impl(&state, &req.agent_id, &req.filename)
51 }
52 });
53}
54
55fn register_memory_write(engine: &Arc<WshRpcEngine>, state: &AppState) {
56 let state = state.clone();
57 engine.register_typed(COMMAND_MEMORY_WRITE, move |req: MemoryWriteReq, ctx| {
58 let state = state.clone();
59 async move {
60 check_s1(&state.mstore, &ctx, &req.agent_id)?;
61 memory_write_impl(&state, &req.agent_id, &req.filename, &req.content, None)
62 }
63 });
64}
65
66pub(crate) fn memory_list_impl<'o>(
67 state: &AppState,
68 owner: impl Into<SelfOwner<'o>>,
69) -> Result<NativeMemoryListResult, String> {
70 let owner = owner.into();
71 let memory_dir = owner.dir(&state.mstore).map_err(|e| format!("memory.list: {e}"))?;
72
73 let mut files: Vec<NativeMemoryFileMeta> = Vec::new();
74 let entries = match std::fs::read_dir(&memory_dir) {
75 Ok(e) => e,
76 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
77 return Ok(NativeMemoryListResult { files: Vec::new(), unverified: false });
78 }
79 Err(e) => return Err(format!("memory.list: read_dir: {e}")),
80 };
81 for entry in entries {
82 let entry = entry.map_err(|e| format!("memory.list: {e}"))?;
83 let name = entry.file_name().to_string_lossy().into_owned();
84 if !name.ends_with(".md") { continue; }
85 let file_type = entry.file_type()
86 .map_err(|e| format!("memory.list: file_type {name}: {e}"))?;
87 if !file_type.is_file() { continue; }
88 let meta = match entry.metadata() {
89 Ok(m) => m,
90 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
91 Err(e) => return Err(format!("memory.list: metadata {name}: {e}")),
92 };
93 let preview_content = {
94 use std::io::Read;
95 std::fs::File::open(entry.path())
96 .map(|f| {
97 let mut buf = Vec::with_capacity(512);
98 f.take(512).read_to_end(&mut buf).ok();
99 String::from_utf8_lossy(&buf).into_owned()
100 })
101 .unwrap_or_default()
102 };
103 files.push(NativeMemoryFileMeta {
104 is_index: name == "MEMORY.md",
105 metadata_type: crate::server::native_memory_handlers::parse_memory_frontmatter_type(&preview_content),
106 size_bytes: meta.len(),
107 modified_at: meta.modified().ok()
108 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
109 .map(|d| d.as_millis() as i64)
110 .unwrap_or(0),
111 filename: name,
112 });
113 }
114 files.sort_by(|a, b| b.is_index.cmp(&a.is_index).then(a.filename.cmp(&b.filename)));
115 Ok(NativeMemoryListResult { files, unverified: false })
116}
117
118pub(crate) fn memory_read_impl<'o>(
119 state: &AppState,
120 owner: impl Into<SelfOwner<'o>>,
121 filename: &str,
122) -> Result<NativeMemoryReadFileResult, String> {
123 let owner = owner.into();
124 crate::server::native_memory_handlers::validate_memory_filename(filename)
125 .map_err(|e| format!("memory.read: {e}"))?;
126 let path = owner.dir(&state.mstore).map_err(|e| format!("memory.read: {e}"))?.join(filename);
127
128 let file_type = std::fs::symlink_metadata(&path)
129 .map_err(|e| format!("memory.read: {filename}: {e}"))?.file_type();
130 if !file_type.is_file() {
131 return Err(format!("memory.read: {filename} is not a regular file"));
132 }
133 const MAX: u64 = 10 * 1024 * 1024;
134 let mut buf = Vec::new();
135 std::fs::File::open(&path)
136 .and_then(|f| { use std::io::Read; f.take(MAX).read_to_end(&mut buf) })
137 .map_err(|e| format!("memory.read: {filename}: {e}"))?;
138 let content = String::from_utf8_lossy(&buf).into_owned();
139 Ok(NativeMemoryReadFileResult { content })
140}
141
142pub(crate) struct MemoryWriteProvenance<'a> {
149 pub source: &'a str,
150 pub detail: &'a str,
151}
152
153pub(crate) fn memory_write_impl<'o>(
154 state: &AppState,
155 owner: impl Into<SelfOwner<'o>>,
156 filename: &str,
157 content: &str,
158 provenance: Option<MemoryWriteProvenance<'_>>,
159) -> Result<(), String> {
160 let owner = owner.into();
161 let agent_id = owner.label();
162 crate::server::native_memory_handlers::validate_memory_filename(filename)
163 .map_err(|e| format!("memory.write: {e}"))?;
164 const MAX: usize = 10 * 1024 * 1024;
165 if content.len() > MAX {
166 return Err(format!("memory.write: content too large ({} bytes, max {MAX})", content.len()));
167 }
168 let dir = owner.dir_for_write(&state.mstore).map_err(|e| format!("memory.write: {e}"))?;
169 std::fs::create_dir_all(&dir).map_err(|e| format!("memory.write: mkdir: {e}"))?;
170
171 let version_agent_id = owner.owner_id(&state.mstore)
199 .map_err(|e| format!("memory.write: {e}"))?;
200 let (source, detail) = match &provenance {
201 Some(p) => (p.source, p.detail),
202 None => ("agent_inferred", "{}"),
203 };
204 if let Err(e) = state.id_store.agent_native_memory_version_insert(&version_agent_id, filename, content, source, detail, "") {
205 tracing::warn!(agent_id, filename, error = %e, "memory.write: version insert failed (non-fatal)");
206 }
207 crate::backend::memory_reconcile::record_agentmux_write(&version_agent_id, &dir, filename, content.as_bytes(), source, detail);
210
211 let dest = dir.join(filename);
212 let tmp = dir.join(format!(".{}.{}.tmp", filename, uuid::Uuid::new_v4()));
213 if let Err(e) = std::fs::write(&tmp, content) {
214 let _ = std::fs::remove_file(&tmp);
215 return Err(format!("memory.write: write tmp: {e}"));
216 }
217 if let Err(e) = std::fs::rename(&tmp, &dest) {
218 let _ = std::fs::remove_file(&tmp);
219 return Err(format!("memory.write: rename: {e}"));
220 }
221
222 let metadata_type = crate::server::native_memory_handlers::parse_memory_frontmatter_type(content);
230 let dest_meta = std::fs::metadata(&dest).ok();
231 let size_bytes = dest_meta.as_ref().map(|m| m.len() as i64).unwrap_or(content.len() as i64);
232 let mtime_ms = dest_meta
233 .and_then(|m| m.modified().ok())
234 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
235 .map(|d| d.as_millis() as i64)
236 .unwrap_or(0);
237 if let Err(e) = state.id_store.agent_native_memory_upsert(
238 &version_agent_id,
239 filename,
240 content,
241 metadata_type.as_deref(),
242 &dest.to_string_lossy(),
243 size_bytes,
244 mtime_ms,
245 ) {
246 tracing::warn!(agent_id, filename, error = %e, "memory.write: mirror upsert failed (non-fatal)");
247 }
248
249 state.broker.publish_event_for::<crate::backend::mps_events::AgentMemoryChanged>(&version_agent_id, &(), vec![], 0);
260 Ok(())
261}
262
263pub(crate) fn memory_history_impl<'o>(
286 state: &AppState,
287 owner: impl Into<SelfOwner<'o>>,
288 filename: &str,
289) -> Result<serde_json::Value, String> {
290 let owner = owner.into();
291 crate::server::native_memory_handlers::validate_memory_filename(filename)
292 .map_err(|e| format!("memory.history: {e}"))?;
293 let version_agent_id = owner.owner_id(&state.mstore)
296 .map_err(|e| format!("memory.history: {e}"))?;
297 let versions: Vec<crate::backend::rpc_types::NativeMemoryVersionMeta> = state
298 .id_store
299 .agent_native_memory_version_list(&version_agent_id, filename)
300 .map_err(|e| format!("memory.history: store: {e}"))?
301 .into_iter()
302 .map(|v| crate::backend::rpc_types::NativeMemoryVersionMeta {
303 id: v.id,
304 content_hash: v.content_hash,
305 parent_version_id: v.parent_version_id,
306 source: v.source,
307 source_detail: v.source_detail,
308 session_id: v.session_id,
309 created_at: v.created_at,
310 })
311 .collect();
312 serde_json::to_value(crate::backend::rpc_types::NativeMemoryHistoryResult { versions })
313 .map_err(|e| e.to_string())
314}
315
316pub(crate) fn memory_diff_impl<'o>(
317 state: &AppState,
318 owner: impl Into<SelfOwner<'o>>,
319 from_version_id: &str,
320 to_version_id: &str,
321) -> Result<serde_json::Value, String> {
322 let owner = owner.into();
323 let agent_id = owner.label();
324 let version_agent_id = owner.owner_id(&state.mstore)
327 .map_err(|e| format!("memory.diff: {e}"))?;
328 let from = state
329 .id_store
330 .agent_native_memory_version_get(from_version_id)
331 .map_err(|e| format!("memory.diff: store: {e}"))?
332 .ok_or_else(|| format!("memory.diff: version {from_version_id} not found"))?;
333 let to = state
334 .id_store
335 .agent_native_memory_version_get(to_version_id)
336 .map_err(|e| format!("memory.diff: store: {e}"))?
337 .ok_or_else(|| format!("memory.diff: version {to_version_id} not found"))?;
338 if from.agent_id != version_agent_id || to.agent_id != version_agent_id {
341 return Err(format!("memory.diff: one or both versions do not belong to {agent_id}"));
342 }
343 if from.filename != to.filename {
346 return Err(format!(
347 "memory.diff: from_version_id and to_version_id are versions of different files ({} vs {})",
348 from.filename, to.filename
349 ));
350 }
351 let diff = crate::server::native_memory_handlers::line_diff(&from.content, &to.content);
352 serde_json::to_value(crate::backend::rpc_types::NativeMemoryDiffResult { diff }).map_err(|e| e.to_string())
353}
354
355pub(crate) fn memory_revert_impl<'o>(
356 state: &AppState,
357 owner: impl Into<SelfOwner<'o>>,
358 filename: &str,
359 target_version_id: &str,
360) -> Result<serde_json::Value, String> {
361 let owner = owner.into();
362 let agent_id = owner.label();
363 crate::server::native_memory_handlers::validate_memory_filename(filename)
364 .map_err(|e| format!("memory.revert: {e}"))?;
365
366 let version_agent_id = owner.owner_id(&state.mstore)
369 .map_err(|e| format!("memory.revert: {e}"))?;
370
371 let target = state
372 .id_store
373 .agent_native_memory_version_get(target_version_id)
374 .map_err(|e| format!("memory.revert: store: {e}"))?
375 .ok_or_else(|| format!("memory.revert: version {target_version_id} not found"))?;
376 if target.agent_id != version_agent_id || target.filename != filename {
377 return Err(format!(
378 "memory.revert: version {target_version_id} does not belong to {agent_id}/{filename}"
379 ));
380 }
381
382 let dir = owner.dir_for_write(&state.mstore).map_err(|e| format!("memory.revert: {e}"))?;
383 std::fs::create_dir_all(&dir).map_err(|e| format!("memory.revert: mkdir: {e}"))?;
384
385 let detail = json!({ "reverted_to": target_version_id }).to_string();
391 let new_version = state
392 .id_store
393 .agent_native_memory_version_insert(&version_agent_id, filename, &target.content, "revert", &detail, "")
394 .map_err(|e| format!("memory.revert: version insert: {e}"))?;
395
396 let dest = dir.join(filename);
397 let tmp = dir.join(format!(".{}.{}.tmp", filename, uuid::Uuid::new_v4()));
398 if let Err(e) = std::fs::write(&tmp, &target.content) {
399 let _ = std::fs::remove_file(&tmp);
400 return Err(format!("memory.revert: write tmp: {e}"));
401 }
402 if let Err(e) = std::fs::rename(&tmp, &dest) {
403 let _ = std::fs::remove_file(&tmp);
404 return Err(format!("memory.revert: rename: {e}"));
405 }
406
407 let metadata_type = crate::server::native_memory_handlers::parse_memory_frontmatter_type(&target.content);
418 let dest_meta = std::fs::metadata(&dest).ok();
419 let size_bytes = dest_meta.as_ref().map(|m| m.len() as i64).unwrap_or(target.content.len() as i64);
420 let mtime_ms = dest_meta
421 .and_then(|m| m.modified().ok())
422 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
423 .map(|d| d.as_millis() as i64)
424 .unwrap_or(0);
425 if let Err(e) = state.id_store.agent_native_memory_upsert(
426 &version_agent_id,
427 filename,
428 &target.content,
429 metadata_type.as_deref(),
430 &dest.to_string_lossy(),
431 size_bytes,
432 mtime_ms,
433 ) {
434 tracing::warn!(agent_id, filename, error = %e, "memory.revert: mirror upsert failed (non-fatal)");
435 }
436
437 state.broker.publish_event_for::<crate::backend::mps_events::AgentMemoryChanged>(&version_agent_id, &(), vec![], 0);
440
441 serde_json::to_value(crate::backend::rpc_types::NativeMemoryRevertResult {
442 version: crate::backend::rpc_types::NativeMemoryVersionMeta {
443 id: new_version.id,
444 content_hash: new_version.content_hash,
445 parent_version_id: new_version.parent_version_id,
446 source: new_version.source,
447 source_detail: new_version.source_detail,
448 session_id: new_version.session_id,
449 created_at: new_version.created_at,
450 },
451 })
452 .map_err(|e| e.to_string())
453}