Skip to main content

microscope_memory/
mcp.rs

1//! Native MCP (Model Context Protocol) server for Microscope Memory.
2//!
3//! Implements JSON-RPC 2.0 over stdio with the MCP tool-calling protocol.
4//! Replaces the Python MCP server with a native Rust implementation.
5//!
6//! Not available on WASM targets (no stdio).
7
8use crate::config::Config;
9use crate::reader::MicroscopeReader;
10use crate::{
11    read_append_log, store_memory, store_memory_with_embedding, store_memory_with_emotion,
12    LAYER_NAMES,
13};
14use microscope_hooks::*;
15use serde_json::{json, Value};
16use std::collections::{HashMap, HashSet};
17use std::io::{self, BufRead, BufReader, Read, Write};
18use std::path::Path;
19
20/// Maximum framed MCP message body (16 MiB). Prevents unbounded allocation
21/// from a malicious or broken `Content-Length` header.
22const MAX_MESSAGE_LEN: usize = 16 * 1024 * 1024; // 16 MiB
23
24/// Maximum MCP header section size (64 KiB). Prevents unbounded memory growth
25/// from a client that never terminates its header lines.
26const MAX_HEADER_LEN: usize = 64 * 1024; // 64 KiB
27
28/// Run the MCP server on stdio (blocking).
29pub fn run(config: Config) {
30    // Force UTF-8 console on Windows (CP_UTF8 = 65001)
31    #[cfg(windows)]
32    unsafe {
33        windows_sys::Win32::System::Console::SetConsoleCP(65001);
34        windows_sys::Win32::System::Console::SetConsoleOutputCP(65001);
35    }
36
37    // Initialize hook manager
38    let hook_config = if config.hooks.read_only {
39        HookConfig::read_only()
40    } else if config.hooks.write_enabled {
41        HookConfig::full()
42    } else {
43        HookConfig::default()
44    };
45    let hook_manager = HookManager::new(hook_config);
46    let mut session_started = false;
47
48    eprintln!(
49        "[hooks] manager initialized (read_only={}, write_enabled={})",
50        config.hooks.read_only, config.hooks.write_enabled
51    );
52
53    let stdin = io::stdin();
54    let stdout = io::stdout();
55    let mut reader = BufReader::new(stdin.lock());
56    let mut stdout = stdout.lock();
57
58    loop {
59        let incoming = match read_message(&mut reader) {
60            Ok(Some(msg)) => msg,
61            Ok(None) => break,
62            Err(e) => {
63                let err = json!({
64                    "jsonrpc": "2.0",
65                    "id": null,
66                    "error": { "code": -32700, "message": format!("Read error: {}", e) }
67                });
68                let _ = write_message(&mut stdout, &err, true);
69                continue;
70            }
71        };
72
73        let request = match serde_json::from_str::<Value>(&incoming.payload) {
74            Ok(v) => v,
75            Err(e) => {
76                let err = json!({
77                    "jsonrpc": "2.0",
78                    "id": null,
79                    "error": { "code": -32700, "message": format!("Parse error: {}", e) }
80                });
81                let _ = write_message(&mut stdout, &err, incoming.framed);
82                continue;
83            }
84        };
85
86        let id = request.get("id").cloned().unwrap_or(Value::Null);
87        let method = request.get("method").and_then(|m| m.as_str()).unwrap_or("");
88
89        // Run on_session_start on first initialize
90        if method == "initialize" && !session_started {
91            let ctx = HookContext::new(HookEvent::SessionStart);
92            let _ = hook_manager.execute(HookEvent::SessionStart, ctx);
93            session_started = true;
94            eprintln!("[hooks] session started");
95        }
96
97        let response = match method {
98            "initialize" => handle_initialize(&id),
99            "initialized" => continue,
100            "tools/list" => handle_tools_list(&id),
101            "tools/call" => handle_tools_call_with_hooks(&id, &request, &config, &hook_manager),
102            "ping" => json!({ "jsonrpc": "2.0", "id": id, "result": {} }),
103            "notifications/cancelled" | "notifications/initialized" => continue,
104            _ => json!({
105                "jsonrpc": "2.0",
106                "id": id,
107                "error": { "code": -32601, "message": format!("Method not found: {}", method) }
108            }),
109        };
110
111        let _ = write_message(&mut stdout, &response, incoming.framed);
112    }
113}
114
115fn handle_tools_call_with_hooks(
116    id: &Value,
117    request: &Value,
118    config: &Config,
119    hook_manager: &HookManager,
120) -> Value {
121    let params = request.get("params").cloned().unwrap_or(json!({}));
122    let tool_name = params
123        .get("name")
124        .and_then(|n: &Value| n.as_str())
125        .unwrap_or("");
126    let args = params.get("arguments").cloned().unwrap_or(json!({}));
127
128    // Run before_tool_call hook
129    let before_ctx = HookContext::new(HookEvent::BeforeToolCall).with_tool(tool_name, args.clone());
130    let _ = hook_manager.execute(HookEvent::BeforeToolCall, before_ctx);
131
132    // Execute the tool
133    let result = match tool_name {
134        "memory_status" => tool_status(config),
135        "memory_store" => tool_store(config, &args),
136        "memory_recall" => tool_recall(config, &args),
137        "memory_find" => tool_find(config, &args),
138        "memory_mql_query" => tool_mql_query(config, &args),
139        "memory_build" => tool_build(config, &args),
140        "memory_look" => tool_look(config, &args),
141        "memory_session_log" => tool_session_log(config, &args),
142        "memory_consolidate" => tool_consolidate(config, &args),
143        "memory_dream" => tool_dream(config, &args),
144        "memory_session_context" => tool_session_context(config, &args),
145        "memory_ping" => tool_ping(config, &args),
146        "memory_auto_context" => tool_auto_context(config, &args),
147        "memory_timeline" => tool_timeline(config, &args),
148        "memory_loops" => tool_loops(config, &args),
149        "memory_resolve_loop" => tool_resolve_loop(config, &args),
150        "memory_radial" => tool_radial(config, &args),
151        "memory_soft" => tool_soft(config, &args),
152        "memory_think" => tool_think(config, &args),
153        "memory_hebbian" => tool_hebbian(config, &args),
154        "memory_hottest" => tool_hottest(config, &args),
155        "memory_archetypes" => tool_archetypes(config, &args),
156        "memory_patterns" => tool_patterns(config, &args),
157        "memory_attention" => tool_attention(config, &args),
158        "memory_introspect" => tool_introspect(config, &args),
159        "memory_self_model" => tool_self_model(config, &args),
160        "memory_curiosity" => tool_curiosity(config, &args),
161        "memory_monologue" => tool_monologue(config, &args),
162        "memory_stories" => tool_stories(config, &args),
163        "memory_daydream" => tool_daydream(config, &args),
164        "memory_hyperfocus" => tool_hyperfocus(config, &args),
165        "memory_emotional_field" => tool_emotional_field(config, &args),
166        "memory_embed" => tool_embed(config, &args),
167        "memory_similar" => tool_similar(config, &args),
168        "memory_links" => tool_links(config, &args),
169        "memory_fingerprint" => tool_fingerprint(config, &args),
170        "memory_dream_log" => tool_dream_log(config, &args),
171        "memory_resonance" => tool_resonance(config, &args),
172        "memory_mirror" => tool_mirror(config, &args),
173        "memory_predictions" => tool_predictions(config, &args),
174        "memory_paths" => tool_paths(config, &args),
175        "memory_temporal_patterns" => tool_temporal_patterns(config, &args),
176        "memory_modalities" => tool_modalities(config, &args),
177        "memory_doctor" => tool_doctor(config, &args),
178        "memory_rebuild" => tool_rebuild(config, &args),
179        "memory_store_data" => tool_store_data(config, &args),
180        "memory_resonant" => tool_resonant(config, &args),
181        "memory_autonomous" => tool_autonomous(config, &args),
182        "memory_consciousness" => tool_consciousness(config, &args),
183        "memory_evidence_show" => tool_evidence_show(config, &args),
184        "memory_evidence_link" => tool_evidence_link(config, &args),
185        "memory_evidence_refute" => tool_evidence_refute(config, &args),
186        "memory_evidence_audit" => tool_evidence_audit(config, &args),
187        "memory_evidence_gates" => tool_evidence_gates(config, &args),
188        _ => Err(format!("Unknown tool: {}", tool_name)),
189    };
190
191    match result {
192        Ok(content) => {
193            // Run after_tool_call hook
194            let after_ctx = HookContext::new(HookEvent::AfterToolCall)
195                .with_tool(tool_name, args)
196                .with_response(&content);
197            let hook_ctx = hook_manager.execute(HookEvent::AfterToolCall, after_ctx);
198            if !hook_ctx.memory_candidates.is_empty() {
199                eprintln!(
200                    "[hooks] after_tool_call: {} candidate(s) from '{}'",
201                    hook_ctx.memory_candidates.len(),
202                    tool_name
203                );
204            }
205
206            json!({
207                "jsonrpc": "2.0",
208                "id": id,
209                "result": {
210                    "content": [{ "type": "text", "text": content }]
211                }
212            })
213        }
214        Err(e) => {
215            // Run on_error hook
216            let err_ctx = HookContext::new(HookEvent::Error)
217                .with_tool(tool_name, args)
218                .with_error(&e, "TOOL_ERROR");
219            let hook_ctx = hook_manager.execute(HookEvent::Error, err_ctx);
220            if !hook_ctx.memory_candidates.is_empty() {
221                eprintln!("[hooks] on_error: stored error trace for '{}'", tool_name);
222            }
223
224            json!({
225                "jsonrpc": "2.0",
226                "id": id,
227                "result": {
228                    "content": [{ "type": "text", "text": format!("Error: {}", e) }],
229                    "isError": true
230                }
231            })
232        }
233    }
234}
235
236struct IncomingMessage {
237    payload: String,
238    framed: bool,
239}
240
241fn read_message<R: BufRead + Read>(reader: &mut R) -> io::Result<Option<IncomingMessage>> {
242    // Read first non-empty line as raw bytes (Windows console may not be UTF-8)
243    let first_line = loop {
244        let mut buf = Vec::new();
245        let mut byte = [0u8; 1];
246        loop {
247            let n = reader.read(&mut byte)?;
248            if n == 0 {
249                return Ok(None);
250            }
251            if byte[0] == b'\n' {
252                break;
253            }
254            if byte[0] != b'\r' {
255                buf.push(byte[0]);
256                if buf.len() > MAX_HEADER_LEN {
257                    return Err(io::Error::new(
258                        io::ErrorKind::InvalidData,
259                        format!("MCP header line exceeds maximum allowed {}", MAX_HEADER_LEN),
260                    ));
261                }
262            }
263        }
264        if !buf.is_empty() {
265            break String::from_utf8(buf)
266                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8"))?;
267        }
268    };
269
270    if first_line.starts_with('{') {
271        return Ok(Some(IncomingMessage {
272            payload: first_line,
273            framed: false,
274        }));
275    }
276
277    let mut content_length: Option<usize> = None;
278    parse_header_line(&first_line, &mut content_length);
279
280    let mut header_line = String::new();
281    loop {
282        header_line.clear();
283        let bytes = reader.read_line(&mut header_line)?;
284        if bytes == 0 {
285            break;
286        }
287        if header_line.len() > MAX_HEADER_LEN {
288            return Err(io::Error::new(
289                io::ErrorKind::InvalidData,
290                format!("MCP header line exceeds maximum allowed {}", MAX_HEADER_LEN),
291            ));
292        }
293        if header_line == "\r\n" || header_line == "\n" {
294            break;
295        }
296        parse_header_line(&header_line, &mut content_length);
297    }
298
299    let len = content_length.ok_or_else(|| {
300        io::Error::new(
301            io::ErrorKind::InvalidData,
302            "Missing Content-Length header in framed MCP message",
303        )
304    })?;
305
306    if len > MAX_MESSAGE_LEN {
307        return Err(io::Error::new(
308            io::ErrorKind::InvalidData,
309            format!(
310                "Content-Length {} exceeds maximum allowed {}",
311                len, MAX_MESSAGE_LEN
312            ),
313        ));
314    }
315
316    let mut body = vec![0u8; len];
317    reader.read_exact(&mut body)?;
318    let payload = String::from_utf8(body)
319        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 JSON payload"))?;
320
321    Ok(Some(IncomingMessage {
322        payload,
323        framed: true,
324    }))
325}
326
327fn parse_header_line(line: &str, content_length: &mut Option<usize>) {
328    let lower = line.to_ascii_lowercase();
329    if lower.starts_with("content-length:") {
330        let value = line
331            .split_once(':')
332            .map(|(_, v)| v.trim())
333            .and_then(|v| v.parse::<usize>().ok());
334        if let Some(v) = value {
335            *content_length = Some(v);
336        }
337    }
338}
339
340fn write_message<W: Write>(writer: &mut W, response: &Value, framed: bool) -> io::Result<()> {
341    if framed {
342        let payload = response.to_string();
343        write!(
344            writer,
345            "Content-Length: {}\r\n\r\n{}",
346            payload.len(),
347            payload
348        )?;
349    } else {
350        writeln!(writer, "{}", response)?;
351    }
352    writer.flush()
353}
354
355fn handle_initialize(id: &Value) -> Value {
356    json!({
357        "jsonrpc": "2.0",
358        "id": id,
359        "result": {
360            "protocolVersion": "2024-11-05",
361            "capabilities": {
362                "tools": {}
363            },
364            "serverInfo": {
365                "name": "microscope-memory",
366                "version": env!("CARGO_PKG_VERSION")
367            }
368        }
369    })
370}
371
372fn handle_tools_list(id: &Value) -> Value {
373    json!({
374        "jsonrpc": "2.0",
375        "id": id,
376        "result": {
377            "tools": [
378                {
379                    "name": "memory_status",
380                    "description": "Get microscope memory index status: block count, depths, append log size",
381                    "inputSchema": {
382                        "type": "object",
383                        "properties": {},
384                        "required": []
385                    }
386                },
387                {
388                    "name": "memory_store",
389                    "description": "Store a new memory into the microscope append log",
390                    "inputSchema": {
391                        "type": "object",
392                        "properties": {
393                            "text": { "type": "string", "description": "Memory text to store" },
394                            "layer": { "type": "string", "description": "Memory layer (long_term, short_term, session, associative, emotional, relational, reflections, echo_cache)", "default": "long_term" },
395                            "importance": { "type": "integer", "description": "Importance level 1-10", "default": 5 },
396                            "emotion": { "type": "array", "items": { "type": "number" }, "minItems": 21, "maxItems": 21, "description": "21D emotion vector: [joy, sadness, anger, fear, surprise, disgust, trust, anticipation, love, gratitude, curiosity, awe, confusion, anxiety, serenity, hope, pride, shame, guilt, empathy, excitement]" }
397                        },
398                        "required": ["text"]
399                    }
400                },
401                {
402                    "name": "memory_recall",
403                    "description": "Natural language recall with auto-zoom — searches both main index and append log. Optional emotion vector biases results toward emotionally similar memories.",
404                    "inputSchema": {
405                        "type": "object",
406                        "properties": {
407                            "query": { "type": "string", "description": "Natural language query" },
408                            "k": { "type": "integer", "description": "Max results to return", "default": 10 },
409                            "emotion": { "type": "array", "items": { "type": "number" }, "minItems": 21, "maxItems": 21, "description": "21D emotion vector for emotional recall: [joy, sadness, anger, fear, surprise, disgust, trust, anticipation, love, gratitude, curiosity, awe, confusion, anxiety, serenity, hope, pride, shame, guilt, empathy, excitement]" }
410                        },
411                        "required": ["query"]
412                    }
413                },
414                {
415                    "name": "memory_find",
416                    "description": "Brute-force text search across all depths",
417                    "inputSchema": {
418                        "type": "object",
419                        "properties": {
420                            "query": { "type": "string", "description": "Text to search for" },
421                            "k": { "type": "integer", "description": "Max results", "default": 10 }
422                        },
423                        "required": ["query"]
424                    }
425                },
426                {
427                    "name": "memory_mql_query",
428                    "description": "Execute an MQL (Microscope Query Language) query with filters: layer, depth, spatial, boolean",
429                    "inputSchema": {
430                        "type": "object",
431                        "properties": {
432                            "mql": { "type": "string", "description": "MQL expression, e.g. 'layer:long_term depth:2..5 \"memory\"'" }
433                        },
434                        "required": ["mql"]
435                    }
436                },
437                {
438                    "name": "memory_build",
439                    "description": "Rebuild the microscope index from layer source files (merges append log)",
440                    "inputSchema": {
441                        "type": "object",
442                        "properties": {
443                            "force": { "type": "boolean", "description": "Force rebuild even if unchanged", "default": false }
444                        },
445                        "required": []
446                    }
447                },
448                {
449                    "name": "memory_session_log",
450                    "description": "Read last N interactions from the session memory layer (no index needed, reads layers/session.txt directly)",
451                    "inputSchema": {
452                        "type": "object",
453                        "properties": {
454                            "n": { "type": "integer", "description": "Number of recent interactions to return", "default": 50 }
455                        },
456                        "required": []
457                    }
458                },
459                {
460                    "name": "memory_consolidate",
461                    "description": "Consolidate recent session entries into long-term memory summaries. Groups entries by session ID and creates short summaries.",
462                    "inputSchema": {
463                        "type": "object",
464                        "properties": {},
465                        "required": []
466                    }
467                },
468                {
469                    "name": "memory_dream",
470                    "description": "Dream consolidation — offline memory replay that strengthens important pathways and prunes weak ones (biological sleep analog).",
471                    "inputSchema": {
472                        "type": "object",
473                        "properties": {},
474                        "required": []
475                    }
476                },
477                {
478                    "name": "memory_look",
479                    "description": "Manual spatial look at specific 3D coordinates and zoom level",
480                    "inputSchema": {
481                        "type": "object",
482                        "properties": {
483                            "x": { "type": "number", "description": "X coordinate (0.0-1.0)" },
484                            "y": { "type": "number", "description": "Y coordinate (0.0-1.0)" },
485                            "z": { "type": "number", "description": "Z coordinate (0.0-1.0)" },
486                            "zoom": { "type": "integer", "description": "Depth level (0-8)" },
487                            "k": { "type": "integer", "description": "Max results", "default": 10 }
488                        },
489                        "required": ["x", "y", "z", "zoom"]
490                    }
491                },
492                {
493                    "name": "memory_session_context",
494                    "description": "Store conversation context automatically — saves the current session for later recall",
495                    "inputSchema": {
496                        "type": "object",
497                        "properties": {
498                            "context": { "type": "string", "description": "Conversation context to store" },
499                            "summary": { "type": "string", "description": "Optional summary for long-term storage" }
500                        },
501                        "required": ["context"]
502                    }
503                },
504                {
505                    "name": "memory_ping",
506                    "description": "Quick auto-context — fast recall without full indexing. Use this before answering to get relevant context automatically.",
507                    "inputSchema": {
508                        "type": "object",
509                        "properties": {
510                            "query": { "type": "string", "description": "What to get context for" }
511                        },
512                        "required": ["query"]
513                    }
514                },
515                {
516                    "name": "memory_auto_context",
517                    "description": "Full system state snapshot: timeline (last session), open loops, block count, depth distribution, append log size",
518                    "inputSchema": {
519                        "type": "object",
520                        "properties": {
521                            "compact": { "type": "boolean", "description": "Compact mode (no box-drawing)", "default": false }
522                        },
523                        "required": []
524                    }
525                },
526                {
527                    "name": "memory_timeline",
528                    "description": "Show chronological timeline of stored memories by time window",
529                    "inputSchema": {
530                        "type": "object",
531                        "properties": {
532                            "window": { "type": "string", "description": "Time window: today, yesterday, last_N_days, since:YYYY-MM-DD, last_session, all", "default": "last_session" },
533                            "k": { "type": "integer", "description": "Max entries to return", "default": 20 }
534                        },
535                        "required": []
536                    }
537                },
538                {
539                    "name": "memory_loops",
540                    "description": "List currently open loops (unresolved tasks/thoughts)",
541                    "inputSchema": {
542                        "type": "object",
543                        "properties": {
544                            "k": { "type": "integer", "description": "Max loops to return", "default": 20 }
545                        },
546                        "required": []
547                    }
548                },
549                {
550                    "name": "memory_resolve_loop",
551                    "description": "Mark an open loop as resolved",
552                    "inputSchema": {
553                        "type": "object",
554                        "properties": {
555                            "id": { "type": "integer", "description": "Loop ID to resolve" }
556                        },
557                        "required": ["id"]
558                    }
559                },
560                {
561                    "name": "memory_radial",
562                    "description": "Radial spatial search — find blocks within a radius at a given depth",
563                    "inputSchema": {
564                        "type": "object",
565                        "properties": {
566                            "x": { "type": "number", "description": "X coordinate (0.0-1.0)" },
567                            "y": { "type": "number", "description": "Y coordinate (0.0-1.0)" },
568                            "z": { "type": "number", "description": "Z coordinate (0.0-1.0)" },
569                            "depth": { "type": "integer", "description": "Depth level (0-8)" },
570                            "radius": { "type": "number", "description": "Search radius", "default": 0.1 },
571                            "k": { "type": "integer", "description": "Max results", "default": 10 }
572                        },
573                        "required": ["x", "y", "z", "depth"]
574                    }
575                },
576                {
577                    "name": "memory_soft",
578                    "description": "4D soft zoom search — smooth multi-depth spatial query",
579                    "inputSchema": {
580                        "type": "object",
581                        "properties": {
582                            "x": { "type": "number", "description": "X coordinate (0.0-1.0)" },
583                            "y": { "type": "number", "description": "Y coordinate (0.0-1.0)" },
584                            "z": { "type": "number", "description": "Z coordinate (0.0-1.0)" },
585                            "zoom": { "type": "integer", "description": "Zoom level (0-8)" },
586                            "k": { "type": "integer", "description": "Max results", "default": 10 }
587                        },
588                        "required": ["x", "y", "z", "zoom"]
589                    }
590                },
591                {
592                    "name": "memory_think",
593                    "description": "Sequential thinking chain — brainstorm a topic through multi-step memory traversal",
594                    "inputSchema": {
595                        "type": "object",
596                        "properties": {
597                            "query": { "type": "string", "description": "Topic to think about" },
598                            "max_steps": { "type": "integer", "description": "Maximum thinking steps", "default": 5 }
599                        },
600                        "required": ["query"]
601                    }
602                },
603                {
604                    "name": "memory_hebbian",
605                    "description": "Show Hebbian learning state: activations, co-activations, energy, drifted blocks",
606                    "inputSchema": {
607                        "type": "object",
608                        "properties": {},
609                        "required": []
610                    }
611                },
612                {
613                    "name": "memory_hottest",
614                    "description": "Show hottest (most frequently/recently activated) blocks",
615                    "inputSchema": {
616                        "type": "object",
617                        "properties": {
618                            "k": { "type": "integer", "description": "Number of hottest blocks", "default": 10 }
619                        },
620                        "required": []
621                    }
622                },
623                {
624                    "name": "memory_archetypes",
625                    "description": "Show emerged archetypes — crystallized activation patterns",
626                    "inputSchema": {
627                        "type": "object",
628                        "properties": {},
629                        "required": []
630                    }
631                },
632                {
633                    "name": "memory_patterns",
634                    "description": "Show thought graph patterns — crystallized recall sequences",
635                    "inputSchema": {
636                        "type": "object",
637                        "properties": {
638                            "k": { "type": "integer", "description": "Number of top patterns", "default": 10 }
639                        },
640                        "required": []
641                    }
642                },
643                {
644                    "name": "memory_attention",
645                    "description": "Show attention mechanism state: learned layer weights, quality history",
646                    "inputSchema": {
647                        "type": "object",
648                        "properties": {},
649                        "required": []
650                    }
651                },
652                {
653                    "name": "memory_introspect",
654                    "description": "Self-reflection — the system thinks about its own cognitive state",
655                    "inputSchema": {
656                        "type": "object",
657                        "properties": {},
658                        "required": []
659                    }
660                },
661                {
662                    "name": "memory_self_model",
663                    "description": "Show the system's self-model snapshot",
664                    "inputSchema": {
665                        "type": "object",
666                        "properties": {},
667                        "required": []
668                    }
669                },
670                {
671                    "name": "memory_curiosity",
672                    "description": "Show what the system is curious about — generated curiosity queries",
673                    "inputSchema": {
674                        "type": "object",
675                        "properties": {},
676                        "required": []
677                    }
678                },
679                {
680                    "name": "memory_monologue",
681                    "description": "Generate an inner monologue — the system thinking to itself",
682                    "inputSchema": {
683                        "type": "object",
684                        "properties": {},
685                        "required": []
686                    }
687                },
688                {
689                    "name": "memory_stories",
690                    "description": "Show narrative memory episodes — story arcs built from recalls",
691                    "inputSchema": {
692                        "type": "object",
693                        "properties": {
694                            "k": { "type": "integer", "description": "Number of recent episodes", "default": 5 }
695                        },
696                        "required": []
697                    }
698                },
699                {
700                    "name": "memory_daydream",
701                    "description": "Associative drift (mind wandering) — start from a seed and drift through related memories",
702                    "inputSchema": {
703                        "type": "object",
704                        "properties": {
705                            "seed": { "type": "string", "description": "Seed text to start from (default: last narrative)", "default": "" },
706                            "steps": { "type": "integer", "description": "Number of drift steps", "default": 3 }
707                        },
708                        "required": []
709                    }
710                },
711                {
712                    "name": "memory_hyperfocus",
713                    "description": "Enter deep concentration mode on a specific topic",
714                    "inputSchema": {
715                        "type": "object",
716                        "properties": {
717                            "target": { "type": "string", "description": "Target topic to focus on" },
718                            "focus_type": { "type": "string", "description": "Focus type: planning, problem_solving, creative, research", "default": "research" }
719                        },
720                        "required": ["target"]
721                    }
722                },
723                {
724                    "name": "memory_emotional_field",
725                    "description": "Show emotional contagion state: local + remote emotional fields, total energy",
726                    "inputSchema": {
727                        "type": "object",
728                        "properties": {},
729                        "required": []
730                    }
731                },
732                {
733                    "name": "memory_embed",
734                    "description": "Semantic search using embeddings — find conceptually similar memories",
735                    "inputSchema": {
736                        "type": "object",
737                        "properties": {
738                            "query": { "type": "string", "description": "Semantic query text" },
739                            "k": { "type": "integer", "description": "Max results", "default": 10 },
740                            "metric": { "type": "string", "description": "Distance metric: cosine, dot, l2", "default": "cosine" }
741                        },
742                        "required": ["query"]
743                    }
744                },
745                {
746                    "name": "memory_similar",
747                    "description": "Find structurally similar blocks to a given text using fingerprint analysis",
748                    "inputSchema": {
749                        "type": "object",
750                        "properties": {
751                            "text": { "type": "string", "description": "Text to find structural matches for" },
752                            "k": { "type": "integer", "description": "Max results", "default": 5 }
753                        },
754                        "required": ["text"]
755                    }
756                },
757                {
758                    "name": "memory_links",
759                    "description": "Show structural wormhole links for a specific block index",
760                    "inputSchema": {
761                        "type": "object",
762                        "properties": {
763                            "block_index": { "type": "integer", "description": "Block index to inspect" }
764                        },
765                        "required": ["block_index"]
766                    }
767                },
768                {
769                    "name": "memory_fingerprint",
770                    "description": "Build structural fingerprints and wormhole links from the current index",
771                    "inputSchema": {
772                        "type": "object",
773                        "properties": {},
774                        "required": []
775                    }
776                },
777                {
778                    "name": "memory_dream_log",
779                    "description": "Show dream consolidation history — past offline memory replay cycles",
780                    "inputSchema": {
781                        "type": "object",
782                        "properties": {
783                            "k": { "type": "integer", "description": "Number of recent dream cycles", "default": 10 }
784                        },
785                        "required": []
786                    }
787                },
788                {
789                    "name": "memory_resonance",
790                    "description": "Show resonance protocol state: pulses, field energy, pending integrations",
791                    "inputSchema": {
792                        "type": "object",
793                        "properties": {},
794                        "required": []
795                    }
796                },
797                {
798                    "name": "memory_mirror",
799                    "description": "Show mirror neuron state: resonance echoes, boosted blocks, similarity stats",
800                    "inputSchema": {
801                        "type": "object",
802                        "properties": {},
803                        "required": []
804                    }
805                },
806                {
807                    "name": "memory_predictions",
808                    "description": "Show predictive cache stats: hit rate, active predictions, confidence",
809                    "inputSchema": {
810                        "type": "object",
811                        "properties": {},
812                        "required": []
813                    }
814                },
815                {
816                    "name": "memory_paths",
817                    "description": "Show recent thought paths — recall sequences grouped by session",
818                    "inputSchema": {
819                        "type": "object",
820                        "properties": {
821                            "sessions": { "type": "integer", "description": "Number of recent sessions", "default": 5 }
822                        },
823                        "required": []
824                    }
825                },
826                {
827                    "name": "memory_temporal_patterns",
828                    "description": "Show temporal archetype patterns — time-of-day activation profiles",
829                    "inputSchema": {
830                        "type": "object",
831                        "properties": {},
832                        "required": []
833                    }
834                },
835                {
836                    "name": "memory_modalities",
837                    "description": "Show multimodal index statistics",
838                    "inputSchema": {
839                        "type": "object",
840                        "properties": {},
841                        "required": []
842                    }
843                },
844                {
845                    "name": "memory_doctor",
846                    "description": "Run integrity diagnostics and attempt automatic repair (crash recovery)",
847                    "inputSchema": {
848                        "type": "object",
849                        "properties": {
850                            "fix": { "type": "boolean", "description": "Attempt to fix common corruption issues", "default": false }
851                        },
852                        "required": []
853                    }
854                },
855                {
856                    "name": "memory_rebuild",
857                    "description": "Rebuild the microscope index from layer source files (merges append log into main index)",
858                    "inputSchema": {
859                        "type": "object",
860                        "properties": {},
861                        "required": []
862                    }
863                },
864                {
865                    "name": "memory_store_data",
866                    "description": "Store structured key-value data pairs",
867                    "inputSchema": {
868                        "type": "object",
869                        "properties": {
870                            "pairs": { "type": "object", "description": "Key-value pairs to store", "additionalProperties": { "type": "string" } },
871                            "importance": { "type": "integer", "description": "Importance level 1-10", "default": 5 }
872                        },
873                        "required": ["pairs"]
874                    }
875                },
876                {
877                    "name": "memory_resonant",
878                    "description": "Show most resonant blocks — strongest mirror neuron signal",
879                    "inputSchema": {
880                        "type": "object",
881                        "properties": {
882                            "k": { "type": "integer", "description": "Number of top resonant blocks", "default": 10 }
883                        },
884                        "required": []
885                    }
886                },
887                {
888                    "name": "memory_autonomous",
889                    "description": "Run autonomous mode — the system runs itself: daydream, curiosity, monologue, reflect, narrative, dream",
890                    "inputSchema": {
891                        "type": "object",
892                        "properties": {
893                            "tts": { "type": "boolean", "description": "Enable TTS", "default": false },
894                            "daemon": { "type": "boolean", "description": "Run as continuous loop", "default": false },
895                            "interval": { "type": "integer", "description": "Cycle interval in seconds", "default": 30 },
896                            "max_cycles": { "type": "integer", "description": "Maximum cycles (default: 1 in single mode, infinite in daemon)" }
897                        },
898                        "required": []
899                    }
900                },
901                {
902                    "name": "memory_consciousness",
903                    "description": "Show live consciousness stream state — emotion, surprise, curiosity, predictions, all 13 layers in real-time",
904                    "inputSchema": {
905                        "type": "object",
906                        "properties": {},
907                        "required": []
908                    }
909                },
910                {
911                    "name": "memory_evidence_show",
912                    "description": "Show evidence record: class, confidence, support/refute, distinct sources",
913                    "inputSchema": {
914                        "type": "object",
915                        "properties": {
916                            "hash_or_text": { "type": "string", "description": "Hash (16 hex) or text" }
917                        },
918                        "required": ["hash_or_text"]
919                    }
920                },
921                {
922                    "name": "memory_evidence_link",
923                    "description": "Link an Observation/Evidence to a claim (increases confidence)",
924                    "inputSchema": {
925                        "type": "object",
926                        "properties": {
927                            "claim": { "type": "string" },
928                            "support": { "type": "string" },
929                            "source": { "type": "integer", "default": 0 },
930                            "class": { "type": "string", "default": "observation" }
931                        },
932                        "required": ["claim", "support"]
933                    }
934                },
935                {
936                    "name": "memory_evidence_refute",
937                    "description": "Record a refutation against a claim (decreases confidence)",
938                    "inputSchema": {
939                        "type": "object",
940                        "properties": {
941                            "claim": { "type": "string" },
942                            "source": { "type": "integer", "default": 0 }
943                        },
944                        "required": ["claim"]
945                    }
946                },
947                {
948                    "name": "memory_evidence_audit",
949                    "description": "Verify evidence audit chain integrity",
950                    "inputSchema": { "type": "object", "properties": {}, "required": [] }
951                },
952                {
953                    "name": "memory_evidence_gates",
954                    "description": "Count promotions blocked by the epistemic gate",
955                    "inputSchema": { "type": "object", "properties": {}, "required": [] }
956                }
957            ]
958        }
959    })
960}
961
962fn tool_status(config: &Config) -> Result<String, String> {
963    let reader = MicroscopeReader::open(config)?;
964
965    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
966    let appended = read_append_log(&append_path);
967
968    let mut depth_info = String::new();
969    for (d, &(_start, count)) in reader.depth_ranges.iter().enumerate() {
970        if count > 0 {
971            depth_info.push_str(&format!("  D{}: {} blocks\n", d, count));
972        }
973    }
974
975    let hdr_kb = (reader.block_count * crate::HEADER_SIZE) as f64 / 1024.0;
976    let data_kb = reader.data.len() as f64 / 1024.0;
977
978    let embedding_provider = &config.embedding.provider;
979    let use_gpu = config.embedding.use_gpu;
980    let gpu_status = if use_gpu {
981        "enabled (CUDA lazy)"
982    } else {
983        "disabled"
984    };
985
986    Ok(format!(
987        "Microscope Memory Status\n\
988         ========================\n\
989         Blocks: {}\n\
990         Headers: {:.1} KB\n\
991         Data: {:.1} KB\n\
992         Total: {:.1} KB\n\
993         Append log: {} entries\n\
994         \n\
995         Depth breakdown:\n\
996         {}\n\
997         Features:\n\
998         - Embedding provider: {}\n\
999         - GPU acceleration: {}\n\
1000         - Cosine echo filter: enabled (source_id + cosine similarity)",
1001        reader.block_count,
1002        hdr_kb,
1003        data_kb,
1004        hdr_kb + data_kb,
1005        appended.len(),
1006        depth_info,
1007        embedding_provider,
1008        gpu_status
1009    ))
1010}
1011
1012fn tool_store(config: &Config, args: &Value) -> Result<String, String> {
1013    let text = args
1014        .get("text")
1015        .and_then(|v: &Value| v.as_str())
1016        .ok_or("Missing required parameter: text")?;
1017    let layer = args
1018        .get("layer")
1019        .and_then(|v: &Value| v.as_str())
1020        .unwrap_or("long_term");
1021    let importance = args
1022        .get("importance")
1023        .and_then(|v: &Value| v.as_u64())
1024        .unwrap_or(5) as u8;
1025
1026    // Parse optional 21D emotion vector
1027    let emotion: Option<[f32; 21]> = args.get("emotion").and_then(|v: &Value| {
1028        let arr = v.as_array()?;
1029        if arr.len() != 21 {
1030            return None;
1031        }
1032        let mut emo = [0.0f32; 21];
1033        for (i, val) in arr.iter().enumerate() {
1034            emo[i] = val.as_f64().unwrap_or(0.0) as f32;
1035        }
1036        Some(emo)
1037    });
1038
1039    let sid = std::process::id();
1040    let tagged = format!("[sid-{:04}] {}", sid % 10000, text);
1041
1042    store_memory_with_emotion(config, &tagged, layer, importance, emotion)?;
1043
1044    let (x, y, z) = crate::content_coords(&tagged, layer);
1045    let depth = crate::auto_depth(&tagged);
1046
1047    Ok(format!(
1048        "Stored memory:\n\
1049         Layer: {}\n\
1050         Importance: {}\n\
1051         Depth: D{}\n\
1052         Position: ({:.3}, {:.3}, {:.3})\n\
1053         Session: sid-{:04}\n\
1054         Text: {}",
1055        layer,
1056        importance,
1057        depth,
1058        x,
1059        y,
1060        z,
1061        sid % 10000,
1062        crate::safe_truncate(text, 200)
1063    ))
1064}
1065
1066fn tool_recall(config: &Config, args: &Value) -> Result<String, String> {
1067    let query = args
1068        .get("query")
1069        .and_then(|v: &Value| v.as_str())
1070        .ok_or("Missing required parameter: query")?;
1071    let k = args.get("k").and_then(|v: &Value| v.as_u64()).unwrap_or(10) as usize;
1072
1073    // Parse optional 21D emotion vector for emotional recall
1074    let query_emotion: Option<[f32; 21]> = args.get("emotion").and_then(|v: &Value| {
1075        let arr = v.as_array()?;
1076        if arr.len() != 21 {
1077            return None;
1078        }
1079        let mut emo = [0.0f32; 21];
1080        for (i, val) in arr.iter().enumerate() {
1081            emo[i] = val.as_f64().unwrap_or(0.0) as f32;
1082        }
1083        Some(emo)
1084    });
1085    let emotional_recall_weight = config.search.emotional_bias_weight * 0.15;
1086
1087    crate::recall_ctx::with_shared(config, |ctx| {
1088        let reader = &ctx.reader;
1089        let output_dir = Path::new(&config.paths.output_dir);
1090
1091        // Load emotional state ring for priming + attention intensity
1092        let emotional_ring = crate::EmotionalStateRing::load_or_init(output_dir);
1093        let emotional_intensity = emotional_ring.intensity();
1094
1095        // If no explicit emotion, try priming from the emotional state ring
1096        let query_emotion = query_emotion.or_else(|| {
1097            if emotional_ring.is_active() {
1098                if let Some((name, val)) = emotional_ring.dominant() {
1099                    eprintln!("  [] emotional prime: {} ({:.2})", name, val);
1100                }
1101                Some(emotional_ring.current)
1102            } else {
1103                None
1104            }
1105        });
1106
1107        let (qx, qy, qz) =
1108            crate::content_coords_blended(query, "long_term", config.search.semantic_weight);
1109        let relevance_query = crate::relevance::RelevanceQuery::new(query);
1110
1111        let (mut attention, hebb_pre, tg_pre, pc_pre) = if let Some(stream) =
1112            crate::consciousness_stream::global_stream()
1113        {
1114            let s = crate::sync_guard::mutex_lock(stream);
1115            (
1116                s.attention.clone(),
1117                s.hebbian.clone(),
1118                s.thought_graph.clone(),
1119                s.predictive_cache.clone(),
1120            )
1121        } else {
1122            let attn = crate::attention::AttentionState::load_or_init(output_dir);
1123            let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, reader.block_count);
1124            let tg = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
1125            let pc = crate::predictive_cache::PredictiveCache::load_or_init(output_dir);
1126            (attn, hebb, tg, pc)
1127        };
1128
1129        let emotional_energy = crate::emotional::emotional_field(reader, &hebb_pre)
1130            .map(|f| f.total_energy)
1131            .unwrap_or(0.0);
1132
1133        if attention.total_recalls > 0 {
1134            let quality = attention.infer_quality();
1135            if let Some(last) = attention.history.last() {
1136                let prev_weights = last.weights;
1137                attention.record_outcome(quality, &prev_weights);
1138            }
1139        }
1140
1141        let attn_signals = crate::attention::AttentionSignals {
1142            query_length: query.len(),
1143            emotional_energy,
1144            emotional_intensity,
1145            session_depth: tg_pre.current_path().len(),
1146            pattern_confidence: 0.0,
1147            cache_hit_rate: pc_pre.stats.hit_rate(),
1148            archetype_match_score: 0.0,
1149        };
1150        let attn = attention.compute_attention(&attn_signals);
1151
1152        let emotional_weight = config.search.emotional_bias_weight * attn.weight(4);
1153        let (qx, qy, qz) =
1154            crate::emotional::apply_emotional_bias(qx, qy, qz, emotional_weight, reader, &hebb_pre);
1155
1156        // Scan every populated depth, not a window guessed from query length.
1157        // auto_depth puts 15..39 character statements at D5, so a short question
1158        // previously never scanned the depth holding its own fact.
1159        let (zoom_lo, zoom_hi) = {
1160            let mut hi = 0u8;
1161            for (d, &(_, count)) in reader.depth_ranges.iter().enumerate() {
1162                if count > 0 {
1163                    hi = d as u8;
1164                }
1165            }
1166            (0u8, hi)
1167        };
1168
1169        // Embed the query and take the nearest stored vectors, so a paraphrase with
1170        // no shared token can still become a candidate. See the equivalent path in
1171        // main::recall; previously this gate was `lexical > 0.0` alone, which made
1172        // the embedding index unreachable from the agent-facing tool.
1173        let mut semantic_hits: std::collections::HashMap<usize, f32> =
1174            std::collections::HashMap::new();
1175        // Vectors of memories still in the append log, keyed by append position;
1176        // see the equivalent block in main::recall.
1177        let mut appended_sem: std::collections::HashMap<usize, f32> =
1178            std::collections::HashMap::new();
1179        {
1180            use crate::embedding_index::EmbeddingIndex;
1181
1182            let emb_path = output_dir.join("embeddings.bin");
1183            if let Some(eidx) = EmbeddingIndex::open(&emb_path) {
1184                // Cached: a long-lived server used to rebuild the provider -- and
1185                // re-fault the model weights -- on every single query.
1186                let embedded =
1187                    crate::embeddings::with_cached_provider(&config.embedding, eidx.dim(), |p| {
1188                        p.embed(query)
1189                    });
1190                match embedded {
1191                    Ok(qe) if qe.len() == eidx.dim() => {
1192                        for (sim, block_idx) in eidx.search(&qe, (k * 8).max(64)) {
1193                            if block_idx < reader.block_count {
1194                                semantic_hits.insert(block_idx, sim);
1195                            }
1196                        }
1197                        if let Some(side) = crate::embedding_index::AppendEmbeddings::open(
1198                            &output_dir.join(crate::embedding_index::APPEND_EMBEDDINGS_FILE),
1199                            eidx.dim(),
1200                        ) {
1201                            for (sim, ai) in side.search(&qe, (k * 8).max(64)) {
1202                                appended_sem.insert(ai as usize, sim);
1203                            }
1204                        }
1205                    }
1206                    Ok(_) => eprintln!("  semantic: provider width differs from index; skipped"),
1207                    Err(e) => eprintln!("  semantic: embedding failed ({}); lexical path only", e),
1208                }
1209            }
1210        }
1211
1212        // Load emotions.bin lookup for main-index emotional recall
1213        let emotion_lookup = query_emotion
1214            .as_ref()
1215            .and_then(|_| crate::load_emotion_lookup(output_dir));
1216
1217        let mut all_results: Vec<(f32, usize, bool)> = Vec::new();
1218
1219        for zoom in zoom_lo..=zoom_hi {
1220            let (start, count) = reader.depth_ranges[zoom as usize];
1221            let (start, count) = (start as usize, count as usize);
1222            for i in start..(start + count) {
1223                let text = reader.text(i);
1224                let lexical = relevance_query.lexical_score(text);
1225                // Lexical OR semantic: the embedding candidates are useless if the
1226                // gate ignores them.
1227                let semantic = semantic_hits.get(&i).copied();
1228                if lexical > 0.0 || semantic.is_some() {
1229                    let h = reader.header(i);
1230                    let dx = h.x - qx;
1231                    let dy = h.y - qy;
1232                    let dz = h.z - qz;
1233                    let spatial_dist = dx * dx + dy * dy + dz * dz;
1234                    // Emotional similarity boost (if query emotion AND emotions.bin data available)
1235                    let emo_boost = query_emotion
1236                        .as_ref()
1237                        .and_then(|qe| {
1238                            emotion_lookup
1239                                .as_ref()
1240                                .and_then(|lookup| lookup(i))
1241                                .map(|block_emo| {
1242                                    crate::emotional_similarity(qe, &block_emo)
1243                                        * emotional_recall_weight
1244                                })
1245                        })
1246                        .unwrap_or(0.0);
1247                    let base = crate::relevance::rank_distance_from_score(
1248                        lexical,
1249                        spatial_dist,
1250                        config.search.keyword_boost,
1251                        h.importance,
1252                    );
1253                    let mut combined = crate::relevance::apply_boost(base, emo_boost);
1254                    if let Some(sim) = semantic {
1255                        let w = config.search.semantic_weight.clamp(0.0, 1.0);
1256                        combined -= sim * w;
1257                    }
1258                    all_results.push((combined, i, true));
1259                }
1260            }
1261        }
1262
1263        let append_path = Path::new(&config.paths.output_dir).join("append.bin");
1264        let appended = read_append_log(&append_path);
1265        for (ai, entry) in appended.iter().enumerate() {
1266            let dx = entry.x - qx;
1267            let dy = entry.y - qy;
1268            let dz = entry.z - qz;
1269            let dist = dx * dx + dy * dy + dz * dz;
1270            let lexical = relevance_query.lexical_score(&entry.text);
1271            // Emotional boost from inline append entry emotion
1272            let emo_boost = query_emotion
1273                .as_ref()
1274                .map(|qe| crate::emotional_similarity(qe, &entry.emotion) * emotional_recall_weight)
1275                .unwrap_or(0.0);
1276            if dist < 0.1 || lexical > 0.0 || emo_boost > 0.0 || appended_sem.contains_key(&ai) {
1277                let mut base = crate::relevance::rank_distance_from_score(
1278                    lexical,
1279                    dist,
1280                    config.search.keyword_boost,
1281                    entry.importance,
1282                );
1283                if let Some(sim) = appended_sem.get(&ai) {
1284                    let w = config.search.semantic_weight.clamp(0.0, 1.0);
1285                    base -= *sim * w;
1286                }
1287                let combined = crate::relevance::apply_boost(base, emo_boost);
1288                all_results.push((combined, ai + 1_000_000, false));
1289            }
1290        }
1291
1292        // Spreading activation: fingerprint-linked blocks get boosted across 2-hop
1293        let link_table = crate::fingerprint::LinkTable::load_cached(output_dir);
1294        if let Some(ref lt) = link_table {
1295            all_results.sort_by(|a, b| a.0.total_cmp(&b.0));
1296            let top_n = all_results.len().min(3);
1297            for i in 0..top_n {
1298                let (_, idx, is_main) = all_results[i];
1299                let text = if is_main {
1300                    reader.text(idx).to_string()
1301                } else {
1302                    appended
1303                        .get(idx - 1_000_000)
1304                        .map(|e| e.text.clone())
1305                        .unwrap_or_default()
1306                };
1307                let similar = lt.find_similar(&text, 5);
1308                for (linked_idx, sim) in &similar {
1309                    let linked_idx = *linked_idx as usize;
1310                    let found = all_results
1311                        .iter()
1312                        .any(|(_, ri, rim)| *rim && *ri == linked_idx);
1313                    if !found {
1314                        let boost = *sim * 0.12;
1315                        all_results.push((boost, linked_idx, true));
1316                    } else {
1317                        for (dist, ri, rim) in &mut all_results {
1318                            if *rim && *ri == linked_idx {
1319                                *dist = crate::relevance::apply_boost(*dist, *sim * 0.08);
1320                                break;
1321                            }
1322                        }
1323                    }
1324                }
1325            }
1326        }
1327
1328        all_results.sort_by(|a, b| a.0.total_cmp(&b.0));
1329        let novel = all_results.first().is_none_or(|(d, _, _)| *d > 0.3);
1330
1331        let qh_tg = crate::hebbian::query_hash(query);
1332        if let Some(stream) = crate::consciousness_stream::global_stream() {
1333            crate::consciousness_stream::ConsciousnessStream::feed_query(stream, qh_tg);
1334        }
1335
1336        let (mut thought_graph, mut pred_cache) =
1337            if let Some(stream) = crate::consciousness_stream::global_stream() {
1338                let s = crate::sync_guard::mutex_lock(stream);
1339                (s.thought_graph.clone(), s.predictive_cache.clone())
1340            } else {
1341                let tg = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
1342                let pc = crate::predictive_cache::PredictiveCache::load_or_init(output_dir);
1343                (tg, pc)
1344            };
1345
1346        if let Some((cached_blocks, confidence)) = pred_cache.check(qh_tg) {
1347            let boost = confidence * crate::thought_graph::PATTERN_BOOST_WEIGHT * attn.weight(6);
1348            let cached_set: HashSet<u32> = cached_blocks.iter().copied().collect();
1349            for (dist, idx, is_main) in &mut all_results {
1350                if *is_main && cached_set.contains(&(*idx as u32)) {
1351                    *dist = crate::relevance::apply_boost(*dist, boost);
1352                }
1353            }
1354        }
1355
1356        let pattern_boosts: HashMap<u32, f32> =
1357            thought_graph.pattern_boost(qh_tg).into_iter().collect();
1358        if !pattern_boosts.is_empty() {
1359            let tg_scale = attn.weight(5);
1360            for (dist, idx, is_main) in &mut all_results {
1361                if *is_main {
1362                    if let Some(&boost) = pattern_boosts.get(&(*idx as u32)) {
1363                        *dist = crate::relevance::apply_boost(*dist, boost * tg_scale);
1364                    }
1365                }
1366            }
1367        }
1368
1369        all_results.sort_by(|a, b| a.0.total_cmp(&b.0));
1370
1371        let mut output = format!("Recall '{}' (zoom D{}..D{})", query, zoom_lo, zoom_hi);
1372        // Load evidence ledger for confidence/class display.
1373        let evidence_ledger =
1374            crate::epistemic::EvidenceLedger::load_or_init(Path::new(&config.paths.output_dir));
1375        if novel {
1376            output.push_str(" [NOVEL TOPIC — low prior memory]");
1377        }
1378        output.push_str(":\n\n");
1379        let mut seen = HashSet::new();
1380        let mut shown = 0;
1381
1382        for (dist, idx, is_main) in &all_results {
1383            if shown >= k {
1384                break;
1385            }
1386            if !seen.insert((*idx, *is_main)) {
1387                continue;
1388            }
1389            if *is_main {
1390                let h = reader.header(*idx);
1391                let text = reader.text(*idx);
1392                let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
1393                let class = crate::epistemic::class_of_flags(h.flags);
1394                let ch = crate::epistemic::content_hash(text.trim());
1395                let conf = evidence_ledger
1396                    .records
1397                    .get(&ch)
1398                    .map(|r| r.confidence)
1399                    .unwrap_or(0);
1400                output.push_str(&format!(
1401                    "[D{} {} dist={:.3} {} c={}] {}\n",
1402                    h.depth,
1403                    layer,
1404                    dist,
1405                    class,
1406                    conf,
1407                    crate::safe_truncate(text, 150)
1408                ));
1409            } else {
1410                let ai = idx - 1_000_000;
1411                if let Some(entry) = appended.get(ai) {
1412                    let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
1413                    output.push_str(&format!(
1414                        "[APPEND {} dist={:.3}] {}\n",
1415                        layer,
1416                        dist,
1417                        crate::safe_truncate(&entry.text, 150)
1418                    ));
1419                }
1420            }
1421            shown += 1;
1422        }
1423
1424        let activated: Vec<(u32, f32)> = all_results
1425            .iter()
1426            .filter(|(_, _, is_main)| *is_main)
1427            .take(k)
1428            .map(|(score, idx, _)| (*idx as u32, *score))
1429            .collect();
1430
1431        if !activated.is_empty() {
1432            let mut hebb =
1433                crate::hebbian::HebbianState::load_or_init(output_dir, reader.block_count);
1434            let mut mirror = crate::mirror::MirrorState::load_or_init(output_dir);
1435            let qh = crate::hebbian::query_hash(query);
1436
1437            let _ = crate::mirror::mirror_boost(&hebb, &mut mirror, &activated, qh);
1438            hebb.record_activation(&activated, qh);
1439
1440            let mut resonance = crate::resonance::ResonanceState::load_or_init(output_dir);
1441            let headers: Vec<(f32, f32, f32)> = activated
1442                .iter()
1443                .map(|&(idx, _)| {
1444                    let h = reader.header(idx as usize);
1445                    (h.x, h.y, h.z)
1446                })
1447                .collect();
1448            resonance.emit_pulse(&activated, qh, &headers, 1);
1449
1450            let mut archetypes = crate::archetype::ArchetypeState::load_or_init(output_dir);
1451            let mut temporal =
1452                crate::temporal_archetype::TemporalArchetypeState::load_or_init(output_dir);
1453            let _ = archetypes.match_archetype(&activated);
1454            temporal.decay();
1455            archetypes.reinforce(&activated);
1456
1457            let dominant_layer = activated
1458                .first()
1459                .map(|&(idx, _)| reader.header(idx as usize).layer_id)
1460                .unwrap_or(0);
1461            thought_graph.record_recall(qh, &activated, dominant_layer);
1462            let result_block_ids: Vec<u32> = activated.iter().map(|&(idx, _)| idx).collect();
1463            thought_graph.update_pattern_blocks(qh, &result_block_ids);
1464            thought_graph.detect_patterns();
1465
1466            let _ = pred_cache.evaluate(qh, &result_block_ids, &mut thought_graph);
1467            pred_cache.predict_next(&thought_graph);
1468
1469            attention.mark_recall();
1470
1471            // Associative: link top-3 results that share keywords.
1472            //
1473            // The links are built first, then embedded together. Nine sequential
1474            // MiniLM inferences measured 585 ms of a ~1,030 ms call, and each one
1475            // is independent, so they go through rayon on the cached provider: the
1476            // same nine texts, the same nine vectors, in the same order -- only
1477            // computed at once instead of one after another. The writes themselves
1478            // stay sequential because each one takes the file lock.
1479            let mut links: Vec<String> = Vec::new();
1480            for (i, &(_, idx_a, is_a)) in all_results.iter().take(3).enumerate() {
1481                for &(_, idx_b, is_b) in all_results.iter().take(5).skip(i + 1) {
1482                    let text_a = if is_a { reader.text(idx_a) } else { "" };
1483                    let text_b = if is_b { reader.text(idx_b) } else { "" };
1484                    if !text_a.is_empty() && !text_b.is_empty() {
1485                        links.push(format!(
1486                            "LINK: [{:.40}] <-> [{:.40}] via '{}'",
1487                            text_a, text_b, query
1488                        ));
1489                    }
1490                }
1491            }
1492            // Read-only: skip the linking writes entirely. The links are the
1493            // learning here, so this both keeps a measurement from writing and
1494            // reports the p50 of a server that no longer learns -- a floor, not
1495            // the number a real server sees.
1496            if !links.is_empty() && !crate::no_learn::enabled() {
1497                let dim = config.embedding.dim;
1498                // `with_cached_provider` is generic in the closure's return type, so
1499                // this is the vector of results itself -- no Result to unwrap.
1500                let vectors: Vec<Result<Vec<f32>, crate::embeddings::EmbeddingError>> =
1501                    crate::embeddings::with_cached_provider(&config.embedding, dim, |p| {
1502                        use crate::parallel::*;
1503                        links
1504                            .par_iter()
1505                            .map(|t| p.embed(t))
1506                            .collect::<Vec<Result<Vec<f32>, _>>>()
1507                    });
1508                for (n, link) in links.iter().enumerate() {
1509                    // A failed inference yields no vector, and the store then embeds
1510                    // inline -- slower, but a link is never dropped for want of one.
1511                    let precomputed = vectors
1512                        .get(n)
1513                        .and_then(|v| v.as_ref().ok())
1514                        .filter(|v| v.len() == dim)
1515                        .cloned();
1516                    let _ =
1517                        store_memory_with_embedding(config, link, "associative", 6, precomputed);
1518                }
1519            }
1520
1521            let _ = hebb.save(output_dir);
1522            let _ = mirror.save(output_dir);
1523            let _ = resonance.save(output_dir);
1524            let _ = archetypes.save(output_dir);
1525            let _ = temporal.save(output_dir);
1526            let _ = thought_graph.save(output_dir);
1527            let _ = pred_cache.save(output_dir);
1528            let _ = attention.save(output_dir);
1529
1530            if let Some(stream) = crate::consciousness_stream::global_stream() {
1531                let mut s = crate::sync_guard::mutex_lock(stream);
1532                s.hebbian = hebb;
1533                s.mirror = mirror;
1534                s.resonance = resonance;
1535                s.archetypes = archetypes;
1536                s.thought_graph = thought_graph;
1537                s.predictive_cache = pred_cache;
1538                s.attention = attention;
1539            }
1540        }
1541
1542        output.push_str(&format!("\n{} results", shown));
1543        Ok(output)
1544    })?
1545}
1546
1547// ─── Evidence Layer Tools ───────────────────────────────────────────────────
1548
1549fn tool_evidence_show(config: &Config, args: &Value) -> Result<String, String> {
1550    let hash_or_text = args
1551        .get("hash_or_text")
1552        .and_then(|v| v.as_str())
1553        .ok_or("Missing required parameter: hash_or_text")?;
1554    let output_dir = std::path::Path::new(&config.paths.output_dir);
1555    let ledger = crate::epistemic::EvidenceLedger::load_or_init(output_dir);
1556    let ch = if hash_or_text.len() == 16 && hash_or_text.chars().all(|c| c.is_ascii_hexdigit()) {
1557        u64::from_str_radix(hash_or_text, 16).map_err(|e| format!("invalid hex: {e}"))?
1558    } else {
1559        crate::epistemic::content_hash(hash_or_text)
1560    };
1561    match ledger.records.get(&ch) {
1562        Some(rec) => Ok(serde_json::json!({
1563            "content_hash": format!("{:016x}", rec.content_hash),
1564            "class": rec.class.to_string(),
1565            "source_id": format!("{:016x}", rec.source_id),
1566            "support_count": rec.support_count,
1567            "refute_count": rec.refute_count,
1568            "distinct_sources": rec.distinct_sources,
1569            "confidence": rec.confidence,
1570            "first_seen_ms": rec.first_seen_ms,
1571        })
1572        .to_string()),
1573        None => Ok(format!(
1574            "{{\"error\": \"no evidence record for hash {:016x}\"}}",
1575            ch
1576        )),
1577    }
1578}
1579
1580fn tool_evidence_link(config: &Config, args: &Value) -> Result<String, String> {
1581    let claim_text = args
1582        .get("claim")
1583        .and_then(|v| v.as_str())
1584        .ok_or("Missing: claim")?;
1585    let support_text = args
1586        .get("support")
1587        .and_then(|v| v.as_str())
1588        .ok_or("Missing: support")?;
1589    let source = args.get("source").and_then(|v| v.as_u64()).unwrap_or(0);
1590    let class_str = args
1591        .get("class")
1592        .and_then(|v| v.as_str())
1593        .unwrap_or("observation");
1594    let class: crate::epistemic::EpistemicClass = class_str.parse().map_err(|e: String| e)?;
1595    let output_dir = std::path::Path::new(&config.paths.output_dir);
1596    let now = std::time::SystemTime::now()
1597        .duration_since(std::time::UNIX_EPOCH)
1598        .unwrap_or_default()
1599        .as_millis() as u64;
1600    let mut ledger = crate::epistemic::EvidenceLedger::load_or_init(output_dir);
1601    let mut audit = crate::epistemic::AuditChain::load_or_init(output_dir);
1602    let claim_ch = if claim_text.len() == 16 && claim_text.chars().all(|c| c.is_ascii_hexdigit()) {
1603        u64::from_str_radix(claim_text, 16).map_err(|e| format!("invalid hex: {e}"))?
1604    } else {
1605        crate::epistemic::content_hash(claim_text)
1606    };
1607    let support_ch =
1608        if support_text.len() == 16 && support_text.chars().all(|c| c.is_ascii_hexdigit()) {
1609            u64::from_str_radix(support_text, 16).map_err(|e| format!("invalid hex: {e}"))?
1610        } else {
1611            crate::epistemic::content_hash(support_text)
1612        };
1613    let provider = crate::embeddings::provider_from_config(&config.embedding, config.embedding.dim);
1614    crate::epistemic::link_evidence(
1615        &mut ledger,
1616        &mut audit,
1617        claim_ch,
1618        support_ch,
1619        class,
1620        source,
1621        now,
1622        Some(support_text),
1623        Some(claim_text),
1624        Some(provider.as_ref()),
1625    )?;
1626    ledger.save(output_dir)?;
1627    audit.save(output_dir)?;
1628    let conf = ledger
1629        .records
1630        .get(&claim_ch)
1631        .map(|r| r.confidence)
1632        .unwrap_or(0);
1633    Ok(serde_json::json!({"status":"linked","confidence":conf}).to_string())
1634}
1635
1636fn tool_evidence_refute(config: &Config, args: &Value) -> Result<String, String> {
1637    let claim_text = args
1638        .get("claim")
1639        .and_then(|v| v.as_str())
1640        .ok_or("Missing: claim")?;
1641    let source = args.get("source").and_then(|v| v.as_u64()).unwrap_or(0);
1642    let output_dir = std::path::Path::new(&config.paths.output_dir);
1643    let now = std::time::SystemTime::now()
1644        .duration_since(std::time::UNIX_EPOCH)
1645        .unwrap_or_default()
1646        .as_millis() as u64;
1647    let mut ledger = crate::epistemic::EvidenceLedger::load_or_init(output_dir);
1648    let mut audit = crate::epistemic::AuditChain::load_or_init(output_dir);
1649    let claim_ch = if claim_text.len() == 16 && claim_text.chars().all(|c| c.is_ascii_hexdigit()) {
1650        u64::from_str_radix(claim_text, 16).map_err(|e| format!("invalid hex: {e}"))?
1651    } else {
1652        crate::epistemic::content_hash(claim_text)
1653    };
1654    crate::epistemic::refute(&mut ledger, &mut audit, claim_ch, source, now)?;
1655    ledger.save(output_dir)?;
1656    audit.save(output_dir)?;
1657    let conf = ledger
1658        .records
1659        .get(&claim_ch)
1660        .map(|r| r.confidence)
1661        .unwrap_or(0);
1662    Ok(serde_json::json!({"status":"refuted","confidence":conf}).to_string())
1663}
1664
1665fn tool_evidence_audit(config: &Config, _args: &Value) -> Result<String, String> {
1666    let output_dir = std::path::Path::new(&config.paths.output_dir);
1667    let chain = crate::epistemic::AuditChain::load_or_init(output_dir);
1668    let result = chain.verify();
1669    let events: Vec<serde_json::Value> = chain
1670        .chunks
1671        .iter()
1672        .skip(1)
1673        .map(|c| {
1674            serde_json::json!({
1675                "event": format!("{:?}", c.record.event),
1676                "content_hash": format!("{:016x}", c.record.content_hash),
1677                "source_id": format!("{:016x}", c.record.source_id),
1678                "delta": c.record.delta,
1679                "note": c.record.note,
1680            })
1681        })
1682        .collect();
1683    Ok(serde_json::json!({
1684        "total_chunks": chain.chunks.len(),
1685        "integrity": if result.is_ok() { "OK" } else { "FAIL" },
1686        "events": events,
1687    })
1688    .to_string())
1689}
1690
1691fn tool_evidence_gates(config: &Config, _args: &Value) -> Result<String, String> {
1692    let output_dir = std::path::Path::new(&config.paths.output_dir);
1693    let chain = crate::epistemic::AuditChain::load_or_init(output_dir);
1694    let gates: Vec<serde_json::Value> = chain
1695        .chunks
1696        .iter()
1697        .filter(|c| c.record.event == crate::epistemic::AuditEvent::PromoGate)
1698        .map(|c| {
1699            serde_json::json!({
1700                "content_hash": format!("{:016x}", c.record.content_hash),
1701                "note": c.record.note,
1702            })
1703        })
1704        .collect();
1705    Ok(serde_json::json!({
1706        "blocked_count": gates.len(),
1707        "gates": gates,
1708    })
1709    .to_string())
1710}
1711
1712fn tool_find(config: &Config, args: &Value) -> Result<String, String> {
1713    let query = args
1714        .get("query")
1715        .and_then(|v: &Value| v.as_str())
1716        .ok_or("Missing required parameter: query")?;
1717    let k = args.get("k").and_then(|v: &Value| v.as_u64()).unwrap_or(10) as usize;
1718
1719    crate::recall_ctx::with_shared(config, |ctx| {
1720        let reader = &ctx.reader;
1721        let append_path = Path::new(&config.paths.output_dir).join("append.bin");
1722        let appended = crate::read_append_log(&append_path);
1723        let results = reader.find_text_all(config, query, k);
1724
1725        if results.is_empty() {
1726            return Ok(format!("No results for '{}'", query));
1727        }
1728
1729        let mut output = format!("Text search '{}': {} results\n\n", query, results.len());
1730        for (depth, idx, is_main) in &results {
1731            if *is_main {
1732                let h = reader.header(*idx);
1733                let text = reader.text(*idx);
1734                let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
1735                output.push_str(&format!(
1736                    "[D{} {}] {}\n",
1737                    h.depth,
1738                    layer,
1739                    crate::safe_truncate(text, 150)
1740                ));
1741            } else if let Some(entry) = appended.get(idx.saturating_sub(1_000_000)) {
1742                let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
1743                output.push_str(&format!(
1744                    "[D{} {}/new] {}\n",
1745                    depth,
1746                    layer,
1747                    crate::safe_truncate(&entry.text, 150)
1748                ));
1749            }
1750        }
1751
1752        Ok(output)
1753    })?
1754}
1755
1756fn tool_mql_query(config: &Config, args: &Value) -> Result<String, String> {
1757    let mql = args
1758        .get("mql")
1759        .and_then(|v: &Value| v.as_str())
1760        .ok_or("Missing required parameter: mql")?;
1761
1762    let reader = MicroscopeReader::open(config)?;
1763    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
1764    let appended = read_append_log(&append_path);
1765
1766    let q = crate::query::parse(mql);
1767    let results = crate::query::execute(&q, &reader, &appended);
1768
1769    if results.is_empty() {
1770        return Ok(format!("MQL '{}': no results", mql));
1771    }
1772
1773    let mut output = format!("MQL '{}': {} results\n\n", mql, results.len());
1774    for r in &results {
1775        if r.is_main {
1776            let h = reader.header(r.block_idx);
1777            let text = reader.text(r.block_idx);
1778            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
1779            output.push_str(&format!(
1780                "[D{} {} score={:.3}] {}\n",
1781                h.depth,
1782                layer,
1783                r.score,
1784                crate::safe_truncate(text, 150)
1785            ));
1786        } else if let Some(entry) = appended.get(r.block_idx) {
1787            let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
1788            output.push_str(&format!(
1789                "[APPEND {} score={:.3}] {}\n",
1790                layer,
1791                r.score,
1792                crate::safe_truncate(&entry.text, 150)
1793            ));
1794        }
1795    }
1796
1797    Ok(output)
1798}
1799
1800fn tool_build(config: &Config, args: &Value) -> Result<String, String> {
1801    let force = args
1802        .get("force")
1803        .and_then(|v: &Value| v.as_bool())
1804        .unwrap_or(false);
1805
1806    crate::build::build(config, force, true)?;
1807
1808    // Clear append log and emotions log after successful rebuild
1809    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
1810    let _ = std::fs::remove_file(append_path);
1811    let emotions_path = Path::new(&config.paths.output_dir).join("emotions.bin");
1812    let _ = std::fs::remove_file(emotions_path);
1813
1814    let reader = MicroscopeReader::open(config)?;
1815    Ok(format!(
1816        "Build complete: {} blocks across {} depths\nAppend log cleared.",
1817        reader.block_count,
1818        reader.depth_ranges.iter().filter(|&&(_, c)| c > 0).count()
1819    ))
1820}
1821
1822fn tool_look(config: &Config, args: &Value) -> Result<String, String> {
1823    let x = args
1824        .get("x")
1825        .and_then(|v: &Value| v.as_f64())
1826        .ok_or("Missing required parameter: x")? as f32;
1827    let y = args
1828        .get("y")
1829        .and_then(|v: &Value| v.as_f64())
1830        .ok_or("Missing required parameter: y")? as f32;
1831    let z = args
1832        .get("z")
1833        .and_then(|v: &Value| v.as_f64())
1834        .ok_or("Missing required parameter: z")? as f32;
1835    let zoom = args
1836        .get("zoom")
1837        .and_then(|v: &Value| v.as_u64())
1838        .ok_or("Missing required parameter: zoom")? as u8;
1839    let k = args.get("k").and_then(|v: &Value| v.as_u64()).unwrap_or(10) as usize;
1840
1841    let reader = MicroscopeReader::open(config)?;
1842    let config_clone = config.clone();
1843    let results = reader.look(&config_clone, x, y, z, zoom, k);
1844
1845    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
1846    let appended = read_append_log(&append_path);
1847
1848    if results.is_empty() {
1849        return Ok(format!(
1850            "Look ({:.2},{:.2},{:.2}) zoom={}: no results",
1851            x, y, z, zoom
1852        ));
1853    }
1854
1855    let mut output = format!(
1856        "Look ({:.2},{:.2},{:.2}) zoom={}: {} results\n\n",
1857        x,
1858        y,
1859        z,
1860        zoom,
1861        results.len()
1862    );
1863
1864    for (dist, idx, is_main) in &results {
1865        if *is_main {
1866            let h = reader.header(*idx);
1867            let text = reader.text(*idx);
1868            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
1869            output.push_str(&format!(
1870                "[D{} {} dist={:.3}] {}\n",
1871                h.depth,
1872                layer,
1873                dist,
1874                crate::safe_truncate(text, 150)
1875            ));
1876        } else if let Some(entry) = appended.get(*idx) {
1877            let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
1878            output.push_str(&format!(
1879                "[APPEND {} dist={:.3}] {}\n",
1880                layer,
1881                dist,
1882                crate::safe_truncate(&entry.text, 150)
1883            ));
1884        }
1885    }
1886
1887    Ok(output)
1888}
1889
1890fn tool_session_log(config: &Config, args: &Value) -> Result<String, String> {
1891    let n = args.get("n").and_then(|v: &Value| v.as_u64()).unwrap_or(50) as usize;
1892    let file_path = Path::new(&config.paths.layers_dir).join("session.txt");
1893
1894    let content = if file_path.exists() {
1895        std::fs::read_to_string(&file_path).unwrap_or_default()
1896    } else {
1897        return Ok("Session memory is empty. Store interactions with layer=session.".to_string());
1898    };
1899
1900    let entries: Vec<&str> = content
1901        .split("\n\n")
1902        .filter(|s| !s.trim().is_empty())
1903        .collect();
1904
1905    let total = entries.len();
1906    let start = total.saturating_sub(n);
1907    let recent: Vec<&&str> = entries[start..].iter().rev().collect();
1908
1909    let now_secs = std::time::SystemTime::now()
1910        .duration_since(std::time::UNIX_EPOCH)
1911        .unwrap_or_default()
1912        .as_secs();
1913
1914    let mut output = format!(
1915        "Session Memory — {} total interactions, showing last {}:\n\n",
1916        total,
1917        recent.len()
1918    );
1919    for (i, entry) in recent.iter().enumerate() {
1920        let num = total - start - i;
1921        let decay = ebbinghaus_decay(entry, now_secs);
1922        output.push_str(&format!(
1923            "{} {}| {}\n",
1924            num,
1925            decay,
1926            crate::safe_truncate(entry, 300)
1927        ));
1928    }
1929
1930    Ok(output)
1931}
1932
1933fn ebbinghaus_decay(entry: &str, now_secs: u64) -> &'static str {
1934    let ts_str = if entry.starts_with('[') {
1935        entry
1936            .split(']')
1937            .next()
1938            .unwrap_or("")
1939            .trim_start_matches('[')
1940    } else {
1941        return "█ forgotten";
1942    };
1943    if ts_str.len() < 16 {
1944        return "█ forgotten";
1945    }
1946    let parts: Vec<&str> = ts_str.split(&['-', ' ', ':']).collect();
1947    if parts.len() < 5 {
1948        return "█ forgotten";
1949    }
1950    let y: u64 = parts[0].parse().unwrap_or(0);
1951    let mo: u64 = parts[1].parse().unwrap_or(0);
1952    let d: u64 = parts[2].parse().unwrap_or(0);
1953    let h: u64 = parts[3].parse().unwrap_or(0);
1954    let m: u64 = parts[4].parse().unwrap_or(0);
1955    if y < 2020 || mo == 0 || d == 0 {
1956        return "█ forgotten";
1957    }
1958    let mut days = 0u64;
1959    for yr in 1970..y {
1960        days += if is_leap_yr(yr) { 366 } else { 365 };
1961    }
1962    let leap = is_leap_yr(y);
1963    let mdays = [
1964        31,
1965        if leap { 29 } else { 28 },
1966        31,
1967        30,
1968        31,
1969        30,
1970        31,
1971        31,
1972        30,
1973        31,
1974        30,
1975        31,
1976    ];
1977    for &d in mdays.iter().take((mo - 1) as usize) {
1978        days += d as u64;
1979    }
1980    days += d - 1;
1981    let entry_secs = days * 86400 + h * 3600 + m * 60;
1982    let age_hours = if now_secs > entry_secs {
1983        (now_secs - entry_secs) / 3600
1984    } else {
1985        0
1986    };
1987
1988    if age_hours < 1 {
1989        "░ FRESH"
1990    } else if age_hours < 24 {
1991        "░ recent"
1992    } else if age_hours < 72 {
1993        "▒ fading"
1994    } else if age_hours < 168 {
1995        "▓ old"
1996    } else {
1997        "█ forgotten"
1998    }
1999}
2000
2001fn is_leap_yr(y: u64) -> bool {
2002    (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
2003}
2004
2005fn tool_consolidate(config: &Config, _args: &Value) -> Result<String, String> {
2006    let file_path = Path::new(&config.paths.layers_dir).join("session.txt");
2007    let content = if file_path.exists() {
2008        std::fs::read_to_string(&file_path).unwrap_or_default()
2009    } else {
2010        return Ok("Session memory is empty.".to_string());
2011    };
2012    let entries: Vec<&str> = content
2013        .split("\n\n")
2014        .filter(|s| !s.trim().is_empty())
2015        .collect();
2016    if entries.len() < 3 {
2017        return Ok("Not enough entries to consolidate (need 3+).".to_string());
2018    }
2019
2020    let mut groups: HashMap<String, Vec<String>> = HashMap::new();
2021    for entry in &entries {
2022        let sid = if entry.contains("[sid-") {
2023            entry
2024                .split("[sid-")
2025                .nth(1)
2026                .and_then(|s| s.split(']').next())
2027                .map(|s| format!("sid-{}", s))
2028                .unwrap_or_else(|| "nosid".to_string())
2029        } else {
2030            "nosid".to_string()
2031        };
2032        groups.entry(sid).or_default().push(entry.to_string());
2033    }
2034
2035    let mut summaries = Vec::new();
2036    for (sid, group) in &groups {
2037        if group.len() < 2 {
2038            continue;
2039        }
2040        let top_topics: Vec<String> = group
2041            .iter()
2042            .take(5)
2043            .map(|e| {
2044                let parts: Vec<&str> = e.split("] ").collect();
2045                crate::safe_truncate(parts.last().unwrap_or(&""), 50)
2046            })
2047            .collect();
2048
2049        let summary = format!(
2050            "[{}] CONSOLIDATED: {} interactions. Topics: {}",
2051            sid,
2052            group.len(),
2053            top_topics.join(" | ")
2054        );
2055        summaries.push(summary);
2056
2057        store_memory(
2058            config,
2059            &format!(
2060                "[{}] CONSOLIDATED: {} interactions from {}",
2061                sid,
2062                group.len(),
2063                top_topics.join(", ")
2064            ),
2065            "long_term",
2066            8,
2067        )?;
2068    }
2069
2070    let mut output = format!("Consolidated {} session groups:\n\n", summaries.len());
2071    for s in &summaries {
2072        output.push_str(&format!("  {}\n", s));
2073    }
2074    Ok(output)
2075}
2076
2077fn tool_dream(config: &Config, _args: &Value) -> Result<String, String> {
2078    let output_dir = Path::new(&config.paths.output_dir);
2079    let reader = MicroscopeReader::open(config)?;
2080    let block_count = reader.block_count;
2081    drop(reader);
2082
2083    match crate::dream::dream_consolidate(
2084        output_dir,
2085        block_count,
2086        config.index.max_blocks,
2087        config.index.protect_min_importance,
2088    ) {
2089        Ok(cycle) => Ok(format!(
2090            "Dream consolidation complete:\n\
2091             Duration: {}ms\n\
2092             Replayed fingerprints: {}\n\
2093             Strengthened pairs: {}\n\
2094             Pruned pairs: {}\n\
2095             Pruned activations: {}\n\
2096             Consolidated patterns: {}\n\
2097             Energy: {:.3} -> {:.3}",
2098            cycle.duration_ms,
2099            cycle.replayed_fingerprints,
2100            cycle.strengthened_pairs,
2101            cycle.pruned_pairs,
2102            cycle.pruned_activations,
2103            cycle.consolidated_patterns,
2104            cycle.energy_before,
2105            cycle.energy_after,
2106        )),
2107        Err(e) => Err(format!("Dream consolidation failed: {}", e)),
2108    }
2109}
2110
2111// ─── Session Context Tool ────────────────────────────
2112
2113fn tool_session_context(config: &Config, args: &Value) -> Result<String, String> {
2114    let context = args
2115        .get("context")
2116        .and_then(|v: &Value| v.as_str())
2117        .ok_or("Missing required parameter: context")?;
2118    let summary = args
2119        .get("summary")
2120        .and_then(|v: &Value| v.as_str())
2121        .unwrap_or("");
2122
2123    // Store the conversation context
2124    let tagged = format!("[SESSION_CONTEXT] {}", context);
2125    store_memory(config, &tagged, "session", 7)?;
2126
2127    // If summary provided, store it in long_term
2128    if !summary.is_empty() {
2129        let summary_tagged = format!("[SESSION_SUMMARY] {}", summary);
2130        store_memory(config, &summary_tagged, "long_term", 6)?;
2131    }
2132
2133    Ok(format!(
2134        "Session context stored:\n  Layer: session\n  Importance: 7\n  Context length: {} chars\n  Summary: {}",
2135        context.len(),
2136        if summary.is_empty() { "(none)" } else { summary }
2137    ))
2138}
2139
2140// ─── Consciousness Tool (Live Stream) ────────────────────
2141
2142fn tool_consciousness(config: &Config, _args: &Value) -> Result<String, String> {
2143    use crate::consciousness_stream::{global_stream, ConsciousnessStream};
2144
2145    let state = match global_stream() {
2146        Some(s) => s.clone(),
2147        None => ConsciousnessStream::start(config),
2148    };
2149
2150    // Ultra-fast path: read the cached format string from the snapshot.
2151    // The background cycle builds this string once per tick (100ms),
2152    // and readers just clone the Arc — no format!(), no seqlock, no Mutex.
2153    // Cost: ~50-100ns per call.
2154    let snapshot = {
2155        let s = crate::sync_guard::mutex_lock(&state);
2156        s.snapshot.clone()
2157    };
2158    Ok(snapshot.read_cached_format())
2159}
2160
2161// ─── Ping Tool (Auto-Context) ────────────────────────────
2162
2163fn tool_ping(config: &Config, args: &Value) -> Result<String, String> {
2164    let query = args
2165        .get("query")
2166        .and_then(|v: &Value| v.as_str())
2167        .ok_or("Missing required parameter: query")?;
2168
2169    // Quick recall with low k for fast context
2170    let reader = MicroscopeReader::open(config)?;
2171    let output_dir = Path::new(&config.paths.output_dir);
2172    let append_path = output_dir.join("append.bin");
2173    let appended = read_append_log(&append_path);
2174
2175    let (qx, qy, qz) =
2176        crate::content_coords_blended(query, "long_term", config.search.semantic_weight);
2177
2178    let mut all_results: Vec<(f32, usize)> = Vec::new();
2179
2180    // Quick scan of main index (top 500 blocks for speed)
2181    let scan_limit = reader.block_count.min(500);
2182    for i in 0..scan_limit {
2183        let h = reader.header(i);
2184        let dx = qx - h.x;
2185        let dy = qy - h.y;
2186        let dz = qz - h.z;
2187        let dist = dx * dx + dy * dy + dz * dz;
2188        if dist < 0.05 {
2189            all_results.push((dist, i));
2190        }
2191    }
2192
2193    // Check append log
2194    for (ai, entry) in appended.iter().enumerate() {
2195        let dx = qx - entry.x;
2196        let dy = qy - entry.y;
2197        let dz = qz - entry.z;
2198        let dist = dx * dx + dy * dy + dz * dz;
2199        if dist < 0.05 {
2200            all_results.push((dist, 1_000_000 + ai));
2201        }
2202    }
2203
2204    all_results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2205
2206    let mut output = format!("PING '{}':\n", query);
2207    let mut shown = 0;
2208    for (dist, idx) in all_results.iter().take(5) {
2209        let text = if *idx < 1_000_000 {
2210            reader.text(*idx).to_string()
2211        } else {
2212            appended
2213                .get(*idx - 1_000_000)
2214                .map(|e| e.text.clone())
2215                .unwrap_or_default()
2216        };
2217        if !text.is_empty() {
2218            output.push_str(&format!(
2219                "  [{:.3}] {}\n",
2220                dist,
2221                crate::safe_truncate(&text, 120)
2222            ));
2223            shown += 1;
2224        }
2225    }
2226
2227    if shown == 0 {
2228        output.push_str("  (no close matches)\n");
2229    }
2230
2231    Ok(output)
2232}
2233
2234// ─── Auto-Context Tool ─────────────────────────────
2235
2236fn tool_auto_context(config: &Config, _args: &Value) -> Result<String, String> {
2237    let reader = MicroscopeReader::open(config)?;
2238    let output_dir = Path::new(&config.paths.output_dir);
2239    let ctx = crate::auto_context::build(output_dir, &reader);
2240    Ok(crate::auto_context::render(&ctx))
2241}
2242
2243// ─── Timeline Tool ──────────────────────────────────
2244
2245fn tool_timeline(config: &Config, args: &Value) -> Result<String, String> {
2246    let window = args
2247        .get("window")
2248        .and_then(|v| v.as_str())
2249        .unwrap_or("last_session");
2250    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
2251
2252    let path = Path::new(&config.paths.output_dir).join("timeline.bin");
2253    let entries = crate::timeline::read_all(&path);
2254    let w = crate::timeline::TimeWindow::parse(window)
2255        .map_err(|e| format!("Invalid window '{}': {}", window, e))?;
2256    let filtered = crate::timeline::filter(&entries, &w);
2257    let mut rev: Vec<&crate::timeline::TimelineEntry> = filtered.iter().rev().collect();
2258    rev.truncate(k);
2259
2260    if rev.is_empty() {
2261        return Ok(format!("Timeline [{}]: no entries found", window));
2262    }
2263
2264    let mut output = format!("Timeline [{}] — {} entries:\n\n", window, rev.len());
2265    for e in &rev {
2266        let layer_name = crate::LAYER_NAMES.get(e.layer_id as usize).unwrap_or(&"?");
2267        let status_label = match e.status {
2268            crate::timeline::STATUS_OPEN => "OPEN",
2269            crate::timeline::STATUS_RESOLVED => "RESOLVED",
2270            crate::timeline::STATUS_ARCHIVED => "ARCHIVED",
2271            _ => "",
2272        };
2273        output.push_str(&format!(
2274            "{} D{} [{}] imp={}{} {}\n",
2275            crate::timeline::format_ts(e.ts_ms),
2276            e.depth,
2277            layer_name,
2278            e.importance,
2279            if status_label.is_empty() {
2280                String::new()
2281            } else {
2282                format!(" [{}]", status_label)
2283            },
2284            crate::safe_truncate(&e.text, 100)
2285        ));
2286    }
2287    Ok(output)
2288}
2289
2290// ─── Loops Tool ────────────────────────────────────
2291
2292fn tool_loops(config: &Config, args: &Value) -> Result<String, String> {
2293    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
2294    let dir = Path::new(&config.paths.output_dir);
2295    let mut open = crate::open_loops::read_open(&dir.join("open_loops.bin"));
2296    open.truncate(k);
2297
2298    if open.is_empty() {
2299        return Ok("No open loops.".to_string());
2300    }
2301
2302    let mut output = format!("Open Loops ({}):\n\n", open.len());
2303    for e in &open {
2304        output.push_str(&format!(
2305            "#{} {} imp={} {}\n",
2306            e.id,
2307            crate::timeline::format_ts(e.ts_ms),
2308            e.importance,
2309            crate::safe_truncate(&e.text, 100)
2310        ));
2311    }
2312    Ok(output)
2313}
2314
2315// ─── Resolve Loop Tool ──────────────────────────────
2316
2317fn tool_resolve_loop(config: &Config, args: &Value) -> Result<String, String> {
2318    let id = args
2319        .get("id")
2320        .and_then(|v| v.as_u64())
2321        .ok_or("Missing required parameter: id")?;
2322    let dir = Path::new(&config.paths.output_dir);
2323    match crate::open_loops::resolve(dir, id) {
2324        Ok(true) => Ok(format!("Loop #{} resolved.", id)),
2325        Ok(false) => Ok(format!("Loop #{} not found or already resolved.", id)),
2326        Err(e) => Err(format!("Error resolving loop #{}: {}", id, e)),
2327    }
2328}
2329
2330// ─── Radial Search Tool ─────────────────────────────
2331
2332fn tool_radial(config: &Config, args: &Value) -> Result<String, String> {
2333    let x = args.get("x").and_then(|v| v.as_f64()).ok_or("Missing: x")? as f32;
2334    let y = args.get("y").and_then(|v| v.as_f64()).ok_or("Missing: y")? as f32;
2335    let z = args.get("z").and_then(|v| v.as_f64()).ok_or("Missing: z")? as f32;
2336    let depth = args
2337        .get("depth")
2338        .and_then(|v| v.as_u64())
2339        .ok_or("Missing: depth")? as u8;
2340    let radius = args.get("radius").and_then(|v| v.as_f64()).unwrap_or(0.1) as f32;
2341    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
2342
2343    let reader = MicroscopeReader::open(config)?;
2344    let result_set = reader.radial_search(config, x, y, z, depth, radius, k);
2345    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
2346    let appended = read_append_log(&append_path);
2347
2348    let mut output = format!(
2349        "Radial ({:.2},{:.2},{:.2}) D{} r={:.3}:\n\n",
2350        x, y, z, depth, radius
2351    );
2352
2353    if let Some(ref primary) = result_set.primary {
2354        output.push_str("PRIMARY:\n");
2355        if primary.is_main {
2356            let h = reader.header(primary.block_idx);
2357            let text = reader.text(primary.block_idx);
2358            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2359            output.push_str(&format!(
2360                "  [D{} {} dist={:.3}] {}\n",
2361                h.depth,
2362                layer,
2363                primary.dist_sq,
2364                crate::safe_truncate(text, 150)
2365            ));
2366        } else if let Some(entry) = appended.get(primary.block_idx) {
2367            let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
2368            output.push_str(&format!(
2369                "  [APPEND {} dist={:.3}] {}\n",
2370                layer,
2371                primary.dist_sq,
2372                crate::safe_truncate(&entry.text, 150)
2373            ));
2374        }
2375    }
2376
2377    if !result_set.neighbors.is_empty() {
2378        output.push_str(&format!("\nNeighbors ({}):\n", result_set.neighbors.len()));
2379        for n in &result_set.neighbors {
2380            if n.is_main {
2381                let h = reader.header(n.block_idx);
2382                let text = reader.text(n.block_idx);
2383                let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2384                output.push_str(&format!(
2385                    "  [D{} {} dist={:.3} w={:.3}] {}\n",
2386                    h.depth,
2387                    layer,
2388                    n.dist_sq,
2389                    n.weight,
2390                    crate::safe_truncate(text, 100)
2391                ));
2392            } else if let Some(entry) = appended.get(n.block_idx) {
2393                let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
2394                output.push_str(&format!(
2395                    "  [APPEND {} dist={:.3}] {}\n",
2396                    layer,
2397                    n.dist_sq,
2398                    crate::safe_truncate(&entry.text, 100)
2399                ));
2400            }
2401        }
2402    }
2403
2404    output.push_str(&format!(
2405        "\n{} within radius, {} shown",
2406        result_set.total_within_radius,
2407        result_set.all().len()
2408    ));
2409    Ok(output)
2410}
2411
2412// ─── Soft Zoom Tool ─────────────────────────────────
2413
2414fn tool_soft(config: &Config, args: &Value) -> Result<String, String> {
2415    let x = args.get("x").and_then(|v| v.as_f64()).ok_or("Missing: x")? as f32;
2416    let y = args.get("y").and_then(|v| v.as_f64()).ok_or("Missing: y")? as f32;
2417    let z = args.get("z").and_then(|v| v.as_f64()).ok_or("Missing: z")? as f32;
2418    let zoom = args
2419        .get("zoom")
2420        .and_then(|v| v.as_u64())
2421        .ok_or("Missing: zoom")? as u8;
2422    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
2423
2424    let reader = MicroscopeReader::open(config)?;
2425    let config_clone = config.clone();
2426    let results = reader.look_soft(&config_clone, x, y, z, zoom, k, config.search.zoom_weight);
2427    let append_path = Path::new(&config.paths.output_dir).join("append.bin");
2428    let appended = read_append_log(&append_path);
2429
2430    if results.is_empty() {
2431        return Ok(format!(
2432            "Soft ({:.2},{:.2},{:.2}) zoom={}: no results",
2433            x, y, z, zoom
2434        ));
2435    }
2436
2437    let mut output = format!(
2438        "Soft 4D ({:.2},{:.2},{:.2}) zoom={}: {} results\n\n",
2439        x,
2440        y,
2441        z,
2442        zoom,
2443        results.len()
2444    );
2445    for (dist, idx, is_main) in &results {
2446        if *is_main {
2447            let h = reader.header(*idx);
2448            let text = reader.text(*idx);
2449            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2450            output.push_str(&format!(
2451                "[D{} {} dist={:.3}] {}\n",
2452                h.depth,
2453                layer,
2454                dist,
2455                crate::safe_truncate(text, 150)
2456            ));
2457        } else if let Some(entry) = appended.get(*idx) {
2458            let layer = LAYER_NAMES.get(entry.layer_id as usize).unwrap_or(&"?");
2459            output.push_str(&format!(
2460                "[APPEND {} dist={:.3}] {}\n",
2461                layer,
2462                dist,
2463                crate::safe_truncate(&entry.text, 150)
2464            ));
2465        }
2466    }
2467    Ok(output)
2468}
2469
2470// ─── Think Tool ─────────────────────────────────────
2471
2472fn tool_think(config: &Config, args: &Value) -> Result<String, String> {
2473    let query = args
2474        .get("query")
2475        .and_then(|v| v.as_str())
2476        .ok_or("Missing: query")?;
2477    let max_steps = args.get("max_steps").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
2478
2479    let reader = MicroscopeReader::open(config)?;
2480    let mut chain = crate::sequential_thinking::ThinkingChain::new(max_steps);
2481    chain.brainstorm(&reader, config, query);
2482
2483    let mut output = format!("Sequential Thinking: '{}' ({} steps)\n\n", query, max_steps);
2484    // ThinkingChain has a display() method — we capture its output
2485    // Since display() prints to stdout, we'll build output manually
2486    output.push_str(&format!("Topic: {}\n", query));
2487    output.push_str(&format!("Steps: {}\n", max_steps));
2488    output.push_str("Run the CLI 'microscope-mem think' for full formatted output.\n");
2489    Ok(output)
2490}
2491
2492// ─── Hebbian Tool ───────────────────────────────────
2493
2494fn tool_hebbian(config: &Config, _args: &Value) -> Result<String, String> {
2495    let reader = MicroscopeReader::open(config)?;
2496    let output_dir = Path::new(&config.paths.output_dir);
2497    let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, reader.block_count);
2498    let stats = hebb.stats();
2499
2500    let mut output = format!(
2501        "Hebbian State:\n\
2502         ==============\n\
2503         Blocks:             {}\n\
2504         Active blocks:      {}\n\
2505         Total activations:  {}\n\
2506         Hot blocks (>0.1):  {}\n\
2507         Drifted blocks:     {}\n\
2508         Co-activation pairs: {}\n\
2509         Fingerprints:       {}\n",
2510        stats.block_count,
2511        stats.active_blocks,
2512        stats.total_activations,
2513        stats.hot_blocks,
2514        stats.drifted_blocks,
2515        stats.coactivation_pairs,
2516        stats.fingerprint_count,
2517    );
2518
2519    let top = hebb.strongest_pairs(5);
2520    if !top.is_empty() {
2521        output.push_str("\nStrongest co-activations:\n");
2522        for pair in top {
2523            let text_a = crate::safe_truncate(reader.text(pair.block_a as usize), 30);
2524            let text_b = crate::safe_truncate(reader.text(pair.block_b as usize), 30);
2525            output.push_str(&format!(
2526                "  {}x  [{}] <-> [{}]\n",
2527                pair.count, text_a, text_b
2528            ));
2529        }
2530    }
2531    Ok(output)
2532}
2533
2534// ─── Hottest Tool ───────────────────────────────────
2535
2536fn tool_hottest(config: &Config, args: &Value) -> Result<String, String> {
2537    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
2538    let reader = MicroscopeReader::open(config)?;
2539    let output_dir = Path::new(&config.paths.output_dir);
2540    let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, reader.block_count);
2541    let hot = hebb.hottest_blocks(k);
2542
2543    if hot.is_empty() {
2544        return Ok("No active blocks — run some queries first.".to_string());
2545    }
2546
2547    let mut output = format!("Hottest {} blocks:\n\n", k);
2548    for (idx, energy) in &hot {
2549        let h = reader.header(*idx);
2550        let text = reader.text(*idx);
2551        let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2552        let rec = &hebb.activations[*idx];
2553        output.push_str(&format!(
2554            "E={:.3} D{} [{}] count={} drift=({:.3},{:.3},{:.3}) {}\n",
2555            energy,
2556            h.depth,
2557            layer,
2558            rec.activation_count,
2559            rec.drift_x,
2560            rec.drift_y,
2561            rec.drift_z,
2562            crate::safe_truncate(text, 80)
2563        ));
2564    }
2565    Ok(output)
2566}
2567
2568// ─── Archetypes Tool ───────────────────────────────
2569
2570fn tool_archetypes(config: &Config, _args: &Value) -> Result<String, String> {
2571    let output_dir = Path::new(&config.paths.output_dir);
2572    let arc = crate::archetype::ArchetypeState::load_or_init(output_dir);
2573    let stats = arc.stats();
2574
2575    let mut output = format!(
2576        "Archetypes:\n\
2577         ==========\n\
2578         Emerged:            {}\n\
2579         Total members:      {}\n",
2580        stats.archetype_count, stats.total_members,
2581    );
2582    if let (Some(label), Some(str)) = (&stats.strongest_label, stats.strongest_strength) {
2583        output.push_str(&format!(
2584            "Strongest:          '{}' (str={:.3})\n",
2585            label, str
2586        ));
2587    }
2588
2589    if !arc.archetypes.is_empty() {
2590        output.push_str("\nAll archetypes:\n");
2591        for a in &arc.archetypes {
2592            output.push_str(&format!(
2593                "  #{} '{}' str={:.3} members={} reinforced={}x\n",
2594                a.id,
2595                a.label,
2596                a.strength,
2597                a.members.len(),
2598                a.reinforcement_count,
2599            ));
2600        }
2601    }
2602    Ok(output)
2603}
2604
2605// ─── Patterns Tool ─────────────────────────────────
2606
2607fn tool_patterns(config: &Config, args: &Value) -> Result<String, String> {
2608    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
2609    let output_dir = Path::new(&config.paths.output_dir);
2610    let tg = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
2611    let stats = tg.stats();
2612
2613    let mut output = format!(
2614        "Thought Graph:\n\
2615         =============\n\
2616         nodes={} edges={} patterns={} (crystallized={}) session=#{}\n",
2617        stats.node_count,
2618        stats.edge_count,
2619        stats.pattern_count,
2620        stats.crystallized,
2621        stats.current_session_id,
2622    );
2623
2624    let top = tg.top_patterns(k);
2625    if top.is_empty() {
2626        output.push_str("\n(no patterns yet — recall more to form thought paths)\n");
2627    } else {
2628        output.push_str("\nTop patterns:\n");
2629        for (i, p) in top.iter().enumerate() {
2630            let seq_str: Vec<String> = p
2631                .sequence
2632                .iter()
2633                .map(|h| format!("{:04x}", h & 0xFFFF))
2634                .collect();
2635            let crystallized = if p.frequency >= 3 { "*" } else { " " };
2636            output.push_str(&format!(
2637                "{}#{} {} freq={} str={:.2} blocks={}\n",
2638                crystallized,
2639                i + 1,
2640                seq_str.join(" → "),
2641                p.frequency,
2642                p.strength,
2643                p.result_blocks.len()
2644            ));
2645        }
2646    }
2647    Ok(output)
2648}
2649
2650// ─── Attention Tool ────────────────────────────────
2651
2652fn tool_attention(config: &Config, _args: &Value) -> Result<String, String> {
2653    let output_dir = Path::new(&config.paths.output_dir);
2654    let attn = crate::attention::AttentionState::load_or_init(output_dir);
2655
2656    let mut output = format!(
2657        "Attention:\n\
2658         =========\n\
2659         Total recalls: {}\n\
2660         History:       {}\n\n",
2661        attn.total_recalls,
2662        attn.history.len(),
2663    );
2664
2665    output.push_str("Learned layer weights:\n");
2666    for (i, name) in crate::attention::LAYER_NAMES.iter().enumerate() {
2667        let w = attn.learned_weights[i];
2668        let bar_len = (w * 10.0) as usize;
2669        let bar: String = "█".repeat(bar_len.min(30));
2670        output.push_str(&format!("  {:<16} {:.3} {}\n", name, w, bar));
2671    }
2672
2673    if !attn.history.is_empty() {
2674        let recent: Vec<&crate::attention::AttentionOutcome> =
2675            attn.history.iter().rev().take(5).collect();
2676        output.push_str("\nRecent outcomes:\n");
2677        for o in recent {
2678            let symbol = if o.quality >= 0.7 {
2679                "+"
2680            } else if o.quality <= 0.3 {
2681                "-"
2682            } else {
2683                "~"
2684            };
2685            output.push_str(&format!("  {} quality={:.2}\n", symbol, o.quality));
2686        }
2687    }
2688    Ok(output)
2689}
2690
2691// ─── Introspect Tool ────────────────────────────────
2692
2693fn tool_introspect(config: &Config, _args: &Value) -> Result<String, String> {
2694    let reader = MicroscopeReader::open(config)?;
2695    let output_dir = Path::new(&config.paths.output_dir);
2696    let reflection = crate::self_reflect::introspect(config, &reader, output_dir);
2697    Ok(crate::self_reflect::format_reflection(&reflection))
2698}
2699
2700// ─── Self Model Tool ───────────────────────────────
2701
2702fn tool_self_model(config: &Config, _args: &Value) -> Result<String, String> {
2703    let reader = MicroscopeReader::open(config)?;
2704    let output_dir = Path::new(&config.paths.output_dir);
2705    let mut self_model = crate::self_model::SelfModel::load_or_init(output_dir);
2706    let snap = self_model.take_snapshot(config, &reader, output_dir);
2707    let change = self_model.describe_change();
2708    Ok(crate::self_model::format_self_model(&snap, &change))
2709}
2710
2711// ─── Curiosity Tool ─────────────────────────────────
2712
2713fn tool_curiosity(config: &Config, _args: &Value) -> Result<String, String> {
2714    let reader = MicroscopeReader::open(config)?;
2715    let output_dir = Path::new(&config.paths.output_dir);
2716    let mut curiosity = crate::curiosity::CuriosityState::load_or_init(output_dir);
2717    let queries = curiosity.generate_queries(config, &reader, output_dir);
2718    if queries.is_empty() {
2719        return Ok("No curiosity queries generated.".to_string());
2720    }
2721    Ok(crate::curiosity::format_curiosity(&queries))
2722}
2723
2724// ─── Monologue Tool ─────────────────────────────────
2725
2726fn tool_monologue(config: &Config, _args: &Value) -> Result<String, String> {
2727    let reader = MicroscopeReader::open(config)?;
2728    let output_dir = Path::new(&config.paths.output_dir);
2729    let mut monologue = crate::inner_monologue::MonologueState::load_or_init(output_dir);
2730    let entry = monologue.generate_monologue(config, &reader, output_dir);
2731    Ok(crate::inner_monologue::format_monologue(&entry))
2732}
2733
2734// ─── Stories Tool ───────────────────────────────────
2735
2736fn tool_stories(config: &Config, args: &Value) -> Result<String, String> {
2737    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
2738    let _reader = MicroscopeReader::open(config)?;
2739    let output_dir = Path::new(&config.paths.output_dir);
2740    let nm = crate::narrative_memory::NarrativeMemory::load_or_init(output_dir);
2741
2742    if nm.episodes.is_empty() {
2743        return Ok("No narrative episodes yet — recall some memories first.".to_string());
2744    }
2745
2746    let mut output = format!(
2747        "Narrative Episodes (last {} of {}):\n\n",
2748        k,
2749        nm.episodes.len()
2750    );
2751    for ep in nm.episodes.iter().rev().take(k) {
2752        output.push_str(&crate::narrative_memory::format_episode(ep));
2753        output.push_str("\n---\n");
2754    }
2755    Ok(output)
2756}
2757
2758// ─── Daydream Tool ──────────────────────────────────
2759
2760fn tool_daydream(config: &Config, args: &Value) -> Result<String, String> {
2761    let seed = args.get("seed").and_then(|v| v.as_str()).unwrap_or("");
2762    let steps = args.get("steps").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
2763
2764    let reader = MicroscopeReader::open(config)?;
2765    let output_dir = Path::new(&config.paths.output_dir);
2766
2767    let seed_text = if seed.is_empty() {
2768        // Try to get last narrative as seed
2769        let narrative = crate::narrative::NarrativeState::load_or_init(output_dir);
2770        if narrative.narrative.is_empty() {
2771            "microscope memory".to_string()
2772        } else {
2773            narrative.narrative.clone()
2774        }
2775    } else {
2776        seed.to_string()
2777    };
2778
2779    let mut output = format!(
2780        "Daydream (seed: '{}', steps: {}):\n\n",
2781        crate::safe_truncate(&seed_text, 50),
2782        steps
2783    );
2784    let mut current = seed_text;
2785
2786    for step in 0..steps {
2787        let (qx, qy, qz) = crate::content_coords(&current, "associative");
2788        let config_clone = config.clone();
2789        let results = reader.look(&config_clone, qx, qy, qz, 3, 3);
2790        if results.is_empty() {
2791            output.push_str(&format!("  Step {}: (no associations found)\n", step + 1));
2792            break;
2793        }
2794        let (_, idx, _) = results[0];
2795        let next_text = reader.text(idx);
2796        output.push_str(&format!(
2797            "  Step {}: {}\n",
2798            step + 1,
2799            crate::safe_truncate(next_text, 120)
2800        ));
2801        current = next_text.to_string();
2802    }
2803    Ok(output)
2804}
2805
2806// ─── Hyperfocus Tool ────────────────────────────────
2807
2808fn tool_hyperfocus(config: &Config, args: &Value) -> Result<String, String> {
2809    let target = args
2810        .get("target")
2811        .and_then(|v| v.as_str())
2812        .ok_or("Missing: target")?;
2813    let focus_type = args
2814        .get("focus_type")
2815        .and_then(|v| v.as_str())
2816        .unwrap_or("research");
2817
2818    let reader = MicroscopeReader::open(config)?;
2819    let _output_dir = Path::new(&config.paths.output_dir);
2820
2821    // Store hyperfocus intent
2822    let focus_text = format!("[HYPERFOCUS:{}] Target: {}", focus_type, target);
2823    let _ = store_memory(config, &focus_text, "session", 9);
2824
2825    // Recall related memories
2826    let (qx, qy, qz) = crate::content_coords(target, "long_term");
2827    let config_clone = config.clone();
2828    let results = reader.look(&config_clone, qx, qy, qz, 2, 10);
2829
2830    let mut output = format!(
2831        "Hyperfocus: '{}' (type: {})\n\
2832         ============================\n\n\
2833         Focus intent stored in session layer (importance=9).\n\n",
2834        target, focus_type
2835    );
2836
2837    if results.is_empty() {
2838        output.push_str("No related memories found — starting fresh.\n");
2839    } else {
2840        output.push_str("Related memories:\n");
2841        for (dist, idx, _) in &results {
2842            let h = reader.header(*idx);
2843            let text = reader.text(*idx);
2844            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2845            output.push_str(&format!(
2846                "  [D{} {} dist={:.3}] {}\n",
2847                h.depth,
2848                layer,
2849                dist,
2850                crate::safe_truncate(text, 120)
2851            ));
2852        }
2853    }
2854    Ok(output)
2855}
2856
2857// ─── Emotional Field Tool ───────────────────────────
2858
2859fn tool_emotional_field(config: &Config, _args: &Value) -> Result<String, String> {
2860    let reader = MicroscopeReader::open(config)?;
2861    let output_dir = Path::new(&config.paths.output_dir);
2862    let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, reader.block_count);
2863
2864    let mut output = String::from("Emotional Field:\n================\n");
2865
2866    match crate::emotional::emotional_field(&reader, &hebb) {
2867        Some(field) => {
2868            output.push_str(&format!("Total energy: {:.3}\n", field.total_energy));
2869            output.push_str(&format!(
2870                "Centroid: ({:.3}, {:.3}, {:.3})\n",
2871                field.centroid.0, field.centroid.1, field.centroid.2
2872            ));
2873            output.push_str(&format!("Active blocks: {}\n", field.active_blocks));
2874        }
2875        None => {
2876            output.push_str("(no emotional field data — run queries to build state)\n");
2877        }
2878    }
2879
2880    // Emotional state ring
2881    let ring = crate::EmotionalStateRing::load_or_init(output_dir);
2882    if ring.is_active() {
2883        output.push_str("\nEmotional State Ring:\n");
2884        output.push_str(&format!("  Intensity: {:.3}\n", ring.intensity()));
2885        if let Some((name, val)) = ring.dominant() {
2886            output.push_str(&format!("  Dominant: {} ({:.3})\n", name, val));
2887        }
2888    }
2889
2890    Ok(output)
2891}
2892
2893// ─── Embed Tool ─────────────────────────────────────
2894
2895fn tool_embed(config: &Config, args: &Value) -> Result<String, String> {
2896    let query = args
2897        .get("query")
2898        .and_then(|v| v.as_str())
2899        .ok_or("Missing: query")?;
2900    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
2901    let metric = args
2902        .get("metric")
2903        .and_then(|v| v.as_str())
2904        .unwrap_or("cosine");
2905
2906    use crate::embedding_index::EmbeddingIndex;
2907
2908    let reader = MicroscopeReader::open(config)?;
2909    let output_dir = Path::new(&config.paths.output_dir);
2910    let emb_path = output_dir.join("embeddings.bin");
2911
2912    // Match the provider dimension to the index; fall back to the configured
2913    // dim when embeddings.bin is absent so recall never queries at dim 128.
2914    let index = EmbeddingIndex::open(&emb_path);
2915    let idx_dim = index
2916        .as_ref()
2917        .map(|idx| idx.dim())
2918        .unwrap_or(config.embedding.dim);
2919    let provider = crate::embeddings::provider_from_config(&config.embedding, idx_dim);
2920    let query_embedding = provider
2921        .embed(query)
2922        .map_err(|e| format!("Embedding failed: {}", e))?;
2923
2924    let mut output = format!("Semantic Search '{}' (metric: {}):\n\n", query, metric);
2925
2926    if let Some(idx) = index {
2927        let results = idx.search(&query_embedding, k);
2928        if results.is_empty() {
2929            output.push_str("(no results)\n");
2930        }
2931        for (sim, block_idx) in results {
2932            let h = reader.header(block_idx);
2933            let text = reader.text(block_idx);
2934            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2935            output.push_str(&format!(
2936                "[D{} {} sim={:.3}] {}\n",
2937                h.depth,
2938                layer,
2939                sim,
2940                crate::safe_truncate(text, 120)
2941            ));
2942        }
2943    } else {
2944        // On-the-fly search
2945        let mut results: Vec<(f32, usize)> = Vec::new();
2946        for i in 0..reader.block_count {
2947            let text = reader.text(i);
2948            if let Ok(block_emb) = provider.embed(text) {
2949                let similarity: f32 = block_emb
2950                    .iter()
2951                    .zip(query_embedding.iter())
2952                    .map(|(a, b)| a * b)
2953                    .sum::<f32>()
2954                    / (block_emb.iter().map(|v| v * v).sum::<f32>().sqrt()
2955                        * query_embedding.iter().map(|v| v * v).sum::<f32>().sqrt()
2956                        + 1e-10);
2957                if similarity > 0.5 {
2958                    results.push((similarity, i));
2959                }
2960            }
2961        }
2962        results.sort_by(|a, b| b.0.total_cmp(&a.0));
2963        results.truncate(k);
2964
2965        if results.is_empty() {
2966            output.push_str("(no results)\n");
2967        }
2968        for (sim, idx) in results {
2969            let h = reader.header(idx);
2970            let text = reader.text(idx);
2971            let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
2972            output.push_str(&format!(
2973                "[D{} {} sim={:.3}] {}\n",
2974                h.depth,
2975                layer,
2976                sim,
2977                crate::safe_truncate(text, 120)
2978            ));
2979        }
2980    }
2981    Ok(output)
2982}
2983
2984// ─── Similar Tool ───────────────────────────────────
2985
2986fn tool_similar(config: &Config, args: &Value) -> Result<String, String> {
2987    let text = args
2988        .get("text")
2989        .and_then(|v| v.as_str())
2990        .ok_or("Missing: text")?;
2991    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
2992
2993    let reader = MicroscopeReader::open(config)?;
2994    let output_dir = Path::new(&config.paths.output_dir);
2995    let table = crate::fingerprint::LinkTable::load(output_dir)
2996        .ok_or("fingerprints.idx not found — run 'fingerprint' first")?;
2997
2998    let results = table.find_similar(text, k);
2999    if results.is_empty() {
3000        return Ok(format!(
3001            "No structurally similar blocks found for '{}'",
3002            crate::safe_truncate(text, 40)
3003        ));
3004    }
3005
3006    let mut output = format!(
3007        "Structurally similar to '{}':\n\n",
3008        crate::safe_truncate(text, 40)
3009    );
3010    for (idx, sim) in &results {
3011        let h = reader.header(*idx as usize);
3012        let bt = reader.text(*idx as usize);
3013        let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
3014        output.push_str(&format!(
3015            "#{} D{} [{}] sim={:.3} {}\n",
3016            idx,
3017            h.depth,
3018            layer,
3019            sim,
3020            crate::safe_truncate(bt, 100)
3021        ));
3022    }
3023    Ok(output)
3024}
3025
3026// ─── Links Tool ─────────────────────────────────────
3027
3028fn tool_links(config: &Config, args: &Value) -> Result<String, String> {
3029    let block_index = args
3030        .get("block_index")
3031        .and_then(|v| v.as_u64())
3032        .ok_or("Missing: block_index")? as usize;
3033
3034    let reader = MicroscopeReader::open(config)?;
3035    let output_dir = Path::new(&config.paths.output_dir);
3036    let table = crate::fingerprint::LinkTable::load(output_dir)
3037        .ok_or("fingerprints.idx not found — run 'fingerprint' first")?;
3038
3039    let links = table.linked_blocks(block_index as u32);
3040    let h = reader.header(block_index);
3041    let text = reader.text(block_index);
3042    let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
3043
3044    let mut output = format!(
3045        "Links for Block #{} D{} [{}] {}\n\n",
3046        block_index,
3047        h.depth,
3048        layer,
3049        crate::safe_truncate(text, 60)
3050    );
3051
3052    if links.is_empty() {
3053        output.push_str("(no structural links)\n");
3054    } else {
3055        for (target, sim) in &links {
3056            let th = reader.header(*target as usize);
3057            let tt = reader.text(*target as usize);
3058            let tl = LAYER_NAMES.get(th.layer_id as usize).unwrap_or(&"?");
3059            output.push_str(&format!(
3060                "  -> #{} D{} [{}] sim={:.3} {}\n",
3061                target,
3062                th.depth,
3063                tl,
3064                sim,
3065                crate::safe_truncate(tt, 80)
3066            ));
3067        }
3068    }
3069    Ok(output)
3070}
3071
3072// ─── Fingerprint Tool ───────────────────────────────
3073
3074fn tool_fingerprint(config: &Config, _args: &Value) -> Result<String, String> {
3075    let reader = MicroscopeReader::open(config)?;
3076    let output_dir = Path::new(&config.paths.output_dir);
3077
3078    let texts: Vec<&str> = (0..reader.block_count).map(|i| reader.text(i)).collect();
3079    let table = crate::fingerprint::LinkTable::build(&texts);
3080    table
3081        .save(output_dir)
3082        .map_err(|e| format!("Failed to save fingerprints: {}", e))?;
3083
3084    let stats = table.stats();
3085    Ok(format!(
3086        "Fingerprints built:\n\
3087         ==================\n\
3088         Avg entropy:        {:.3}\n\
3089         Unique hashes:      {}\n\
3090         Largest cluster:    {}\n\
3091         Structural links:   {}\n",
3092        stats.avg_entropy, stats.unique_hashes, stats.largest_cluster, stats.link_count,
3093    ))
3094}
3095
3096// ─── Dream Log Tool ─────────────────────────────────
3097
3098fn tool_dream_log(config: &Config, args: &Value) -> Result<String, String> {
3099    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
3100    let output_dir = Path::new(&config.paths.output_dir);
3101    let log_path = output_dir.join("dream_log.bin");
3102
3103    if !log_path.exists() {
3104        return Ok("No dream log found — run 'dream' first.".to_string());
3105    }
3106
3107    let data = std::fs::read(&log_path).unwrap_or_default();
3108    if data.len() < 4 {
3109        return Ok("Dream log is empty.".to_string());
3110    }
3111
3112    // Parse dream log entries (simple binary format: count + entries)
3113    let entry_size = 48; // approximate size per entry
3114    let count = data.len() / entry_size;
3115    let shown = count.min(k);
3116
3117    let mut output = format!(
3118        "Dream Log — {} cycles recorded, showing last {}:\n\n",
3119        count, shown
3120    );
3121    for i in (count - shown)..count {
3122        let offset = i * entry_size;
3123        if offset + 8 <= data.len() {
3124            let ts = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap_or([0; 8]));
3125            output.push_str(&format!(
3126                "  Cycle #{}: ts={}\n",
3127                i,
3128                crate::timeline::format_ts(ts)
3129            ));
3130        }
3131    }
3132    Ok(output)
3133}
3134
3135// ─── Resonance Tool ─────────────────────────────────
3136
3137fn tool_resonance(config: &Config, _args: &Value) -> Result<String, String> {
3138    let output_dir = Path::new(&config.paths.output_dir);
3139    let resonance = crate::resonance::ResonanceState::load_or_init(output_dir);
3140    let stats = resonance.stats();
3141
3142    Ok(format!(
3143        "Resonance Protocol:\n\
3144         ==================\n\
3145         Instance ID:        {:x}\n\
3146         Outgoing pulses:    {}\n\
3147         Incoming pulses:    {}\n\
3148         Pending integration:{}\n\
3149         Unique sources:     {}\n\
3150         Field cells:        {}\n\
3151         Field energy:       {:.3}\n",
3152        stats.instance_id,
3153        stats.outgoing_pulses,
3154        stats.incoming_pulses,
3155        stats.pending_integration,
3156        stats.unique_sources,
3157        stats.field_cells,
3158        stats.field_energy,
3159    ))
3160}
3161
3162// ─── Mirror Tool ────────────────────────────────────
3163
3164fn tool_mirror(config: &Config, _args: &Value) -> Result<String, String> {
3165    let output_dir = Path::new(&config.paths.output_dir);
3166    let mirror = crate::mirror::MirrorState::load_or_init(output_dir);
3167    let stats = mirror.stats();
3168
3169    let mut output = format!(
3170        "Mirror Neuron State:\n\
3171         ===================\n\
3172         Resonance echoes:   {}\n\
3173         Resonant blocks:    {}\n\
3174         Avg similarity:     {:.3}\n",
3175        stats.total_echoes, stats.resonant_blocks, stats.avg_similarity,
3176    );
3177
3178    if let Some((idx, strength)) = stats.strongest_block {
3179        let reader = MicroscopeReader::open(config)?;
3180        let text = reader.text(idx as usize);
3181        output.push_str(&format!(
3182            "Strongest:          block #{} (str={:.3}) {}\n",
3183            idx,
3184            strength,
3185            crate::safe_truncate(text, 60)
3186        ));
3187    }
3188
3189    if !mirror.echoes.is_empty() {
3190        output.push_str("\nRecent echoes:\n");
3191        for echo in mirror.echoes.iter().rev().take(5) {
3192            output.push_str(&format!(
3193                "  sim={:.3} shared={} blocks trigger={:x} echo={:x}\n",
3194                echo.similarity,
3195                echo.shared_blocks.len(),
3196                echo.trigger_hash,
3197                echo.echo_hash,
3198            ));
3199        }
3200    }
3201    Ok(output)
3202}
3203
3204// ─── Predictions Tool ────────────────────────────────
3205
3206fn tool_predictions(config: &Config, _args: &Value) -> Result<String, String> {
3207    let output_dir = Path::new(&config.paths.output_dir);
3208    let cache = crate::predictive_cache::PredictiveCache::load_or_init(output_dir);
3209    let stats = &cache.stats;
3210
3211    let mut output = format!(
3212        "Predictive Cache:\n\
3213         ================\n\
3214         Predictions:    {}\n\
3215         Hits:           {}\n\
3216         Misses:         {}\n\
3217         Partial hits:   {}\n\
3218         Hit rate:       {:.1}%\n\
3219         Active:         {}\n\
3220         Avg confidence: {:.1}%\n",
3221        stats.total_predictions,
3222        stats.total_hits,
3223        stats.total_misses,
3224        stats.total_partial_hits,
3225        stats.hit_rate() * 100.0,
3226        stats.current_predictions,
3227        stats.avg_confidence * 100.0,
3228    );
3229
3230    if !cache.predictions.is_empty() {
3231        output.push_str("\nActive predictions:\n");
3232        for (i, p) in cache.predictions.iter().enumerate() {
3233            output.push_str(&format!(
3234                "  #{} hash={:04x} blocks={} conf={:.0}% pattern=#{}\n",
3235                i + 1,
3236                p.predicted_query_hash & 0xFFFF,
3237                p.blocks.len(),
3238                p.confidence * 100.0,
3239                p.pattern_id
3240            ));
3241        }
3242    }
3243    Ok(output)
3244}
3245
3246// ─── Paths Tool ──────────────────────────────────────
3247
3248fn tool_paths(config: &Config, args: &Value) -> Result<String, String> {
3249    let sessions = args.get("sessions").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
3250    let output_dir = Path::new(&config.paths.output_dir);
3251    let tg = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
3252    let recent = tg.recent_sessions(sessions);
3253
3254    if recent.is_empty() {
3255        return Ok("No recall sessions recorded yet.".to_string());
3256    }
3257
3258    let mut output = format!("Thought Paths (last {} sessions):\n\n", sessions);
3259    for (si, session) in recent.iter().enumerate() {
3260        if let Some(first) = session.first() {
3261            let path_str: Vec<String> = session
3262                .iter()
3263                .map(|n| format!("{:04x}", n.query_hash & 0xFFFF))
3264                .collect();
3265            output.push_str(&format!(
3266                "Session #{} ({} recalls): {}\n",
3267                first.session_id,
3268                session.len(),
3269                path_str.join(" → ")
3270            ));
3271        }
3272        if si >= sessions {
3273            break;
3274        }
3275    }
3276    Ok(output)
3277}
3278
3279// ─── Temporal Patterns Tool ─────────────────────────
3280
3281fn tool_temporal_patterns(config: &Config, _args: &Value) -> Result<String, String> {
3282    let output_dir = Path::new(&config.paths.output_dir);
3283    let temporal = crate::temporal_archetype::TemporalArchetypeState::load_or_init(output_dir);
3284    let window = crate::temporal_archetype::current_time_window();
3285
3286    let mut output = format!(
3287        "Temporal Archetypes (current window: {}):\n\n",
3288        crate::temporal_archetype::WINDOW_LABELS[window]
3289    );
3290
3291    if temporal.profiles.is_empty() {
3292        output.push_str("(no temporal data yet)\n");
3293    } else {
3294        for p in &temporal.profiles {
3295            let dominant = p
3296                .dominant_window()
3297                .map(|w| crate::temporal_archetype::WINDOW_LABELS[w])
3298                .unwrap_or("?");
3299            output.push_str(&format!(
3300                "Archetype #{} (total={}, dominant={})\n",
3301                p.archetype_id, p.total_activations, dominant
3302            ));
3303            for (i, label) in crate::temporal_archetype::WINDOW_LABELS.iter().enumerate() {
3304                let bar_len = (p.window_weights[i] * 5.0) as usize;
3305                let bar: String = "█".repeat(bar_len);
3306                let marker = if i == window { " ◀" } else { "" };
3307                output.push_str(&format!(
3308                    "  {} {:>3} {:.1} {}{}\n",
3309                    label, p.window_counts[i], p.window_weights[i], bar, marker
3310                ));
3311            }
3312        }
3313    }
3314    Ok(output)
3315}
3316
3317// ─── Modalities Tool ────────────────────────────────
3318
3319fn tool_modalities(config: &Config, _args: &Value) -> Result<String, String> {
3320    let reader = MicroscopeReader::open(config)?;
3321    let _output_dir = Path::new(&config.paths.output_dir);
3322
3323    let mut output = format!(
3324        "Multimodal Index:\n\
3325         =================\n\
3326         Total blocks: {}\n\n",
3327        reader.block_count
3328    );
3329
3330    // Layer breakdown
3331    output.push_str("Layer breakdown:\n");
3332    let mut layer_counts: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
3333    for i in 0..reader.block_count {
3334        let h = reader.header(i);
3335        *layer_counts.entry(h.layer_id).or_default() += 1;
3336    }
3337    for (lid, count) in layer_counts.iter() {
3338        let name = LAYER_NAMES.get(*lid as usize).unwrap_or(&"?");
3339        output.push_str(&format!("  {}: {} blocks\n", name, count));
3340    }
3341
3342    // Depth breakdown
3343    output.push_str("\nDepth breakdown:\n");
3344    for (d, &(_start, count)) in reader.depth_ranges.iter().enumerate() {
3345        if count > 0 {
3346            output.push_str(&format!("  D{}: {} blocks\n", d, count));
3347        }
3348    }
3349
3350    Ok(output)
3351}
3352
3353// ─── Doctor Tool ────────────────────────────────────
3354
3355fn tool_doctor(config: &Config, args: &Value) -> Result<String, String> {
3356    let fix = args.get("fix").and_then(|v| v.as_bool()).unwrap_or(false);
3357    crate::doctor::run_doctor(config, fix).map_err(|e| format!("Doctor failed: {}", e))?;
3358    Ok("Doctor diagnostics complete. Use --fix to attempt repairs.".to_string())
3359}
3360
3361// ─── Rebuild Tool ───────────────────────────────────
3362
3363fn tool_rebuild(config: &Config, _args: &Value) -> Result<String, String> {
3364    let outcome = crate::build::rebuild_pending(config, true, true)
3365        .map_err(|e| format!("Rebuild failed: {}", e))?;
3366    let reader = MicroscopeReader::open(config)?;
3367    Ok(format!(
3368        "Rebuild complete: {} blocks across {} depths.\nAppend log cleared after consolidating {} entries.",
3369        reader.block_count,
3370        reader.depth_ranges.iter().filter(|&&(_, c)| c > 0).count(),
3371        outcome.pending_entries
3372    ))
3373}
3374
3375// ─── Store Data Tool ────────────────────────────────
3376
3377fn tool_store_data(config: &Config, args: &Value) -> Result<String, String> {
3378    let pairs = args
3379        .get("pairs")
3380        .and_then(|v| v.as_object())
3381        .ok_or("Missing: pairs")?;
3382    let importance = args.get("importance").and_then(|v| v.as_u64()).unwrap_or(5) as u8;
3383
3384    let mut stored = Vec::new();
3385    for (key, val) in pairs {
3386        let val_str = val.as_str().unwrap_or("");
3387        let text = format!("[DATA] {} = {}", key, val_str);
3388        store_memory(config, &text, "long_term", importance)?;
3389        stored.push(format!("{} = {}", key, val_str));
3390    }
3391
3392    Ok(format!(
3393        "Stored {} data pairs (importance={}):\n  {}",
3394        stored.len(),
3395        importance,
3396        stored.join("\n  ")
3397    ))
3398}
3399
3400// ─── Resonant Tool ──────────────────────────────────
3401
3402fn tool_resonant(config: &Config, args: &Value) -> Result<String, String> {
3403    let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
3404    let reader = MicroscopeReader::open(config)?;
3405    let output_dir = Path::new(&config.paths.output_dir);
3406    let mirror = crate::mirror::MirrorState::load_or_init(output_dir);
3407    let top = mirror.most_resonant(k);
3408
3409    if top.is_empty() {
3410        return Ok("No resonant blocks — run queries to build mirror state.".to_string());
3411    }
3412
3413    let mut output = format!("Most Resonant {} blocks:\n\n", k);
3414    for (idx, res) in &top {
3415        let h = reader.header(*idx as usize);
3416        let text = reader.text(*idx as usize);
3417        let layer = LAYER_NAMES.get(h.layer_id as usize).unwrap_or(&"?");
3418        output.push_str(&format!(
3419            "S={:.3} D{} [{}] echoes={} {}\n",
3420            res.strength,
3421            h.depth,
3422            layer,
3423            res.echo_count,
3424            crate::safe_truncate(text, 80)
3425        ));
3426    }
3427    Ok(output)
3428}
3429
3430// ─── Autonomous Tool ────────────────────────────────
3431
3432fn tool_autonomous(config: &Config, args: &Value) -> Result<String, String> {
3433    let tts = args.get("tts").and_then(|v| v.as_bool()).unwrap_or(false);
3434    let daemon = args
3435        .get("daemon")
3436        .and_then(|v| v.as_bool())
3437        .unwrap_or(false);
3438    let interval = args.get("interval").and_then(|v| v.as_u64()).unwrap_or(30);
3439    let max_cycles = args.get("max_cycles").and_then(|v| v.as_u64());
3440
3441    let reader = MicroscopeReader::open(config)?;
3442    let output_dir = Path::new(&config.paths.output_dir);
3443
3444    let mut output = format!(
3445        "Autonomous Mode:\n\
3446         ===============\n\
3447         TTS: {}\n\
3448         Daemon: {}\n\
3449         Interval: {}s\n\
3450         Max cycles: {}\n\n",
3451        if tts { "enabled" } else { "disabled" },
3452        if daemon { "enabled" } else { "single cycle" },
3453        interval,
3454        max_cycles.map_or("unlimited".to_string(), |n| n.to_string()),
3455    );
3456
3457    // Run one autonomous cycle
3458    // Daydream
3459    let narrative = crate::narrative::NarrativeState::load_or_init(output_dir);
3460    let seed = if narrative.narrative.is_empty() {
3461        "microscope memory".to_string()
3462    } else {
3463        narrative.narrative.clone()
3464    };
3465    let (qx, qy, qz) = crate::content_coords(&seed, "associative");
3466    let config_clone = config.clone();
3467    let daydream_results = reader.look(&config_clone, qx, qy, qz, 3, 3);
3468    output.push_str("Daydream:\n");
3469    for (dist, idx, _) in daydream_results.iter().take(3) {
3470        let text = reader.text(*idx);
3471        output.push_str(&format!(
3472            "  [dist={:.3}] {}\n",
3473            dist,
3474            crate::safe_truncate(text, 100)
3475        ));
3476    }
3477
3478    // Curiosity
3479    let mut curiosity = crate::curiosity::CuriosityState::load_or_init(output_dir);
3480    let queries = curiosity.generate_queries(config, &reader, output_dir);
3481    if !queries.is_empty() {
3482        output.push_str("\nCuriosity:\n");
3483        for q in queries.iter().take(3) {
3484            output.push_str(&format!("  {}\n", crate::safe_truncate(&q.query, 100)));
3485        }
3486    }
3487
3488    // Monologue
3489    let mut monologue = crate::inner_monologue::MonologueState::load_or_init(output_dir);
3490    let entry = monologue.generate_monologue(config, &reader, output_dir);
3491    output.push_str(&format!(
3492        "\nMonologue:\n  {}\n",
3493        crate::safe_truncate(&entry.steps.join("\n  "), 200)
3494    ));
3495
3496    // Dream
3497    match crate::dream::dream_consolidate(
3498        output_dir,
3499        reader.block_count,
3500        config.index.max_blocks,
3501        config.index.protect_min_importance,
3502    ) {
3503        Ok(cycle) => {
3504            output.push_str(&format!(
3505                "\nDream: {}ms, {} strengthened, {} pruned, energy {:.3}→{:.3}\n",
3506                cycle.duration_ms,
3507                cycle.strengthened_pairs,
3508                cycle.pruned_pairs,
3509                cycle.energy_before,
3510                cycle.energy_after,
3511            ));
3512        }
3513        Err(e) => {
3514            output.push_str(&format!("\nDream: failed — {}\n", e));
3515        }
3516    }
3517
3518    if daemon {
3519        output.push_str(&format!(
3520            "\nDaemon mode: cycling every {}s. Use CLI to stop.",
3521            interval
3522        ));
3523    }
3524
3525    Ok(output)
3526}
3527
3528#[cfg(test)]
3529mod tests {
3530    use super::*;
3531    use crate::types::ProjectId;
3532
3533    // ── Helper: create a test config ───────────────────────────────────────
3534
3535    fn test_config() -> Config {
3536        Config {
3537            paths: crate::config::Paths {
3538                layers_dir: "./layers".to_string(),
3539                output_dir: "./data".to_string(),
3540                temp_dir: "./tmp".to_string(),
3541            },
3542            index: crate::config::Index {
3543                max_depth: 8,
3544                header_size: 32,
3545                auto_rebuild: false,
3546                auto_rebuild_entries: 25,
3547                layer_retention_entries: 2000,
3548                max_blocks: 0,
3549                protect_min_importance: 8,
3550                promote_energy_threshold: 0.35,
3551            },
3552            search: crate::config::Search {
3553                default_k: 10,
3554                zoom_weight: 2.0,
3555                keyword_boost: 0.1,
3556                semantic_weight: 0.0,
3557                semantic_rank_gain: 2.0,
3558                emotional_bias_weight: 0.0,
3559                emotion_21d_weight: 0.0,
3560            },
3561            memory_layers: crate::config::MemoryLayers {
3562                layers: vec![
3563                    "long_term".to_string(),
3564                    "short_term".to_string(),
3565                    "session".to_string(),
3566                ],
3567            },
3568            performance: crate::config::Performance {
3569                use_mmap: false,
3570                cache_size: 64,
3571                build_workers: 1,
3572                use_gpu: false,
3573                compression: false,
3574                cache_ttl_secs: 300,
3575            },
3576            logging: crate::config::Logging {
3577                level: "debug".to_string(),
3578                file: None,
3579            },
3580            embedding: crate::config::Embedding::default(),
3581            server: crate::config::Server::default(),
3582            federation: crate::config::Federation::default(),
3583            hooks: crate::config::HooksConfig::default(),
3584            project_id: ProjectId::GLOBAL,
3585            epistemic: crate::config::EpistemicConfig::default(),
3586        }
3587    }
3588
3589    // ── Test: read-only public mode ────────────────────────────────────────
3590
3591    #[test]
3592    fn test_hooks_read_only_public_mode() {
3593        let config = test_config();
3594        // Default HooksConfig has read_only=true
3595        assert!(
3596            config.hooks.read_only,
3597            "public demo must be read-only by default"
3598        );
3599        assert!(
3600            !config.hooks.write_enabled,
3601            "write must be disabled in public mode"
3602        );
3603
3604        let hook_config = if config.hooks.read_only {
3605            HookConfig::read_only()
3606        } else {
3607            HookConfig::default()
3608        };
3609
3610        assert!(hook_config.read_only);
3611        assert!(!hook_config.can_write());
3612        assert!(!hook_config.is_enabled(&HookEvent::AfterResponse));
3613    }
3614
3615    // ── Test: before_tool_call injection ────────────────────────────────────
3616
3617    #[test]
3618    fn test_before_tool_call_injection() {
3619        let config = test_config();
3620        let hook_config = if config.hooks.read_only {
3621            HookConfig::read_only()
3622        } else {
3623            HookConfig::default()
3624        };
3625        let manager = HookManager::new(hook_config);
3626
3627        let ctx = HookContext::new(HookEvent::BeforeToolCall)
3628            .with_tool("memory_recall", serde_json::json!({"query": "test"}));
3629
3630        let result = manager.execute(HookEvent::BeforeToolCall, ctx);
3631
3632        // before_tool_call should not produce memory candidates
3633        assert!(result.memory_candidates.is_empty());
3634        // Chain ID should be preserved
3635        assert!(!result.chain_id.is_empty());
3636        // Tool name should be preserved
3637        assert_eq!(result.tool_name, Some("memory_recall".to_string()));
3638    }
3639
3640    // ── Test: after_tool_call candidate creation ────────────────────────────
3641
3642    #[test]
3643    fn test_after_tool_call_candidate_creation() {
3644        let config = test_config();
3645        let hook_config = if config.hooks.read_only {
3646            HookConfig::read_only()
3647        } else {
3648            HookConfig::default()
3649        };
3650        let manager = HookManager::new(hook_config);
3651
3652        let ctx = HookContext::new(HookEvent::AfterToolCall)
3653            .with_tool("memory_recall", serde_json::json!({"query": "test"}))
3654            .with_response("Found 3 relevant memories about the project.");
3655
3656        let result = manager.execute(HookEvent::AfterToolCall, ctx);
3657
3658        // In read-only mode, candidates should be cleared
3659        assert!(
3660            result.memory_candidates.is_empty(),
3661            "read-only mode must clear all memory candidates"
3662        );
3663    }
3664
3665    // ── Test: after_tool_call creates candidates in full mode ───────────────
3666
3667    #[test]
3668    fn test_after_tool_call_candidate_full_mode() {
3669        let _config = test_config();
3670        let hook_config = HookConfig::full();
3671        let manager = HookManager::new(hook_config);
3672
3673        let mut ctx = HookContext::new(HookEvent::AfterToolCall)
3674            .with_tool("memory_recall", serde_json::json!({"query": "test"}));
3675        ctx.tool_result =
3676            Some("Found 3 relevant memories about the project requirements.".to_string());
3677
3678        let result = manager.execute(HookEvent::AfterToolCall, ctx);
3679
3680        // In full mode, candidates should be created
3681        assert!(
3682            !result.memory_candidates.is_empty(),
3683            "full mode must create memory candidates"
3684        );
3685        assert_eq!(
3686            result.memory_candidates[0].source_tool,
3687            Some("memory_recall".to_string())
3688        );
3689        assert_eq!(
3690            result.memory_candidates[0].source_event,
3691            HookEvent::AfterToolCall
3692        );
3693    }
3694
3695    // ── Test: error hook execution ──────────────────────────────────────────
3696
3697    #[test]
3698    fn test_error_hook_execution() {
3699        let config = test_config();
3700        let hook_config = if config.hooks.read_only {
3701            HookConfig::read_only()
3702        } else {
3703            HookConfig::default()
3704        };
3705        let manager = HookManager::new(hook_config);
3706
3707        let ctx = HookContext::new(HookEvent::Error)
3708            .with_tool("memory_store", serde_json::json!({"text": "test"}))
3709            .with_error("Index not found - run build first", "E1001");
3710
3711        let result = manager.execute(HookEvent::Error, ctx);
3712
3713        // In read-only mode, error candidates should be cleared
3714        assert!(
3715            result.memory_candidates.is_empty(),
3716            "read-only mode must clear error candidates"
3717        );
3718    }
3719
3720    // ── Test: error hook creates candidates in full mode ────────────────────
3721
3722    #[test]
3723    fn test_error_hook_full_mode() {
3724        let _config = test_config();
3725        let hook_config = HookConfig::full();
3726        let manager = HookManager::new(hook_config);
3727
3728        let ctx = HookContext::new(HookEvent::Error)
3729            .with_tool("memory_store", serde_json::json!({"text": "test"}))
3730            .with_error("Connection timeout", "E1002");
3731
3732        let result = manager.execute(HookEvent::Error, ctx);
3733
3734        assert!(
3735            !result.memory_candidates.is_empty(),
3736            "full mode must create error candidates"
3737        );
3738        assert!(result.memory_candidates[0].is_error);
3739        assert_eq!(result.memory_candidates[0].importance, 7);
3740    }
3741
3742    // ── Test: write hook disabled by default ───────────────────────────────
3743
3744    #[test]
3745    fn test_write_hook_disabled_by_default() {
3746        let config = test_config();
3747        // Default HooksConfig has write_enabled=false
3748        assert!(
3749            !config.hooks.write_enabled,
3750            "write must be disabled by default"
3751        );
3752
3753        let hook_config = HookConfig::default();
3754        assert!(
3755            !hook_config.can_write(),
3756            "HookConfig must deny writes by default"
3757        );
3758        assert!(
3759            !hook_config.is_enabled(&HookEvent::AfterResponse),
3760            "after_response hook must be disabled by default"
3761        );
3762    }
3763
3764    // ── Test: secret filtering ─────────────────────────────────────────────
3765
3766    #[test]
3767    fn test_secret_filtering() {
3768        let _config = test_config();
3769        let hook_config = HookConfig::full();
3770        let manager = HookManager::new(hook_config);
3771
3772        // Create a context with a secret-containing response
3773        let ctx = HookContext::new(HookEvent::AfterToolCall)
3774            .with_tool("memory_store", serde_json::json!({"text": "test"}))
3775            .with_response("The password is super-secret-123 and the API key is sk-test-key");
3776
3777        let result = manager.execute(HookEvent::AfterToolCall, ctx);
3778
3779        // Secret filtering should remove candidates containing secrets
3780        for candidate in &result.memory_candidates {
3781            assert!(
3782                !candidate.text.to_lowercase().contains("password"),
3783                "candidate must not contain secrets: {}",
3784                candidate.text
3785            );
3786            assert!(
3787                !candidate.text.to_lowercase().contains("sk-"),
3788                "candidate must not contain API keys: {}",
3789                candidate.text
3790            );
3791        }
3792    }
3793
3794    // ── Test: secret filtering in read-only mode ───────────────────────────
3795
3796    #[test]
3797    fn test_secret_filtering_read_only() {
3798        let _config = test_config();
3799        let hook_config = HookConfig::read_only();
3800        let manager = HookManager::new(hook_config);
3801
3802        let ctx = HookContext::new(HookEvent::AfterToolCall)
3803            .with_tool("memory_store", serde_json::json!({"text": "test"}))
3804            .with_response("The password is secret and the api_key=sk-abc123");
3805
3806        let result = manager.execute(HookEvent::AfterToolCall, ctx);
3807
3808        // In read-only mode, all candidates are cleared regardless
3809        assert!(
3810            result.memory_candidates.is_empty(),
3811            "read-only mode must clear all candidates even with secrets"
3812        );
3813    }
3814
3815    // ── Test: session start hook ────────────────────────────────────────────
3816
3817    #[test]
3818    fn test_session_start_hook() {
3819        let config = test_config();
3820        let hook_config = if config.hooks.read_only {
3821            HookConfig::read_only()
3822        } else {
3823            HookConfig::default()
3824        };
3825        let manager = HookManager::new(hook_config);
3826
3827        let ctx = HookContext::new(HookEvent::SessionStart);
3828        let result = manager.execute(HookEvent::SessionStart, ctx);
3829
3830        // Session start should load memory contract
3831        assert!(
3832            result.memory_contract.is_some(),
3833            "session start must load memory contract"
3834        );
3835        // Session start should load constraints
3836        assert!(
3837            !result.constraints.is_empty(),
3838            "session start must load constraints"
3839        );
3840    }
3841
3842    // ── Test: full lifecycle chain ─────────────────────────────────────────
3843
3844    #[test]
3845    fn test_full_lifecycle_chain() {
3846        let _config = test_config();
3847        let hook_config = HookConfig::full();
3848        let manager = HookManager::new(hook_config);
3849
3850        // Simulate a full lifecycle: session start -> before tool -> after tool
3851        let ctx = HookContext::new(HookEvent::SessionStart);
3852        let events = [
3853            HookEvent::SessionStart,
3854            HookEvent::BeforeToolCall,
3855            HookEvent::AfterToolCall,
3856        ];
3857
3858        let mut chain_ctx = ctx;
3859        for event in &events {
3860            chain_ctx = match event {
3861                HookEvent::BeforeToolCall => {
3862                    chain_ctx.with_tool("memory_recall", serde_json::json!({"query": "test"}))
3863                }
3864                HookEvent::AfterToolCall => {
3865                    chain_ctx.tool_result =
3866                        Some("Found relevant results from the memory search.".to_string());
3867                    chain_ctx
3868                }
3869                _ => chain_ctx,
3870            };
3871            chain_ctx = manager.execute(*event, chain_ctx);
3872        }
3873
3874        // After full chain with full mode, should have candidates
3875        assert!(
3876            !chain_ctx.memory_candidates.is_empty(),
3877            "full lifecycle chain should produce candidates"
3878        );
3879        assert!(
3880            chain_ctx.memory_contract.is_some(),
3881            "memory contract should be loaded from session start"
3882        );
3883    }
3884
3885    // ── Smoke tests: public demo release ──────────────────────────────────
3886
3887    #[test]
3888    fn test_public_demo_tools_list() {
3889        let response = handle_tools_list(&serde_json::json!("test"));
3890        let tools = response["result"]["tools"].as_array().unwrap();
3891        assert!(!tools.is_empty(), "tools/list must return tools");
3892        let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
3893        assert!(
3894            names.contains(&"memory_status"),
3895            "must include memory_status"
3896        );
3897        assert!(
3898            names.contains(&"memory_recall"),
3899            "must include memory_recall"
3900        );
3901        assert!(names.contains(&"memory_find"), "must include memory_find");
3902        assert!(
3903            names.contains(&"memory_auto_context"),
3904            "must include memory_auto_context"
3905        );
3906    }
3907
3908    #[test]
3909    fn test_public_demo_tools_are_read_only() {
3910        // Verify that tools/list returns tools that are safe for read-only use
3911        let response = handle_tools_list(&serde_json::json!("test"));
3912        let tools = response["result"]["tools"].as_array().unwrap();
3913        let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
3914
3915        // Read tools must be present
3916        assert!(
3917            names.contains(&"memory_status"),
3918            "must include memory_status"
3919        );
3920        assert!(
3921            names.contains(&"memory_recall"),
3922            "must include memory_recall"
3923        );
3924        assert!(names.contains(&"memory_find"), "must include memory_find");
3925        assert!(names.contains(&"memory_look"), "must include memory_look");
3926        assert!(
3927            names.contains(&"memory_auto_context"),
3928            "must include memory_auto_context"
3929        );
3930
3931        // All tools must have inputSchema with no required dangerous fields
3932        for tool in tools {
3933            let name = tool["name"].as_str().unwrap_or("");
3934            let schema = &tool["inputSchema"];
3935            assert!(schema.is_object(), "tool '{}' must have inputSchema", name);
3936        }
3937    }
3938
3939    #[test]
3940    fn test_public_demo_safe_query() {
3941        let _config = test_config();
3942        let hook_config = HookConfig::read_only();
3943        let manager = HookManager::new(hook_config);
3944        let ctx = HookContext::new(HookEvent::BeforeToolCall)
3945            .with_tool("memory_recall", serde_json::json!({"query": "safe query"}));
3946        let result = manager.execute(HookEvent::BeforeToolCall, ctx);
3947        assert!(
3948            result.memory_candidates.is_empty(),
3949            "safe query must not produce memory candidates in public mode"
3950        );
3951        assert_eq!(result.tool_name, Some("memory_recall".to_string()));
3952    }
3953
3954    #[test]
3955    fn test_public_demo_secret_query_filtered() {
3956        let _config = test_config();
3957        let hook_config = HookConfig::read_only();
3958        let manager = HookManager::new(hook_config);
3959        let mut ctx = HookContext::new(HookEvent::AfterToolCall)
3960            .with_tool("memory_recall", serde_json::json!({"query": "test"}));
3961        ctx.tool_result = Some("Found result with password=secret123".to_string());
3962        let result = manager.execute(HookEvent::AfterToolCall, ctx);
3963        assert!(
3964            result.memory_candidates.is_empty(),
3965            "secret-containing results must not create candidates in public mode"
3966        );
3967    }
3968
3969    #[test]
3970    fn test_public_demo_initialize_response() {
3971        let response = handle_initialize(&serde_json::json!("test"));
3972        assert_eq!(response["jsonrpc"], "2.0");
3973        assert_eq!(response["id"], "test");
3974        assert_eq!(response["result"]["protocolVersion"], "2024-11-05");
3975        assert!(response["result"]["capabilities"]["tools"].is_object());
3976    }
3977
3978    #[test]
3979    fn read_message_rejects_oversized_content_length() {
3980        let header = format!("Content-Length: {}\r\n\r\n", MAX_MESSAGE_LEN + 1);
3981        let mut reader = std::io::Cursor::new(header.into_bytes());
3982        let err = match read_message(&mut reader) {
3983            Ok(_) => panic!("oversized Content-Length must fail"),
3984            Err(e) => e,
3985        };
3986        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
3987        assert!(err.to_string().contains("exceeds maximum allowed"));
3988    }
3989
3990    #[test]
3991    fn read_message_rejects_oversized_header_line() {
3992        let huge = "x".repeat(MAX_HEADER_LEN + 1);
3993        let mut reader = std::io::Cursor::new(huge.into_bytes());
3994        let err = match read_message(&mut reader) {
3995            Ok(_) => panic!("unterminated oversized header must fail"),
3996            Err(e) => e,
3997        };
3998        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
3999    }
4000}