1use super::sqlite::SqliteMemory;
2use super::traits::{Memory, MemoryCategory, MemoryEntry, normalize_recent_recall_query};
3use async_trait::async_trait;
4use chrono::Local;
5use parking_lot::Mutex;
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant};
9use tokio::process::Command;
10use tokio::time::timeout;
11
12pub struct LucidMemory {
13 alias: String,
14 local: SqliteMemory,
15 lucid_cmd: String,
16 token_budget: usize,
17 workspace_dir: PathBuf,
18 recall_timeout: Duration,
19 store_timeout: Duration,
20 local_hit_threshold: usize,
21 failure_cooldown: Duration,
22 last_failure_at: Mutex<Option<Instant>>,
23}
24
25impl LucidMemory {
26 const DEFAULT_LUCID_CMD: &'static str = "lucid";
27 const DEFAULT_TOKEN_BUDGET: usize = 200;
28 const DEFAULT_RECALL_TIMEOUT_MS: u64 = 500;
31 const DEFAULT_STORE_TIMEOUT_MS: u64 = 800;
32 const DEFAULT_LOCAL_HIT_THRESHOLD: usize = 3;
33 const DEFAULT_FAILURE_COOLDOWN_MS: u64 = 15_000;
34
35 pub fn new(alias: &str, workspace_dir: &Path, local: SqliteMemory) -> Self {
36 Self {
37 alias: alias.to_string(),
38 local,
39 lucid_cmd: Self::DEFAULT_LUCID_CMD.to_string(),
40 token_budget: Self::DEFAULT_TOKEN_BUDGET,
41 workspace_dir: workspace_dir.to_path_buf(),
42 recall_timeout: Duration::from_millis(Self::DEFAULT_RECALL_TIMEOUT_MS),
43 store_timeout: Duration::from_millis(Self::DEFAULT_STORE_TIMEOUT_MS),
44 local_hit_threshold: Self::DEFAULT_LOCAL_HIT_THRESHOLD,
45 failure_cooldown: Duration::from_millis(Self::DEFAULT_FAILURE_COOLDOWN_MS),
46 last_failure_at: Mutex::new(None),
47 }
48 }
49
50 #[cfg(test)]
51 #[allow(clippy::too_many_arguments)]
52 fn with_options(
53 alias: &str,
54 workspace_dir: &Path,
55 local: SqliteMemory,
56 lucid_cmd: String,
57 token_budget: usize,
58 local_hit_threshold: usize,
59 recall_timeout: Duration,
60 store_timeout: Duration,
61 failure_cooldown: Duration,
62 ) -> Self {
63 Self {
64 alias: alias.to_string(),
65 local,
66 lucid_cmd,
67 token_budget,
68 workspace_dir: workspace_dir.to_path_buf(),
69 recall_timeout,
70 store_timeout,
71 local_hit_threshold: local_hit_threshold.max(1),
72 failure_cooldown,
73 last_failure_at: Mutex::new(None),
74 }
75 }
76
77 fn in_failure_cooldown(&self) -> bool {
78 let guard = self.last_failure_at.lock();
79 guard
80 .as_ref()
81 .is_some_and(|last| last.elapsed() < self.failure_cooldown)
82 }
83
84 fn mark_failure_now(&self) {
85 let mut guard = self.last_failure_at.lock();
86 *guard = Some(Instant::now());
87 }
88
89 fn clear_failure(&self) {
90 let mut guard = self.last_failure_at.lock();
91 *guard = None;
92 }
93
94 fn to_lucid_type(category: &MemoryCategory) -> &'static str {
95 match category {
96 MemoryCategory::Core => "decision",
97 MemoryCategory::Daily => "context",
98 MemoryCategory::Conversation => "conversation",
99 MemoryCategory::Custom(_) => "learning",
100 }
101 }
102
103 fn to_memory_category(label: &str) -> MemoryCategory {
104 let normalized = label.to_lowercase();
105 if normalized.contains("visual") {
106 return MemoryCategory::Custom("visual".to_string());
107 }
108
109 match normalized.as_str() {
110 "decision" | "learning" | "solution" => MemoryCategory::Core,
111 "context" | "conversation" => MemoryCategory::Conversation,
112 "bug" => MemoryCategory::Daily,
113 other => MemoryCategory::Custom(other.to_string()),
114 }
115 }
116
117 fn merge_results(
118 primary_results: Vec<MemoryEntry>,
119 secondary_results: Vec<MemoryEntry>,
120 limit: usize,
121 ) -> Vec<MemoryEntry> {
122 if limit == 0 {
123 return Vec::new();
124 }
125
126 let mut merged = Vec::new();
127 let mut seen = HashSet::new();
128
129 for entry in primary_results.into_iter().chain(secondary_results) {
130 let signature = format!(
131 "{}\u{0}{}",
132 entry.key.to_lowercase(),
133 entry.content.to_lowercase()
134 );
135
136 if seen.insert(signature) {
137 merged.push(entry);
138 if merged.len() >= limit {
139 break;
140 }
141 }
142 }
143
144 merged
145 }
146
147 fn parse_lucid_context(raw: &str) -> Vec<MemoryEntry> {
148 let mut in_context_block = false;
149 let mut entries = Vec::new();
150 let now = Local::now().to_rfc3339();
151
152 for line in raw.lines().map(str::trim) {
153 if line == "<lucid-context>" {
154 in_context_block = true;
155 continue;
156 }
157
158 if line == "</lucid-context>" {
159 break;
160 }
161
162 if !in_context_block || line.is_empty() {
163 continue;
164 }
165
166 let Some(rest) = line.strip_prefix("- [") else {
167 continue;
168 };
169
170 let Some((label, content_part)) = rest.split_once(']') else {
171 continue;
172 };
173
174 let content = content_part.trim();
175 if content.is_empty() {
176 continue;
177 }
178
179 let rank = entries.len();
180 entries.push(MemoryEntry {
181 id: format!("lucid:{rank}"),
182 key: format!("lucid_{rank}"),
183 content: content.to_string(),
184 category: Self::to_memory_category(label.trim()),
185 timestamp: now.clone(),
186 session_id: None,
187 score: Some((1.0 - rank as f64 * 0.05).max(0.1)),
188 namespace: "default".into(),
189 importance: None,
190 superseded_by: None,
191 agent_alias: None,
192 agent_id: None,
193 });
194 }
195
196 entries
197 }
198
199 async fn run_lucid_command_raw(
200 lucid_cmd: &str,
201 args: &[String],
202 timeout_window: Duration,
203 ) -> anyhow::Result<String> {
204 let mut cmd = Command::new(lucid_cmd);
205 cmd.args(args);
206
207 let output = timeout(timeout_window, cmd.output()).await.map_err(|_| {
208 ::zeroclaw_log::record!(
209 ERROR,
210 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Timeout)
211 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
212 .with_attrs(::serde_json::json!({
213 "command": lucid_cmd,
214 "timeout_ms": timeout_window.as_millis() as u64,
215 })),
216 "lucid command timed out"
217 );
218 anyhow::Error::msg(format!(
219 "lucid command timed out after {}ms",
220 timeout_window.as_millis()
221 ))
222 })??;
223
224 if !output.status.success() {
225 let stderr = String::from_utf8_lossy(&output.stderr);
226 anyhow::bail!("lucid command failed: {stderr}");
227 }
228
229 Ok(String::from_utf8_lossy(&output.stdout).to_string())
230 }
231
232 async fn run_lucid_command(
233 &self,
234 args: &[String],
235 timeout_window: Duration,
236 ) -> anyhow::Result<String> {
237 Self::run_lucid_command_raw(&self.lucid_cmd, args, timeout_window).await
238 }
239
240 fn build_store_args(&self, key: &str, content: &str, category: &MemoryCategory) -> Vec<String> {
241 let payload = format!("{key}: {content}");
242 vec![
243 "store".to_string(),
244 payload,
245 format!("--type={}", Self::to_lucid_type(category)),
246 format!("--project={}", self.workspace_dir.display().to_string()),
247 ]
248 }
249
250 fn build_recall_args(&self, query: &str) -> Vec<String> {
251 vec![
252 "context".to_string(),
253 query.to_string(),
254 format!("--budget={}", self.token_budget),
255 format!("--project={}", self.workspace_dir.display().to_string()),
256 ]
257 }
258
259 async fn sync_to_lucid_async(&self, key: &str, content: &str, category: &MemoryCategory) {
260 let args = self.build_store_args(key, content, category);
261 if let Err(error) = self.run_lucid_command(&args, self.store_timeout).await {
262 ::zeroclaw_log::record!(
263 DEBUG,
264 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
265 .with_attrs(
266 ::serde_json::json!({"command": self.lucid_cmd, "error": format!("{}", error)})
267 ),
268 "Lucid store sync failed; sqlite remains authoritative"
269 );
270 }
271 }
272
273 async fn recall_from_lucid(&self, query: &str) -> anyhow::Result<Vec<MemoryEntry>> {
274 let args = self.build_recall_args(query);
275 let output = self.run_lucid_command(&args, self.recall_timeout).await?;
276 Ok(Self::parse_lucid_context(&output))
277 }
278}
279
280#[async_trait]
281impl Memory for LucidMemory {
282 fn name(&self) -> &str {
283 "lucid"
284 }
285
286 async fn store(
287 &self,
288 key: &str,
289 content: &str,
290 category: MemoryCategory,
291 session_id: Option<&str>,
292 ) -> anyhow::Result<()> {
293 self.local
294 .store(key, content, category.clone(), session_id)
295 .await?;
296 self.sync_to_lucid_async(key, content, &category).await;
297 Ok(())
298 }
299
300 async fn recall(
301 &self,
302 query: &str,
303 limit: usize,
304 session_id: Option<&str>,
305 since: Option<&str>,
306 until: Option<&str>,
307 ) -> anyhow::Result<Vec<MemoryEntry>> {
308 let since_dt = since
309 .map(chrono::DateTime::parse_from_rfc3339)
310 .transpose()
311 .map_err(|e| {
312 ::zeroclaw_log::record!(
313 WARN,
314 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
315 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
316 .with_attrs(
317 ::serde_json::json!({"field": "since", "error": format!("{}", e)})
318 ),
319 "recall window bound rejected"
320 );
321 anyhow::Error::msg(format!("invalid 'since' date (expected RFC 3339): {e}"))
322 })?;
323 let until_dt = until
324 .map(chrono::DateTime::parse_from_rfc3339)
325 .transpose()
326 .map_err(|e| {
327 ::zeroclaw_log::record!(
328 WARN,
329 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
330 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
331 .with_attrs(
332 ::serde_json::json!({"field": "until", "error": format!("{}", e)})
333 ),
334 "recall window bound rejected"
335 );
336 anyhow::Error::msg(format!("invalid 'until' date (expected RFC 3339): {e}"))
337 })?;
338 if let (Some(s), Some(u)) = (&since_dt, &until_dt)
339 && s >= u
340 {
341 anyhow::bail!("'since' must be before 'until'");
342 }
343
344 let recall_query = normalize_recent_recall_query(query);
345
346 let local_results = self
347 .local
348 .recall(recall_query, limit, session_id, since, until)
349 .await?;
350 if limit == 0
351 || local_results.len() >= limit
352 || local_results.len() >= self.local_hit_threshold
353 {
354 return Ok(local_results);
355 }
356
357 if self.in_failure_cooldown() {
358 return Ok(local_results);
359 }
360
361 match self.recall_from_lucid(recall_query).await {
362 Ok(lucid_results) if !lucid_results.is_empty() => {
363 self.clear_failure();
364 let merged = Self::merge_results(local_results, lucid_results, limit);
365 let filtered: Vec<MemoryEntry> = merged
366 .into_iter()
367 .filter(|e| {
368 if let Some(ref s) = since_dt
369 && let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&e.timestamp)
370 && ts < *s
371 {
372 return false;
373 }
374 if let Some(ref u) = until_dt
375 && let Ok(ts) = chrono::DateTime::parse_from_rfc3339(&e.timestamp)
376 && ts > *u
377 {
378 return false;
379 }
380 true
381 })
382 .collect();
383 Ok(filtered)
384 }
385 Ok(_) => {
386 self.clear_failure();
387 Ok(local_results)
388 }
389 Err(error) => {
390 self.mark_failure_now();
391 ::zeroclaw_log::record!(DEBUG, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({"command": self.lucid_cmd, "error": format!("{}", error)})), "Lucid context unavailable; using local sqlite results");
392 Ok(local_results)
393 }
394 }
395 }
396
397 async fn get(&self, key: &str) -> anyhow::Result<Option<MemoryEntry>> {
398 self.local.get(key).await
399 }
400
401 async fn get_for_agent(
402 &self,
403 key: &str,
404 agent_id: &str,
405 ) -> anyhow::Result<Option<MemoryEntry>> {
406 self.local.get_for_agent(key, agent_id).await
407 }
408
409 async fn list(
410 &self,
411 category: Option<&MemoryCategory>,
412 session_id: Option<&str>,
413 ) -> anyhow::Result<Vec<MemoryEntry>> {
414 self.local.list(category, session_id).await
415 }
416
417 async fn forget(&self, key: &str) -> anyhow::Result<bool> {
418 self.local.forget(key).await
419 }
420
421 async fn forget_for_agent(&self, key: &str, agent_id: &str) -> anyhow::Result<bool> {
422 self.local.forget_for_agent(key, agent_id).await
423 }
424
425 async fn purge_session_for_agent(
426 &self,
427 session_id: &str,
428 agent_id: &str,
429 ) -> anyhow::Result<usize> {
430 self.local
431 .purge_session_for_agent(session_id, agent_id)
432 .await
433 }
434
435 async fn count(&self) -> anyhow::Result<usize> {
436 self.local.count().await
437 }
438
439 async fn health_check(&self) -> bool {
440 self.local.health_check().await
441 }
442
443 async fn store_with_agent(
444 &self,
445 key: &str,
446 content: &str,
447 category: MemoryCategory,
448 session_id: Option<&str>,
449 namespace: Option<&str>,
450 importance: Option<f64>,
451 agent_id: Option<&str>,
452 ) -> anyhow::Result<()> {
453 self.local
458 .store_with_agent(
459 key,
460 content,
461 category.clone(),
462 session_id,
463 namespace,
464 importance,
465 agent_id,
466 )
467 .await?;
468 self.sync_to_lucid_async(key, content, &category).await;
469 Ok(())
470 }
471
472 async fn recall_for_agents(
473 &self,
474 allowed_agent_ids: &[&str],
475 query: &str,
476 limit: usize,
477 session_id: Option<&str>,
478 since: Option<&str>,
479 until: Option<&str>,
480 ) -> anyhow::Result<Vec<MemoryEntry>> {
481 self.local
486 .recall_for_agents(allowed_agent_ids, query, limit, session_id, since, until)
487 .await
488 }
489
490 async fn ensure_agent_uuid(&self, alias: &str) -> anyhow::Result<String> {
491 self.local.ensure_agent_uuid(alias).await
494 }
495}
496
497#[cfg(all(test, unix))]
498mod tests {
499 use super::*;
500 use std::fs;
501 use std::os::unix::fs::PermissionsExt;
502 use tempfile::TempDir;
503
504 fn write_fake_lucid_script(dir: &Path) -> String {
505 let script_path = dir.join("fake-lucid.sh");
506 let script = r#"#!/usr/bin/env bash
507set -euo pipefail
508
509if [[ "${1:-}" == "store" ]]; then
510 echo '{"success":true,"id":"mem_1"}'
511 exit 0
512fi
513
514if [[ "${1:-}" == "context" ]]; then
515 cat <<'EOF'
516<lucid-context>
517Auth context snapshot
518- [decision] Use token refresh middleware
519- [context] Working in src/auth.rs
520</lucid-context>
521EOF
522 exit 0
523fi
524
525echo "unsupported command" >&2
526exit 1
527"#;
528
529 fs::write(&script_path, script).unwrap();
530 let mut perms = fs::metadata(&script_path).unwrap().permissions();
531 perms.set_mode(0o755);
532 fs::set_permissions(&script_path, perms).unwrap();
533 script_path.display().to_string()
534 }
535
536 fn write_delayed_lucid_script(dir: &Path) -> String {
537 let script_path = dir.join("delayed-lucid.sh");
538 let script = r#"#!/usr/bin/env bash
539set -euo pipefail
540
541if [[ "${1:-}" == "store" ]]; then
542 echo '{"success":true,"id":"mem_1"}'
543 exit 0
544fi
545
546if [[ "${1:-}" == "context" ]]; then
547 # Simulate a cold start that is slower than 120ms but below the 500ms timeout.
548 sleep 0.2
549 cat <<'EOF'
550<lucid-context>
551- [decision] Delayed token refresh guidance
552</lucid-context>
553EOF
554 exit 0
555fi
556
557echo "unsupported command" >&2
558exit 1
559"#;
560
561 fs::write(&script_path, script).unwrap();
562 let mut perms = fs::metadata(&script_path).unwrap().permissions();
563 perms.set_mode(0o755);
564 fs::set_permissions(&script_path, perms).unwrap();
565 script_path.display().to_string()
566 }
567
568 fn write_probe_lucid_script(dir: &Path, marker_path: &Path) -> String {
569 let script_path = dir.join("probe-lucid.sh");
570 let marker = marker_path.display().to_string();
571 let script = format!(
572 r#"#!/usr/bin/env bash
573set -euo pipefail
574
575if [[ "${{1:-}}" == "store" ]]; then
576 echo '{{"success":true,"id":"mem_store"}}'
577 exit 0
578fi
579
580if [[ "${{1:-}}" == "context" ]]; then
581 printf 'context\n' >> "{marker}"
582 cat <<'EOF'
583<lucid-context>
584- [decision] should not be used when local hits are enough
585</lucid-context>
586EOF
587 exit 0
588fi
589
590echo "unsupported command" >&2
591exit 1
592"#
593 );
594
595 fs::write(&script_path, script).unwrap();
596 let mut perms = fs::metadata(&script_path).unwrap().permissions();
597 perms.set_mode(0o755);
598 fs::set_permissions(&script_path, perms).unwrap();
599 script_path.display().to_string()
600 }
601
602 fn test_memory(workspace: &Path, cmd: String) -> LucidMemory {
603 let sqlite = SqliteMemory::new("sqlite", workspace).unwrap();
604 LucidMemory::with_options(
605 "test",
606 workspace,
607 sqlite,
608 cmd,
609 200,
610 3,
611 Duration::from_secs(5),
612 Duration::from_secs(5),
613 Duration::from_secs(2),
614 )
615 }
616
617 #[tokio::test]
618 async fn lucid_name() {
619 let tmp = TempDir::new().unwrap();
620 let memory = test_memory(tmp.path(), "nonexistent-lucid-binary".to_string());
621 assert_eq!(memory.name(), "lucid");
622 }
623
624 #[tokio::test]
625 async fn store_succeeds_when_lucid_missing() {
626 let tmp = TempDir::new().unwrap();
627 let memory = test_memory(tmp.path(), "nonexistent-lucid-binary".to_string());
628
629 memory
630 .store("lang", "User prefers Rust", MemoryCategory::Core, None)
631 .await
632 .unwrap();
633
634 let entry = memory.get("lang").await.unwrap();
635 assert!(entry.is_some());
636 assert_eq!(entry.unwrap().content, "User prefers Rust");
637 }
638
639 #[tokio::test]
640 async fn recall_merges_lucid_and_local_results() {
641 let tmp = TempDir::new().unwrap();
642 let fake_cmd = write_fake_lucid_script(tmp.path());
643 let memory = test_memory(tmp.path(), fake_cmd);
644
645 memory
646 .store(
647 "local_note",
648 "Local sqlite auth fallback note",
649 MemoryCategory::Core,
650 None,
651 )
652 .await
653 .unwrap();
654
655 let entries = memory.recall("auth", 5, None, None, None).await.unwrap();
656
657 assert!(
658 entries
659 .iter()
660 .any(|e| e.content.contains("Local sqlite auth fallback note"))
661 );
662 assert!(entries.iter().any(|e| e.content.contains("token refresh")));
663 }
664
665 #[tokio::test]
666 async fn recall_handles_lucid_cold_start_delay_within_timeout() {
667 let tmp = TempDir::new().unwrap();
668 let delayed_cmd = write_delayed_lucid_script(tmp.path());
669 let memory = test_memory(tmp.path(), delayed_cmd);
670
671 memory
672 .store(
673 "local_note",
674 "Local sqlite auth fallback note",
675 MemoryCategory::Core,
676 None,
677 )
678 .await
679 .unwrap();
680
681 let entries = memory.recall("auth", 5, None, None, None).await.unwrap();
682
683 assert!(
684 entries
685 .iter()
686 .any(|e| e.content.contains("Local sqlite auth fallback note"))
687 );
688 assert!(
689 entries
690 .iter()
691 .any(|e| e.content.contains("Delayed token refresh guidance"))
692 );
693 }
694
695 #[tokio::test]
696 async fn recall_skips_lucid_when_local_hits_are_enough() {
697 let tmp = TempDir::new().unwrap();
698 let marker = tmp.path().join("context_calls.log");
699 let probe_cmd = write_probe_lucid_script(tmp.path(), &marker);
700
701 let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
702 let memory = LucidMemory::with_options(
703 "test",
704 tmp.path(),
705 sqlite,
706 probe_cmd,
707 200,
708 1,
709 Duration::from_secs(5),
710 Duration::from_secs(5),
711 Duration::from_secs(2),
712 );
713
714 memory
715 .store(
716 "pref",
717 "Rust should stay local-first",
718 MemoryCategory::Core,
719 None,
720 )
721 .await
722 .unwrap();
723
724 let entries = memory.recall("rust", 5, None, None, None).await.unwrap();
725 assert!(
726 entries
727 .iter()
728 .any(|e| e.content.contains("Rust should stay local-first"))
729 );
730
731 let context_calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
732 assert!(
733 context_calls.trim().is_empty(),
734 "Expected local-hit short-circuit; got calls: {context_calls}"
735 );
736 }
737
738 fn write_failing_lucid_script(dir: &Path, marker_path: &Path) -> String {
739 let script_path = dir.join("failing-lucid.sh");
740 let marker = marker_path.display().to_string();
741 let script = format!(
742 r#"#!/usr/bin/env bash
743set -euo pipefail
744
745if [[ "${{1:-}}" == "store" ]]; then
746 echo '{{"success":true,"id":"mem_store"}}'
747 exit 0
748fi
749
750if [[ "${{1:-}}" == "context" ]]; then
751 printf 'context\n' >> "{marker}"
752 echo "simulated lucid failure" >&2
753 exit 1
754fi
755
756echo "unsupported command" >&2
757exit 1
758"#
759 );
760
761 fs::write(&script_path, script).unwrap();
762 let mut perms = fs::metadata(&script_path).unwrap().permissions();
763 perms.set_mode(0o755);
764 fs::set_permissions(&script_path, perms).unwrap();
765 script_path.display().to_string()
766 }
767
768 #[tokio::test]
769 async fn failure_cooldown_avoids_repeated_lucid_calls() {
770 let tmp = TempDir::new().unwrap();
771 let marker = tmp.path().join("failing_context_calls.log");
772 let failing_cmd = write_failing_lucid_script(tmp.path(), &marker);
773
774 let sqlite = SqliteMemory::new("test", tmp.path()).unwrap();
775 let memory = LucidMemory::with_options(
776 "test",
777 tmp.path(),
778 sqlite,
779 failing_cmd,
780 200,
781 99,
782 Duration::from_secs(5),
783 Duration::from_secs(5),
784 Duration::from_secs(5),
785 );
786
787 let first = memory.recall("auth", 5, None, None, None).await.unwrap();
788 let second = memory.recall("auth", 5, None, None, None).await.unwrap();
789
790 assert!(first.is_empty());
791 assert!(second.is_empty());
792
793 let calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
794 assert_eq!(calls.lines().count(), 1);
795 }
796}
797
798impl ::zeroclaw_api::attribution::Attributable for LucidMemory {
799 fn role(&self) -> ::zeroclaw_api::attribution::Role {
800 ::zeroclaw_api::attribution::Role::Memory(::zeroclaw_api::attribution::MemoryKind::Lucid)
801 }
802 fn alias(&self) -> &str {
803 &self.alias
804 }
805}