Skip to main content

handshake_core/
memories.rs

1//! Discovery of local harness memories, instructions, and extraction records.
2//! Database reads use private snapshots; explicit edits are limited to Markdown files.
3mod database;
4mod discovery;
5mod editing;
6pub mod handshake;
7mod locations;
8pub mod native_import;
9pub mod shared;
10
11pub use editing::save;
12
13use crate::FileContent;
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use thiserror::Error;
17
18pub const SOURCE_LIMIT: usize = 1_024;
19
20#[derive(Clone, Debug, Default, Deserialize, Serialize)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22pub struct MemoryRequest {
23    pub folder: Option<PathBuf>,
24    pub source_id: Option<String>,
25    pub examples_root: Option<PathBuf>,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub enum MemoryHarness {
31    Codex,
32    ClaudeCode,
33    Antigravity,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub enum MemoryScope {
39    Global,
40    Folder,
41    OtherFolder,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub enum MemoryKind {
47    Summary,
48    Durable,
49    RawMemory,
50    RolloutSummary,
51    Instructions,
52    Rule,
53    AgentMemory,
54    Skill,
55    Extension,
56    Artifact,
57    StoreStatus,
58    Configuration,
59}
60
61#[derive(Clone, Debug, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct MemorySource {
64    pub id: String,
65    pub title: String,
66    pub harness: MemoryHarness,
67    pub scope: MemoryScope,
68    pub kind: MemoryKind,
69    pub location: String,
70    pub group: String,
71    pub format: String,
72    pub modified_at: Option<u64>,
73    pub example: bool,
74    pub association: Option<String>,
75}
76
77#[derive(Clone, Debug, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct MemoryCatalog {
80    pub sources: Vec<MemorySource>,
81    pub diagnostics: Vec<String>,
82}
83
84#[derive(Clone, Debug, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct MemoryRead {
87    pub source: MemorySource,
88    pub text: Option<String>,
89    pub message: Option<String>,
90    #[serde(skip)]
91    pub file: Option<crate::FilePreview>,
92}
93
94#[derive(Debug, Error)]
95pub enum MemoryError {
96    #[error("Memory discovery requires absolute folder paths.")]
97    InvalidPath,
98    #[error("The source is no longer available. Refresh the source list.")]
99    MissingSource,
100    #[error("Only local Markdown files can be edited from Memories.")]
101    UnsupportedEdit,
102    #[error(transparent)]
103    File(#[from] crate::FileError),
104}
105
106/// Lists bounded local sources without generating or editing memory.
107///
108/// # Errors
109/// Returns an error when a requested folder or example root is not absolute.
110pub fn catalog(request: &MemoryRequest) -> Result<MemoryCatalog, MemoryError> {
111    let (sources, diagnostics) = discovery::discover(request)?;
112    Ok(MemoryCatalog {
113        sources: sources.into_iter().map(|e| e.source).collect(),
114        diagnostics,
115    })
116}
117
118/// Reads a source by a discovered opaque ID.
119///
120/// # Errors
121/// Returns an error for invalid paths or a source that is no longer discoverable.
122pub fn read(request: &MemoryRequest) -> Result<MemoryRead, MemoryError> {
123    let id = request
124        .source_id
125        .as_ref()
126        .ok_or(MemoryError::MissingSource)?;
127    let (entries, _) = discovery::discover(request)?;
128    let entry = entries
129        .into_iter()
130        .find(|e| &e.source.id == id)
131        .ok_or(MemoryError::MissingSource)?;
132    let mut file = None;
133    let content = match entry.backing {
134        discovery::Backing::File { root, path } => {
135            let preview = crate::files::read_preview(&root, &path);
136            let content = preview.content.clone();
137            file = Some(preview);
138            content
139        }
140        discovery::Backing::Text(text) => FileContent::Text(text),
141        discovery::Backing::Database {
142            root,
143            path,
144            thread,
145            column,
146        } => database::read_record(&root, &path, &thread, &column),
147    };
148    let (text, message) = match content {
149        FileContent::Text(text) => (Some(text), None),
150        FileContent::Binary => (
151            None,
152            Some("This source is binary, rather than UTF-8 text.".into()),
153        ),
154        FileContent::TooLarge => (
155            None,
156            Some("This source exceeds the 2 MiB reading limit.".into()),
157        ),
158        FileContent::Missing => (
159            None,
160            Some("This source was removed. Refresh the list.".into()),
161        ),
162        FileContent::Unsupported => (
163            None,
164            Some("Symbolic links and special files cannot be read.".into()),
165        ),
166        FileContent::Unavailable(message) => (None, Some(message)),
167    };
168    Ok(MemoryRead {
169        source: entry.source,
170        text,
171        message,
172        file,
173    })
174}