handshake_core/memories/
shared.rs1mod exchange;
4mod storage;
5
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum SharedMemoryError {
14 #[error("Enable cross-agent memory consolidation in Settings > Feature flags first.")]
15 FeatureDisabled,
16 #[error("Choose an existing absolute folder path.")]
17 InvalidFolder,
18 #[error("Enable automatic sharing for this folder first.")]
19 Disabled,
20 #[error("Use nonempty memory text without unsupported control characters.")]
21 InvalidText,
22 #[error("Only a Codex or Claude Code durable memory belonging to this folder can be imported.")]
23 InvalidSource,
24 #[error("This memory is no longer available. Refresh the shared library.")]
25 Missing,
26 #[error(transparent)]
27 Io(#[from] std::io::Error),
28 #[error(transparent)]
29 Database(#[from] rusqlite::Error),
30 #[error(transparent)]
31 Memory(#[from] super::MemoryError),
32}
33
34#[derive(Clone, Debug, Deserialize)]
35#[serde(tag = "operation", rename_all = "camelCase", deny_unknown_fields)]
36pub enum SharedOperation {
37 Refresh,
38 SetEnabled { enabled: bool },
39 Add { text: String },
40 Import { source_id: String },
41 Withdraw { id: String },
42 Read { id: String },
43}
44
45#[derive(Debug, Deserialize)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct SharedRequest {
48 pub data_directory: PathBuf,
49 pub folder: PathBuf,
50 pub action: SharedOperation,
51}
52
53#[derive(Debug, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct SharedSnapshot {
56 pub enabled: bool,
57 pub delivery_ready: bool,
58 pub folder: PathBuf,
59 pub inbox: PathBuf,
60 pub catalog_path: PathBuf,
61 pub notes: Vec<SharedNote>,
62 pub detail: Option<SharedNoteDetail>,
63 pub diagnostics: Vec<String>,
64}
65
66#[derive(Debug, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct SharedNote {
69 pub id: String,
70 pub title: String,
71 pub source: String,
72 pub digest: String,
73 pub status: String,
74}
75
76#[derive(Debug, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct SharedNoteDetail {
79 pub id: String,
80 pub digest: String,
81 pub text: String,
82}
83
84pub fn execute(request: &SharedRequest) -> Result<SharedSnapshot, SharedMemoryError> {
89 if !crate::config::Config::load_user()
90 .feature_flags
91 .cross_agent_memory_consolidation
92 {
93 return Err(SharedMemoryError::FeatureDisabled);
94 }
95 if !request.folder.is_absolute() || !request.folder.is_dir() {
96 return Err(SharedMemoryError::InvalidFolder);
97 }
98 let folder = request.folder.canonicalize()?;
99 let root = library_root(&request.data_directory, &folder)?;
100 storage::execute(&root, &folder, &request.action)
101}
102
103fn library_root(data: &Path, folder: &Path) -> Result<PathBuf, SharedMemoryError> {
104 if !data.is_absolute() {
105 return Err(SharedMemoryError::InvalidFolder);
106 }
107 Ok(data
108 .join("shared-memory")
109 .join(digest(folder.as_os_str().as_encoded_bytes())))
110}
111
112fn digest(bytes: &[u8]) -> String {
113 use std::fmt::Write as _;
114 Sha256::digest(bytes)
115 .iter()
116 .fold(String::with_capacity(64), |mut hex, byte| {
117 let _ = write!(hex, "{byte:02x}");
118 hex
119 })
120}
121
122fn import_text(folder: &Path, source_id: &str) -> Result<(String, String), SharedMemoryError> {
123 let memory = super::read(&super::MemoryRequest {
124 folder: Some(folder.into()),
125 source_id: Some(source_id.into()),
126 examples_root: None,
127 })?;
128 let default_claude_group = format!(
129 "Claude folder: {}",
130 super::locations::claude_key(&super::locations::repository_root(folder))
131 );
132 if memory.source.group.starts_with("Claude folder: ")
133 && memory.source.group != default_claude_group
134 {
135 return Err(SharedMemoryError::InvalidSource);
136 }
137 if memory.source.scope != super::MemoryScope::Folder
138 || memory.source.group == "Claude configured memory directory"
139 || !matches!(
140 memory.source.harness,
141 super::MemoryHarness::Codex | super::MemoryHarness::ClaudeCode
142 )
143 || !matches!(
144 memory.source.kind,
145 super::MemoryKind::Durable | super::MemoryKind::AgentMemory
146 )
147 || memory.source.example
148 {
149 return Err(SharedMemoryError::InvalidSource);
150 }
151 Ok((
152 memory.text.ok_or(SharedMemoryError::InvalidSource)?,
153 memory.source.location,
154 ))
155}