Skip to main content

microscope_memory/
ai_adapter.rs

1//! AI Adapter for direct binary communication with Microscope Memory.
2//!
3//! Provides a high-performance interface for AI systems to interact with the memory
4//! using fixed 256-byte binary commands over Unix domain sockets or named pipes.
5//!
6//! Zero-JSON, zero-copy, zero-latency communication protocol.
7//!
8//! **Experimental and currently UNWIRED**: this module is not referenced from
9//! `main.rs`, `mcp.rs` or `bridge.rs`. Do not rely on its integrity claims —
10//! `update_merkle_tree` fails loudly until the incremental Merkle path exists,
11//! so a stale root can never be reported as freshly verified.
12
13use crate::config::Config;
14use crate::reader::MicroscopeReader;
15use crate::{store_memory, LAYER_NAMES};
16use std::path::Path;
17use std::sync::Arc;
18#[cfg(unix)]
19use std::{io::Read, io::Write};
20
21// ─── Binary Protocol Definition ──────────────────────────
22
23/// Fixed 256-byte binary command structure for AI communication.
24/// Repr(C) ensures consistent layout across platforms.
25#[repr(C)]
26#[derive(Debug, Copy, Clone)]
27pub struct AICommand {
28    pub block_id: u64,      // Block identifier (0 for new blocks)
29    pub weight_delta: f32,  // Learning weight delta for Hebbian updates
30    pub op_code: u8,        // 0: Read, 1: Write, 2: Learn/Drift
31    pub layer: u8,          // Target layer (0-9)
32    pub payload: [u8; 242], // Data payload (fits in 256 bytes total)
33}
34
35impl AICommand {
36    /// Create a new read command
37    pub fn read(layer: u8, block_id: u64) -> Self {
38        Self {
39            op_code: 0,
40            layer,
41            block_id,
42            weight_delta: 0.0,
43            payload: [0; 242],
44        }
45    }
46
47    /// Create a new write command
48    pub fn write(layer: u8, block_id: u64, data: &[u8]) -> Self {
49        let mut payload = [0u8; 242];
50        let len = data.len().min(242);
51        payload[..len].copy_from_slice(&data[..len]);
52
53        Self {
54            op_code: 1,
55            layer,
56            block_id,
57            weight_delta: 0.0,
58            payload,
59        }
60    }
61
62    /// Create a learning/drift command
63    pub fn learn(block_id: u64, weight_delta: f32) -> Self {
64        Self {
65            op_code: 2,
66            layer: 0,
67            block_id,
68            weight_delta,
69            payload: [0; 242],
70        }
71    }
72}
73
74impl Default for AICommand {
75    fn default() -> Self {
76        Self {
77            op_code: 0,
78            layer: 0,
79            block_id: 0,
80            weight_delta: 0.0,
81            payload: [0; 242],
82        }
83    }
84}
85
86const _: () = assert!(std::mem::size_of::<AICommand>() == 256);
87
88// ─── AI Adapter Implementation ───────────────────────────
89
90/// High-performance AI adapter for binary protocol communication.
91pub struct AIAdapter {
92    config: Arc<Config>,
93    reader: Arc<MicroscopeReader>,
94    dirty_blocks: std::collections::HashSet<u64>, // Blocks that need Merkle update
95    command_count: usize,                         // Commands processed since last Merkle update
96}
97
98impl AIAdapter {
99    /// Create a new AI adapter with the given configuration.
100    pub fn new(config: Config) -> Result<Self, String> {
101        let reader = MicroscopeReader::open(&config)?;
102        Ok(Self {
103            config: Arc::new(config),
104            reader: Arc::new(reader),
105            dirty_blocks: std::collections::HashSet::new(),
106            command_count: 0,
107        })
108    }
109
110    /// Process an AI command and return a response.
111    pub fn process_command(&mut self, cmd: AICommand) -> Result<AICommand, String> {
112        self.command_count += 1;
113
114        let result = match cmd.op_code {
115            0 => self.handle_read(cmd),
116            1 => self.handle_write(cmd),
117            2 => self.handle_learn(cmd),
118            _ => Err(format!("Unknown op_code: {}", cmd.op_code)),
119        };
120
121        // Lazy Merkle update: batch updates every 100 commands or when explicitly requested
122        if self.command_count.is_multiple_of(100) && !self.dirty_blocks.is_empty() {
123            self.update_merkle_tree()?;
124        }
125
126        result
127    }
128
129    /// Handle read operations.
130    fn handle_read(&self, cmd: AICommand) -> Result<AICommand, String> {
131        if cmd.block_id as usize >= self.reader.block_count {
132            return Err("Block ID out of range".to_string());
133        }
134
135        let text = self.reader.text(cmd.block_id as usize);
136        let data = text.as_bytes();
137        let mut response = AICommand {
138            op_code: 0, // Read response
139            layer: cmd.layer,
140            block_id: cmd.block_id,
141            ..AICommand::default()
142        };
143        let len = data.len().min(242);
144        response.payload[..len].copy_from_slice(&data[..len]);
145
146        Ok(response)
147    }
148
149    /// Handle write operations.
150    /// The payload format is: [1 byte length][UTF-8 text bytes].
151    /// The text is stored into the append log via store_memory, using the
152    /// layer encoded in cmd.layer and a default importance of 5.
153    fn handle_write(&mut self, cmd: AICommand) -> Result<AICommand, String> {
154        let len = cmd.payload[0] as usize;
155        if len > 241 {
156            return Err("Invalid payload length".to_string());
157        }
158        let text = String::from_utf8(cmd.payload[1..1 + len].to_vec())
159            .map_err(|e| format!("Invalid UTF-8 payload: {}", e))?;
160
161        let layer_name = LAYER_NAMES
162            .get(cmd.layer as usize)
163            .copied()
164            .unwrap_or("long_term");
165
166        store_memory(
167            &self.config,
168            &text,
169            layer_name,
170            5, // default importance
171        )?;
172
173        // Mark block as dirty for future Merkle update support
174        self.dirty_blocks.insert(cmd.block_id);
175
176        Ok(AICommand {
177            op_code: 1, // Write response
178            layer: cmd.layer,
179            block_id: cmd.block_id,
180            ..AICommand::default()
181        })
182    }
183
184    /// Handle learning/drift operations.
185    /// Records a Hebbian activation for the given block with the supplied weight delta.
186    fn handle_learn(&mut self, cmd: AICommand) -> Result<AICommand, String> {
187        use crate::hebbian::HebbianState;
188
189        let mut state = HebbianState::load_or_init(
190            Path::new(&self.config.paths.output_dir),
191            self.reader.block_count,
192        );
193
194        let block_idx = cmd.block_id as u32;
195        let query_hash = cmd.block_id; // use block id as query identifier
196        state.record_activation(&[(block_idx, cmd.weight_delta)], query_hash);
197
198        state.save(Path::new(&self.config.paths.output_dir))?;
199
200        // Mark block as dirty for future Merkle update support
201        self.dirty_blocks.insert(cmd.block_id);
202
203        Ok(AICommand {
204            op_code: 2, // Learn response
205            layer: cmd.layer,
206            block_id: cmd.block_id,
207            ..AICommand::default()
208        })
209    }
210
211    /// Force immediate Merkle tree update for dirty blocks.
212    ///
213    /// The incremental Merkle update is not implemented yet. When blocks are
214    /// dirty this returns an explicit error instead of clearing the dirty set
215    /// and pretending the tree was updated — a stale Merkle root must never be
216    /// reported as freshly verified.
217    pub fn update_merkle_tree(&mut self) -> Result<(), String> {
218        if self.dirty_blocks.is_empty() {
219            return Ok(());
220        }
221        Err(format!(
222            "incremental Merkle update not implemented ({} dirty blocks pending)",
223            self.dirty_blocks.len()
224        ))
225    }
226
227    /// Get current Merkle root for integrity verification.
228    pub fn current_merkle_root(&self) -> Result<[u8; 32], String> {
229        let output_dir = Path::new(&self.config.paths.output_dir);
230        let merkle_data = std::fs::read(output_dir.join("merkle.bin"))
231            .map_err(|e| format!("Failed to read merkle.bin: {}", e))?;
232        let tree = crate::merkle::MerkleTree::from_bytes(&merkle_data)
233            .ok_or("Invalid merkle.bin format")?;
234        Ok(tree.root)
235    }
236}
237
238// ─── Windows Named Pipe Implementation ──────────────────
239
240#[cfg(windows)]
241mod windows_pipe {
242    use std::ffi::CString;
243    use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
244    use windows_sys::Win32::Storage::FileSystem::{ReadFile, WriteFile, PIPE_ACCESS_DUPLEX};
245    use windows_sys::Win32::System::Pipes::{
246        ConnectNamedPipe, CreateNamedPipeA, DisconnectNamedPipe, PIPE_READMODE_MESSAGE,
247        PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_MESSAGE, PIPE_WAIT,
248    };
249
250    pub struct NamedPipeListener {
251        handle: HANDLE,
252    }
253
254    impl NamedPipeListener {
255        pub fn new(name: &str) -> Result<Self, String> {
256            // Ensure pipe name uses Windows named pipe format
257            let pipe_name = if name.starts_with("\\\\.\\pipe\\") {
258                name.to_string()
259            } else {
260                format!("\\\\.\\pipe\\{}", name)
261            };
262            let c_name =
263                CString::new(pipe_name).map_err(|e| format!("Invalid pipe name: {}", e))?;
264
265            let handle = unsafe {
266                CreateNamedPipeA(
267                    c_name.as_ptr() as *const u8,
268                    PIPE_ACCESS_DUPLEX,
269                    PIPE_TYPE_MESSAGE
270                        | PIPE_READMODE_MESSAGE
271                        | PIPE_WAIT
272                        | PIPE_REJECT_REMOTE_CLIENTS,
273                    1,                // max instances
274                    256,              // out buffer
275                    256,              // in buffer
276                    0,                // default timeout
277                    std::ptr::null(), // no security attrs
278                )
279            };
280
281            if handle == INVALID_HANDLE_VALUE {
282                return Err("Failed to create named pipe".to_string());
283            }
284
285            Ok(Self { handle })
286        }
287
288        pub fn accept(&self) -> Result<NamedPipeConnection, String> {
289            let connected = unsafe { ConnectNamedPipe(self.handle, std::ptr::null_mut()) };
290            if connected == 0 {
291                // Client may already be connected; GetLastError would tell, but we proceed
292            }
293            Ok(NamedPipeConnection {
294                handle: self.handle,
295            })
296        }
297    }
298
299    impl Drop for NamedPipeListener {
300        fn drop(&mut self) {
301            unsafe {
302                CloseHandle(self.handle);
303            }
304        }
305    }
306
307    pub struct NamedPipeConnection {
308        handle: HANDLE,
309    }
310
311    impl NamedPipeConnection {
312        pub fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), String> {
313            let mut total = 0usize;
314            while total < buf.len() {
315                let mut read = 0u32;
316                let ok = unsafe {
317                    ReadFile(
318                        self.handle,
319                        buf.as_mut_ptr().add(total),
320                        (buf.len() - total) as u32,
321                        &mut read,
322                        std::ptr::null_mut(),
323                    )
324                };
325                if ok == 0 {
326                    return Err("ReadFile failed".to_string());
327                }
328                if read == 0 {
329                    return Err("Pipe closed".to_string());
330                }
331                total += read as usize;
332            }
333            Ok(())
334        }
335
336        pub fn write_all(&mut self, buf: &[u8]) -> Result<(), String> {
337            let mut total = 0usize;
338            while total < buf.len() {
339                let mut written = 0u32;
340                let ok = unsafe {
341                    WriteFile(
342                        self.handle,
343                        buf.as_ptr().add(total),
344                        (buf.len() - total) as u32,
345                        &mut written,
346                        std::ptr::null_mut(),
347                    )
348                };
349                if ok == 0 {
350                    return Err("WriteFile failed".to_string());
351                }
352                total += written as usize;
353            }
354            Ok(())
355        }
356    }
357
358    impl Drop for NamedPipeConnection {
359        fn drop(&mut self) {
360            unsafe {
361                DisconnectNamedPipe(self.handle);
362            }
363        }
364    }
365} // ─── Cross-Platform Socket Listener ─────────────────────
366
367/// Platform-specific socket listener for AI communication.
368pub struct AISocketListener {
369    #[cfg(unix)]
370    listener: std::os::unix::net::UnixListener,
371    #[cfg(windows)]
372    listener: windows_pipe::NamedPipeListener,
373}
374
375impl AISocketListener {
376    /// Create a new socket listener at the given path.
377    #[cfg(unix)]
378    pub fn new(path: &str) -> Result<Self, String> {
379        // Remove existing socket if it exists
380        let _ = std::fs::remove_file(path);
381
382        let listener = std::os::unix::net::UnixListener::bind(path)
383            .map_err(|e| format!("Failed to bind Unix socket: {}", e))?;
384
385        Ok(Self { listener })
386    }
387
388    #[cfg(windows)]
389    pub fn new(path: &str) -> Result<Self, String> {
390        let listener = windows_pipe::NamedPipeListener::new(path)
391            .map_err(|e| format!("Failed to create named pipe listener: {}", e))?;
392        Ok(Self { listener })
393    }
394
395    /// Accept incoming connections and handle commands.
396    pub fn listen(&self, adapter: &mut AIAdapter) -> Result<(), String> {
397        #[cfg(windows)]
398        let _ = adapter;
399
400        #[cfg(unix)]
401        {
402            for stream in self.listener.incoming() {
403                let mut stream = match stream {
404                    Ok(s) => s,
405                    Err(e) => {
406                        eprintln!("Connection error: {}", e);
407                        continue;
408                    }
409                };
410
411                // Read 256-byte command
412                let mut buffer = [0u8; 256];
413                if let Err(e) = stream.read_exact(&mut buffer) {
414                    eprintln!("Read error: {}", e);
415                    continue;
416                }
417
418                // Zero-copy cast to AICommand
419                let cmd: AICommand = unsafe { std::mem::transmute(buffer) };
420
421                // Process command
422                match adapter.process_command(cmd) {
423                    Ok(response) => {
424                        // Send back 256-byte response
425                        let response_bytes: [u8; 256] = unsafe { std::mem::transmute(response) };
426                        if let Err(e) = stream.write_all(&response_bytes) {
427                            eprintln!("Write error: {}", e);
428                        }
429                    }
430                    Err(e) => {
431                        eprintln!("Command processing error: {}", e);
432                        // Send error response
433                        let error_response = AICommand {
434                            op_code: 255, // Error
435                            ..AICommand::default()
436                        };
437                        let response_bytes: [u8; 256] =
438                            unsafe { std::mem::transmute(error_response) };
439                        let _ = stream.write_all(&response_bytes);
440                    }
441                }
442            }
443        }
444
445        #[cfg(windows)]
446        {
447            loop {
448                let mut conn = match self.listener.accept() {
449                    Ok(c) => c,
450                    Err(e) => {
451                        eprintln!("Connection error: {}", e);
452                        continue;
453                    }
454                };
455
456                let mut buffer = [0u8; 256];
457                if let Err(e) = conn.read_exact(&mut buffer) {
458                    eprintln!("Read error: {}", e);
459                    continue;
460                }
461
462                let cmd: AICommand = unsafe { std::mem::transmute(buffer) };
463
464                match adapter.process_command(cmd) {
465                    Ok(response) => {
466                        let response_bytes: [u8; 256] = unsafe { std::mem::transmute(response) };
467                        if let Err(e) = conn.write_all(&response_bytes) {
468                            eprintln!("Write error: {}", e);
469                        }
470                    }
471                    Err(e) => {
472                        eprintln!("Command processing error: {}", e);
473                        let error_response = AICommand {
474                            op_code: 255,
475                            ..AICommand::default()
476                        };
477                        let response_bytes: [u8; 256] =
478                            unsafe { std::mem::transmute(error_response) };
479                        let _ = conn.write_all(&response_bytes);
480                    }
481                }
482            }
483        }
484
485        #[cfg(not(windows))]
486        Ok(())
487    }
488}
489
490// ─── Tests ──────────────────────────────────────────────
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use crate::config::Config;
496
497    fn test_config() -> Config {
498        let mut config = Config::default();
499        config.paths.output_dir = "test_output".to_string();
500        config.paths.layers_dir = "test_layers".to_string();
501        config
502    }
503
504    #[test]
505    fn test_ai_command_creation() {
506        let read_cmd = AICommand::read(1, 42);
507        assert_eq!(read_cmd.op_code, 0);
508        assert_eq!(read_cmd.layer, 1);
509        assert_eq!(read_cmd.block_id, 42);
510
511        let write_cmd = AICommand::write(2, 0, b"Hello AI");
512        assert_eq!(write_cmd.op_code, 1);
513        assert_eq!(write_cmd.layer, 2);
514        assert_eq!(write_cmd.payload[0], b'H');
515    }
516
517    #[test]
518    fn test_ai_adapter_creation() {
519        // This test requires actual built memory files
520        // For now, just test that the struct can be created
521        let _config = test_config();
522        // Note: This will fail without actual memory files
523        // let adapter = AIAdapter::new(config);
524        // assert!(adapter.is_ok());
525    }
526
527    #[test]
528    fn update_merkle_tree_is_honest_about_dirty_blocks() {
529        let tmp = tempfile::tempdir().unwrap();
530        let mut config = Config::default();
531        config.paths.layers_dir = tmp.path().join("layers").to_string_lossy().to_string();
532        config.paths.output_dir = tmp.path().join("data").to_string_lossy().to_string();
533        config.memory_layers.layers = vec!["long_term".to_string()];
534        config.embedding.provider = "none".to_string();
535        std::fs::create_dir_all(&config.paths.layers_dir).unwrap();
536        crate::build::build(&config, true, true).unwrap();
537
538        let mut adapter = AIAdapter::new(config).unwrap();
539
540        // Clean state: no pending work, Ok.
541        assert!(adapter.update_merkle_tree().is_ok());
542
543        // Dirty state: must fail loudly and must NOT clear the dirty set.
544        adapter.dirty_blocks.insert(42);
545        let err = adapter.update_merkle_tree().unwrap_err();
546        assert!(err.contains("not implemented"), "err: {err}");
547        assert!(
548            adapter.dirty_blocks.contains(&42),
549            "dirty set must survive a failed update"
550        );
551    }
552}