1use std::io::{BufRead, BufReader, Read, Write};
15use std::net::{TcpListener, TcpStream};
16use std::sync::{
17 atomic::{AtomicUsize, Ordering},
18 Arc,
19};
20use std::time::Duration;
21use tokio::runtime::Handle;
22use tokio::task::block_in_place;
23
24use crate::bridge::MemoryBridge;
25
26const MAX_TOP_K: u64 = 100;
27const MAX_DIRECT_IDS: usize = 100;
28const MAX_RERANK_RESULTS: usize = 50;
29const MAX_RERANK_CONTENT_BYTES: usize = 2_000;
30const MAX_GRAPH_EDGES_PER_TOOL_CALL: usize = 20_000;
31const RERANK_MODEL: &str = "granite4.1:3b";
32
33fn truncate_rerank_content(content: &str) -> &str {
34 if content.len() <= MAX_RERANK_CONTENT_BYTES {
35 return content;
36 }
37 let mut end = MAX_RERANK_CONTENT_BYTES;
38 while !content.is_char_boundary(end) {
39 end -= 1;
40 }
41 &content[..end]
42}
43
44fn rerank_candidate_limit(top_k: usize) -> usize {
45 top_k.saturating_mul(2).min(MAX_RERANK_RESULTS)
46}
47
48fn rerank_results(
51 query: &str,
52 results: &[serde_json::Value],
53 model: &str,
54) -> (Vec<serde_json::Value>, &'static str) {
55 rerank_results_at(query, results, model, "http://127.0.0.1:11434")
56}
57
58fn rerank_results_at(
59 query: &str,
60 results: &[serde_json::Value],
61 model: &str,
62 base_url: &str,
63) -> (Vec<serde_json::Value>, &'static str) {
64 let client = match reqwest::blocking::Client::builder()
65 .connect_timeout(Duration::from_secs(1))
66 .timeout(Duration::from_secs(3))
67 .build()
68 {
69 Ok(client) => client,
70 Err(_) => return (results.to_vec(), "degraded"),
71 };
72 let scored: Result<Vec<(f64, serde_json::Value)>, ()> = results
73 .iter()
74 .map(|r| -> Result<(f64, serde_json::Value), ()> {
75 let content = r.get("content").and_then(|v| v.as_str()).unwrap_or("");
76 let truncated = truncate_rerank_content(content);
77 let prompt = format!(
78 "Rate the relevance of this document to the query on a scale of 1-5. Reply with ONLY the number.\nQuery: {query}\nDocument: {truncated}\nRating:"
79 );
80 let body = serde_json::json!({
81 "model": model,
82 "prompt": prompt,
83 "stream": false,
84 "options": {"temperature": 0, "num_predict": 1}
85 });
86 let response = client
87 .post(format!("{base_url}/api/generate"))
88 .json(&body)
89 .send()
90 .map_err(|_| ())?
91 .error_for_status()
92 .map_err(|_| ())?
93 .json::<serde_json::Value>()
94 .map_err(|_| ())?;
95 let rating = response
96 .get("response")
97 .and_then(|r| r.as_str())
98 .and_then(|s| s.trim().chars().next())
99 .and_then(|c| c.to_digit(10))
100 .map(|d| d as f64)
101 .filter(|rating| (1.0..=5.0).contains(rating))
103 .ok_or(())?;
104 Ok((rating, r.clone()))
105 })
106 .collect::<Result<_, _>>();
107 let Ok(mut scored) = scored else {
108 return (results.to_vec(), "degraded");
109 };
110 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
111 (
112 scored
113 .into_iter()
114 .map(|(score, mut r)| {
115 if let Some(obj) = r.as_object_mut() {
116 obj.insert("rerank_score".to_string(), serde_json::json!(score));
117 }
118 r
119 })
120 .collect::<Vec<_>>(),
121 "applied",
122 )
123}
124
125pub struct HttpServerHandle {
126 pub local_addr: std::net::SocketAddr,
127 _thread: std::thread::JoinHandle<()>,
128}
129
130struct ConnectionSlot {
131 active: Arc<AtomicUsize>,
132}
133
134impl ConnectionSlot {
135 fn new(active: Arc<AtomicUsize>) -> Self {
136 Self { active }
137 }
138}
139
140impl Drop for ConnectionSlot {
141 fn drop(&mut self) {
142 self.active.fetch_sub(1, Ordering::AcqRel);
143 }
144}
145
146pub fn start_http_server(
147 port: u16,
148 auth_token: &str,
149 bridge: MemoryBridge,
150 handle: Handle,
151 profile: crate::profile::ToolProfile,
152) -> std::io::Result<HttpServerHandle> {
153 let listener = TcpListener::bind(("127.0.0.1", port))?;
154 let local_addr = listener.local_addr()?;
155 let auth_token = auth_token.to_string();
156 let active = Arc::new(AtomicUsize::new(0));
157 let thread = std::thread::spawn(move || {
158 eprintln!("HTTP search server listening on {local_addr}");
159 for stream in listener.incoming() {
160 let stream = match stream {
161 Ok(s) => s,
162 Err(_) => continue,
163 };
164
165 if active.fetch_add(1, Ordering::AcqRel) >= 32 {
166 active.fetch_sub(1, Ordering::AcqRel);
167 let _ = stream.shutdown(std::net::Shutdown::Both);
168 continue;
169 }
170 let bridge = bridge.clone();
171 let h = handle.clone();
172 let token = auth_token.clone();
173 let active = active.clone();
174 std::thread::spawn(move || {
175 let _slot = ConnectionSlot::new(active);
176 handle_connection(stream, &token, bridge, h, profile);
177 });
178 }
179 });
180 Ok(HttpServerHandle {
181 local_addr,
182 _thread: thread,
183 })
184}
185
186fn handle_connection(
187 mut stream: TcpStream,
188 auth_token: &str,
189 bridge: MemoryBridge,
190 handle: Handle,
191 profile: crate::profile::ToolProfile,
192) {
193 const MAX_HEADER_BYTES: usize = 16 * 1024;
194 const MAX_HEADER_COUNT: usize = 64;
195 let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
196 let _ = stream.set_write_timeout(Some(Duration::from_secs(2)));
197 let Ok(reader_stream) = stream.try_clone() else {
198 return;
199 };
200 let mut reader = BufReader::new(reader_stream);
201 let mut request_line = String::new();
202 if !read_bounded_line(&mut reader, 4096, &mut request_line) {
203 return;
204 }
205
206 let parts: Vec<&str> = request_line.split_whitespace().collect();
207 if parts.len() < 2 {
208 return;
209 }
210 let method = parts[0];
211 let path = parts[1];
212
213 let mut content_length = 0;
214 let mut auth_header: Option<String> = None;
215 let mut host_header: Option<String> = None;
216 let mut origin_header: Option<String> = None;
217 let mut header_bytes = request_line.len();
218 let mut header_count = 0usize;
219 loop {
220 let mut header = String::new();
221 let remaining = MAX_HEADER_BYTES.saturating_sub(header_bytes);
222 if !read_bounded_line(&mut reader, remaining, &mut header) {
223 return;
224 }
225 if header.trim().is_empty() {
226 break;
227 }
228 header_count += 1;
229 header_bytes += header.len();
230 if header_count > MAX_HEADER_COUNT || header_bytes > MAX_HEADER_BYTES {
231 let _ = stream.write_all(b"HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
232 return;
233 }
234 let Some((name, value)) = header.split_once(':') else {
235 return;
236 };
237 match name.to_ascii_lowercase().as_str() {
238 "content-length" => {
239 content_length = match value.trim().parse() {
240 Ok(v) => v,
241 Err(_) => return,
242 }
243 }
244 "authorization" => auth_header = Some(value.trim().to_string()),
245 "host" => host_header = Some(value.trim().to_string()),
246 "origin" => origin_header = Some(value.trim().to_string()),
247 _ => {}
248 }
249 }
250
251 let authorized = auth_header
253 .as_deref()
254 .map(|h| h == format!("Bearer {}", auth_token))
255 .unwrap_or(false);
256 if !authorized {
257 let response =
258 "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
259 let _ = stream.write_all(response.as_bytes());
260 return;
261 }
262
263 let host_ok = host_header.as_deref().is_some_and(is_loopback_authority);
265 let origin_ok = origin_header.as_deref().map_or(true, is_loopback_origin);
266 if !host_ok || !origin_ok {
267 let response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
268 let _ = stream.write_all(response.as_bytes());
269 return;
270 }
271
272 const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
274 if content_length > MAX_BODY_SIZE {
275 let response =
276 "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
277 let _ = stream.write_all(response.as_bytes());
278 return;
279 }
280
281 let mut body = vec![0u8; content_length];
282 if content_length > 0 && reader.read_exact(&mut body).is_err() {
283 return;
284 }
285 let body_str = String::from_utf8_lossy(&body);
286
287 let (status, response) = match (method, path) {
288 ("GET", "/health") => (
289 "200 OK",
290 serde_json::json!({"ok": true, "service": "semantic-memory-mcp"}),
291 ),
292 (_, _) if !profile.allows_http_route() => (
293 "404 Not Found",
294 serde_json::json!({"error": "not found", "path": path}),
295 ),
296 ("POST", "/search") => handle_search(&body_str, &bridge, &handle),
297 ("POST", "/search-routed") => handle_search_routed(&body_str, &bridge, &handle),
298 ("POST", "/rerank") => handle_rerank(&body_str),
299 ("POST", "/stats") => handle_stats(&bridge, &handle),
300 ("POST", "/add") if profile.allows_http_write() => {
301 handle_add_fact(&body_str, &bridge, &handle)
302 }
303 ("POST", "/record-outcome") if profile.allows_http_write() => {
304 handle_record_outcome(&body_str, &bridge, &handle)
305 }
306 ("GET", "/verify-integrity") => handle_verify_integrity(&bridge, &handle),
307 ("POST", "/discord") => handle_discord(&body_str, &bridge, &handle),
308 ("POST", "/maintenance/check") if profile.allows_http_maintenance() => {
309 handle_maintenance_check(&bridge, &handle)
310 }
311 ("POST", "/maintenance/vacuum") if profile.allows_http_maintenance() => {
312 handle_maintenance_vacuum(&bridge, &handle)
313 }
314 ("POST", "/maintenance/reembed") if profile.allows_http_maintenance() => {
315 handle_maintenance_reembed(&bridge, &handle)
316 }
317 ("POST", "/maintenance/reconcile") if profile.allows_http_maintenance() => {
318 handle_maintenance_reconcile(&body_str, &bridge, &handle)
319 }
320 ("POST", "/maintenance/rebuild-hnsw") if profile.allows_http_maintenance() => {
321 handle_maintenance_rebuild_hnsw(&bridge, &handle)
322 }
323 ("POST", "/maintenance/compact-hnsw") if profile.allows_http_maintenance() => {
324 handle_maintenance_compact_hnsw(&bridge, &handle)
325 }
326 _ => (
327 "404 Not Found",
328 serde_json::json!({"error": "not found", "path": path}),
329 ),
330 };
331
332 let response_str = match serde_json::to_string(&response) {
333 Ok(value) => value,
334 Err(error) => {
335 let fallback = format!(
336 "{{\"ok\":false,\"error\":\"failed to serialize response: {}\"}}",
337 error
338 );
339 fallback
340 }
341 };
342 let response_bytes = response_str.as_bytes();
343 let http_response = format!(
344 "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
345 status,
346 response_bytes.len()
347 );
348
349 let _ = stream.write_all(http_response.as_bytes());
350 let _ = stream.write_all(response_bytes);
351 let _ = stream.flush();
352}
353
354fn read_bounded_line(reader: &mut BufReader<TcpStream>, max: usize, output: &mut String) -> bool {
355 if max == 0 {
356 return false;
357 }
358 let Ok(read) = reader.take((max + 1) as u64).read_line(output) else {
359 return false;
360 };
361 read > 0 && read <= max && output.ends_with('\n')
362}
363
364fn is_loopback_authority(value: &str) -> bool {
365 matches!(value, "localhost" | "127.0.0.1" | "[::1]")
366 || value.strip_prefix("localhost:").is_some_and(valid_port)
367 || value.strip_prefix("127.0.0.1:").is_some_and(valid_port)
368 || value.strip_prefix("[::1]:").is_some_and(valid_port)
369}
370
371fn valid_port(value: &str) -> bool {
372 value.parse::<u16>().is_ok()
373}
374
375fn is_loopback_origin(value: &str) -> bool {
376 value
377 .strip_prefix("http://")
378 .is_some_and(is_loopback_authority)
379 || value
380 .strip_prefix("https://")
381 .is_some_and(is_loopback_authority)
382}
383
384fn handle_search(
385 body: &str,
386 bridge: &MemoryBridge,
387 handle: &Handle,
388) -> (&'static str, serde_json::Value) {
389 let params: serde_json::Value = match serde_json::from_str(body) {
390 Ok(v) => v,
391 Err(e) => {
392 return (
393 "400 Bad Request",
394 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
395 )
396 }
397 };
398
399 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
400 let top_k = match bounded_top_k(¶ms, 5) {
401 Ok(k) => k,
402 Err(response) => return response,
403 };
404 let namespaces: Option<Vec<String>> = params
405 .get("namespaces")
406 .and_then(|v| serde_json::from_value(v.clone()).ok());
407 let do_rerank = params
408 .get("rerank")
409 .and_then(|v| v.as_bool())
410 .unwrap_or(false);
411
412 if query.is_empty() {
413 return (
414 "400 Bad Request",
415 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
416 );
417 }
418
419 let store = &bridge.store;
420 let ns_slice: Option<Vec<&str>> = namespaces
421 .as_ref()
422 .map(|v| v.iter().map(|s| s.as_str()).collect());
423 let fetch_k = if do_rerank {
425 rerank_candidate_limit(top_k)
426 } else {
427 top_k
428 };
429 let result = block_in_place(|| {
430 handle.block_on(store.search(query, Some(fetch_k), ns_slice.as_deref(), None))
431 });
432
433 match result {
434 Ok(results) => {
435 let json_results: Vec<serde_json::Value> = results
436 .iter()
437 .map(|r| {
438 let namespace = match &r.source {
439 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
440 semantic_memory::SearchSource::Chunk { document_title, .. } => {
441 document_title.clone()
442 }
443 _ => String::new(),
444 };
445 serde_json::json!({
446 "result_id": r.source.result_id(),
447 "content": r.content,
448 "score": r.score,
449 "cosine_similarity": r.cosine_similarity,
450 "namespace": namespace,
451 })
452 })
453 .collect();
454
455 let (final_results, rerank_status): (Vec<serde_json::Value>, &str) =
456 if do_rerank && !json_results.is_empty() {
457 let (results, status) = rerank_results(query, &json_results, RERANK_MODEL);
458 (results.into_iter().take(top_k).collect(), status)
459 } else {
460 (json_results, "not_applicable")
461 };
462
463 let count = final_results.len();
464 let provenance = serde_json::json!({
465 "stages_fired": {
466 "bm25": true,
467 "vector": true,
468 "late_interaction": false,
469 "rerank": rerank_status == "applied",
470 },
471 "rerank_requested": do_rerank,
472 "result_count": count,
473 "view": "semantic",
474 "widening_occurred": null,
475 "widening_reason": null,
476 "verification_status": "unverified",
477 "proof_reference": null,
478 });
479 (
480 "200 OK",
481 serde_json::json!({
482 "ok": true,
483 "query": query,
484 "top_k": top_k,
485 "results": final_results,
486 "count": count,
487 "reranked": rerank_status == "applied",
488 "rerank_status": rerank_status,
489 "provenance": provenance,
490 }),
491 )
492 }
493 Err(e) => (
494 "500 Internal Server Error",
495 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
496 ),
497 }
498}
499
500fn handle_search_routed(
508 body: &str,
509 bridge: &MemoryBridge,
510 handle: &Handle,
511) -> (&'static str, serde_json::Value) {
512 use semantic_memory::integration::plan_execution;
513 use semantic_memory::rl_routing::{is_trained, route_with_policy};
514 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
515
516 let params: serde_json::Value = match serde_json::from_str(body) {
517 Ok(v) => v,
518 Err(e) => {
519 return (
520 "400 Bad Request",
521 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
522 )
523 }
524 };
525
526 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
527 let base_top_k = match bounded_top_k(¶ms, 12) {
528 Ok(k) => k,
529 Err(response) => return response,
530 };
531 let query_class = params
532 .get("query_class")
533 .and_then(|v| v.as_str())
534 .unwrap_or("A");
535 let namespaces: Option<Vec<String>> = params
536 .get("namespaces")
537 .and_then(|v| serde_json::from_value(v.clone()).ok());
538 let contradictions: Vec<(String, String)> = match params.get("contradictions") {
539 Some(raw) => match serde_json::from_value(raw.clone()) {
540 Ok(list) => list,
541 Err(error) => {
542 return (
543 "400 Bad Request",
544 serde_json::json!({"ok": false, "error": format!("invalid contradictions: {error}")}),
545 );
546 }
547 },
548 None => Vec::new(),
549 };
550 let group_by_community = params
551 .get("group_by_community")
552 .and_then(|v| v.as_bool())
553 .unwrap_or(false);
554
555 if query.is_empty() {
556 return (
557 "400 Bad Request",
558 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
559 );
560 }
561
562 let router = RetrievalRouter {
564 decoder_enabled: true,
565 discord_enabled: true,
566 corpus_density: 0.5,
567 ..Default::default()
568 };
569 let store = &bridge.store;
570 let policy = match block_in_place(|| handle.block_on(store.load_routing_policy())) {
571 Ok(policy) => policy,
572 Err(e) => {
573 return (
574 "500 Internal Server Error",
575 serde_json::json!({"ok": false, "error": format!("load routing policy error: {e}")}),
576 )
577 }
578 };
579 let profile = QueryProfile::from_query(query);
580 let (decision, routing_source) = match policy.as_ref().filter(|p| is_trained(p)) {
581 Some(policy) => (route_with_policy(policy, &profile), "trained_policy"),
582 None => (router.route(&profile), "heuristic"),
583 };
584 let contras = contradictions.clone();
585 let plan = plan_execution(&decision, contras.clone());
586
587 let top_k = if query_class == "D" {
589 (base_top_k * 2).min(20)
590 } else {
591 base_top_k
592 };
593
594 let ns_slice: Option<Vec<&str>> = namespaces
595 .as_ref()
596 .map(|v| v.iter().map(|s| s.as_str()).collect());
597
598 let result = if query_class == "C" {
600 use semantic_memory::{ExactnessProfile, SearchContext};
601 let mut ctx = SearchContext::default_now();
602 ctx.exactness_profile = ExactnessProfile::PreferExact;
603 block_in_place(|| {
604 handle.block_on(store.search_with_context(
605 query,
606 Some(top_k),
607 ns_slice.as_deref(),
608 None,
609 ctx,
610 ))
611 })
612 .map(|r| r.results)
613 } else {
614 block_in_place(|| {
615 handle.block_on(store.search(query, Some(top_k), ns_slice.as_deref(), None))
616 })
617 };
618
619 match result {
620 Ok(results) => {
621 let json_results: Vec<serde_json::Value> = results
622 .iter()
623 .map(|r| {
624 let namespace = match &r.source {
625 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
626 semantic_memory::SearchSource::Chunk { document_title, .. } => {
627 document_title.clone()
628 }
629 _ => String::new(),
630 };
631 serde_json::json!({
632 "result_id": r.source.result_id(),
633 "content": r.content,
634 "score": r.score,
635 "cosine_similarity": r.cosine_similarity,
636 "namespace": namespace,
637 "source_type": match &r.source {
638 semantic_memory::SearchSource::Fact { .. } => "fact",
639 semantic_memory::SearchSource::Chunk { .. } => "chunk",
640 semantic_memory::SearchSource::Message { .. } => "message",
641 _ => "unknown",
642 },
643 })
644 })
645 .collect();
646
647 let mut factor_graph_payload = serde_json::json!({"enabled": false});
648 let mut decoder_executed = false;
649 let mut discord_executed = false;
650 let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
651
652 if decision.decoder {
654 #[cfg(feature = "full")]
655 {
656 use semantic_memory::factor_graph::{
657 factors_from_edges, FactorGraph, FactorGraphConfig,
658 };
659
660 let graph_edges = block_in_place(|| {
661 handle.block_on(store.list_all_graph_edges_with_limit(MAX_GRAPH_EDGES_PER_TOOL_CALL))
662 });
663
664 if let Ok(edges) = graph_edges {
665 let raw_edges: Vec<(
666 String,
667 String,
668 semantic_memory::GraphEdgeType,
669 f64,
670 Option<String>,
671 )> = edges
672 .iter()
673 .map(|edge| {
674 let parsed_type = edge
675 .edge_type_parsed
676 .clone()
677 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
678 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
679 relation: "unknown".to_string(),
680 });
681 (
682 edge.source.clone(),
683 edge.target.clone(),
684 parsed_type,
685 edge.weight,
686 edge.metadata.clone(),
687 )
688 })
689 .collect();
690
691 let nodes: Vec<(String, f64)> = results
692 .iter()
693 .map(|r| (r.source.result_id(), r.score))
694 .collect();
695 let factors = factors_from_edges(&raw_edges);
696 let graph = FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
697 let propagated = graph.propagate();
698 let top_beliefs = propagated.top_k(top_k);
699
700 factor_graph_payload = serde_json::json!({
701 "enabled": true,
702 "top_k_beliefs": top_beliefs
703 .into_iter()
704 .map(|(item_id, belief)| serde_json::json!({
705 "item_id": item_id,
706 "belief": belief,
707 }))
708 .collect::<Vec<_>>(),
709 "iterations": propagated.iterations,
710 "converged": propagated.converged,
711 "elapsed_ms": propagated.elapsed_ms,
712 "factor_counts": {
713 "semantic": propagated.factor_counts.semantic,
714 "temporal": propagated.factor_counts.temporal,
715 "causal": propagated.factor_counts.causal,
716 "entity": propagated.factor_counts.entity,
717 "total": propagated.factor_counts.total(),
718 },
719 });
720 decoder_executed = true;
721 }
722 }
723
724 if !plan.contradictions.is_empty() {
726 use semantic_memory::decoder::{compute_correction, detect_syndromes};
727 let result_scores: Vec<(String, f64)> = results
728 .iter()
729 .map(|r| (r.source.result_id(), r.score))
730 .collect();
731 let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
732 let _ = compute_correction(&syndromes, 10.0);
733 decoder_executed = true;
734 }
735 }
736
737 if plan.use_discord {
739 use semantic_memory::discord::DiscordScorer;
740 let direct_ids: Vec<String> =
741 results.iter().map(|r| r.source.result_id()).collect();
742 let existing_ids: std::collections::HashSet<String> =
743 direct_ids.iter().cloned().collect();
744 let edges_result = block_in_place(|| {
745 handle.block_on(store.list_graph_edges_for_neighborhood(
746 direct_ids.clone(),
747 2,
748 200,
749 ))
750 });
751 if let Ok(raw_edges) = edges_result {
752 let edge_refs: Vec<semantic_memory::discord::GraphEdgeRef> = raw_edges
753 .iter()
754 .map(|edge| {
755 let parsed_type = edge
756 .edge_type_parsed
757 .clone()
758 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
759 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
760 relation: "unknown".to_string(),
761 });
762 let type_str = match parsed_type {
763 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
764 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
765 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
766 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
767 };
768 semantic_memory::discord::GraphEdgeRef {
769 source: edge.source.clone(),
770 target: edge.target.clone(),
771 edge_type: type_str.to_string(),
772 weight: edge.weight,
773 }
774 })
775 .collect();
776 let scorer = DiscordScorer::with_defaults();
777 let discord_hits = scorer.score(&direct_ids, &edge_refs);
778 for hit in &discord_hits {
779 if !existing_ids.contains(&hit.item_id) {
780 discord_results_payload.push(serde_json::json!({
781 "result_id": hit.item_id,
782 "discord_score": hit.discord_score,
783 "anchor_ids": hit.anchor_ids,
784 "relationship_types": hit.relationship_types,
785 }));
786 }
787 }
788 discord_executed = true;
789 }
790 }
791
792 let grouped_results_payload: serde_json::Value = if group_by_community {
794 let seed_ids: Vec<String> = results.iter().map(|r| r.source.result_id()).collect();
795 let edges_result = block_in_place(|| {
796 handle.block_on(store.list_graph_edges_for_neighborhood(
797 seed_ids.clone(),
798 2,
799 200,
800 ))
801 });
802 let edges: Vec<(String, String)> = match edges_result {
803 Ok(raw_edges) => raw_edges
804 .iter()
805 .map(|edge| (edge.source.clone(), edge.target.clone()))
806 .collect(),
807 Err(_) => Vec::new(),
808 };
809 if !edges.is_empty() {
810 use semantic_memory::community::detect_communities;
811 let communities = detect_communities(&edges, 1.0, 42);
812 let mut member_to_comm: std::collections::HashMap<String, String> =
813 std::collections::HashMap::new();
814 for c in &communities {
815 for m in &c.members {
816 member_to_comm.insert(m.clone(), c.id.clone());
817 }
818 }
819 let mut groups: std::collections::HashMap<String, Vec<serde_json::Value>> =
820 std::collections::HashMap::new();
821 let mut ungrouped: Vec<serde_json::Value> = Vec::new();
822 for r in &json_results {
823 if let Some(rid) = r.get("result_id").and_then(|v| v.as_str()) {
824 match member_to_comm.get(rid).cloned() {
825 Some(cid) => groups.entry(cid).or_default().push(r.clone()),
826 None => ungrouped.push(r.clone()),
827 }
828 }
829 }
830 let mut map = serde_json::Map::new();
831 for (cid, items) in groups {
832 map.insert(format!("community_{cid}"), serde_json::json!(items));
833 }
834 if !ungrouped.is_empty() {
835 map.insert("ungrouped".to_string(), serde_json::json!(ungrouped));
836 }
837 serde_json::Value::Object(map)
838 } else {
839 serde_json::Value::Null
840 }
841 } else {
842 serde_json::Value::Null
843 };
844
845 let provenance = serde_json::json!({
847 "stages_fired": {
848 "bm25": results.iter().any(|r| r.bm25_rank.is_some()),
849 "vector": results.iter().any(|r| r.vector_rank.is_some()),
850 "late_interaction": true,
851 "discord": discord_executed,
852 "decoder": decoder_executed,
853 },
854 "result_count": results.len(),
855 "view": "routed",
856 "query_class": query_class,
857 "widening_occurred": null, "widening_reason": null,
859 "verification_status": "unverified",
860 "proof_reference": null,
861 });
862
863 (
864 "200 OK",
865 serde_json::json!({
866 "ok": true,
867 "query": query,
868 "top_k": base_top_k,
869 "results": json_results,
870 "provenance": provenance,
871 "query_class": query_class,
872 "routed": true,
873 "routing_decision": {
874 "source": routing_source,
875 "bm25_coarse": decision.bm25_coarse,
876 "vector_medium": decision.vector_medium,
877 "rerank_fine": decision.rerank_fine,
878 "graph_expansion": decision.graph_expansion,
879 "decoder": decision.decoder,
880 "discord": decision.discord,
881 "no_retrieval": decision.no_retrieval,
882 "reasoning": decision.reasoning,
883 },
884 "decoder_planned": plan.use_decoder,
885 "decoder_executed": decoder_executed,
886 "discord_planned": plan.use_discord,
887 "discord_executed": discord_executed,
888 "discord_results": discord_results_payload,
889 "factor_graph": factor_graph_payload,
890 "grouped_results": grouped_results_payload,
891 }),
892 )
893 }
894 Err(e) => (
895 "500 Internal Server Error",
896 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
897 ),
898 }
899}
900
901fn handle_stats(bridge: &MemoryBridge, handle: &Handle) -> (&'static str, serde_json::Value) {
902 let store = &bridge.store;
903 let core = block_in_place(|| handle.block_on(store.stats()));
904 let graph = block_in_place(|| handle.block_on(store.count_graph_edges()));
905 let core_health = match &core {
906 Ok(_) => serde_json::json!({"health":"healthy","error":null}),
907 Err(e) => serde_json::json!({"health":"error","error":e.to_string()}),
908 };
909 let graph_health = match &graph {
910 Ok(_) => serde_json::json!({"health":"healthy","error":null}),
911 Err(e) => serde_json::json!({"health":"error","error":e.to_string()}),
912 };
913 let stats = core.ok();
914 let graph_edges = graph.ok();
915 let ok = stats.is_some() && graph_edges.is_some();
916 (
917 if ok {
918 "200 OK"
919 } else {
920 "503 Service Unavailable"
921 },
922 serde_json::json!({
923 "ok": ok,
924 "components": {"core": core_health, "graph": graph_health},
925 "facts": stats.as_ref().map(|s| s.total_facts),
926 "documents": stats.as_ref().map(|s| s.total_documents),
927 "chunks": stats.as_ref().map(|s| s.total_chunks),
928 "graph_edges": graph_edges,
929 "db_size_mb": stats.as_ref().map(|s| (s.database_size_bytes as f64) / (1024.0 * 1024.0)),
930 }),
931 )
932}
933
934fn handle_rerank(body: &str) -> (&'static str, serde_json::Value) {
935 let params: serde_json::Value = match serde_json::from_str(body) {
936 Ok(v) => v,
937 Err(e) => {
938 return (
939 "400 Bad Request",
940 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
941 )
942 }
943 };
944
945 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
946 let results = match params.get("results").and_then(|v| v.as_array()) {
947 Some(r) => r.clone(),
948 None => {
949 return (
950 "400 Bad Request",
951 serde_json::json!({"ok": false, "error": "missing 'results' array"}),
952 )
953 }
954 };
955
956 if query.is_empty() {
957 return (
958 "400 Bad Request",
959 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
960 );
961 }
962 if results.len() > MAX_RERANK_RESULTS {
963 return (
964 "400 Bad Request",
965 serde_json::json!({"ok": false, "error": format!("results exceeds maximum of {MAX_RERANK_RESULTS}")}),
966 );
967 }
968 if results.iter().any(|r| {
969 r.get("content")
970 .and_then(|v| v.as_str())
971 .is_some_and(|s| s.len() > MAX_RERANK_CONTENT_BYTES)
972 }) {
973 return (
974 "400 Bad Request",
975 serde_json::json!({"ok": false, "error": format!("result content exceeds maximum of {MAX_RERANK_CONTENT_BYTES} bytes")}),
976 );
977 }
978
979 let (reranked, rerank_status) = rerank_results(query, &results, RERANK_MODEL);
980 let count = reranked.len();
981 (
982 "200 OK",
983 serde_json::json!({
984 "ok": true,
985 "results": reranked,
986 "count": count,
987 "rerank_status": rerank_status,
988 "model": RERANK_MODEL,
989 }),
990 )
991}
992
993fn handle_add_fact(
994 _body: &str,
995 _bridge: &MemoryBridge,
996 _handle: &Handle,
997) -> (&'static str, serde_json::Value) {
998 (
999 "503 Service Unavailable",
1000 serde_json::json!({
1001 "ok": false,
1002 "error": "HTTP evidence admission is disabled: no trusted authenticated authority issuer or immutable evidence resolver is configured"
1003 }),
1004 )
1005}
1006
1007fn handle_record_outcome(
1009 body: &str,
1010 bridge: &MemoryBridge,
1011 handle: &Handle,
1012) -> (&'static str, serde_json::Value) {
1013 use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome};
1014 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
1015
1016 let params: serde_json::Value = match serde_json::from_str(body) {
1017 Ok(v) => v,
1018 Err(e) => {
1019 return (
1020 "400 Bad Request",
1021 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1022 )
1023 }
1024 };
1025
1026 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
1027 let outcome = params
1028 .get("outcome")
1029 .and_then(|v| v.as_str())
1030 .unwrap_or("neutral");
1031 let _query_class = params
1032 .get("query_class")
1033 .and_then(|v| v.as_str())
1034 .unwrap_or("A");
1035
1036 if query.is_empty() {
1037 return (
1038 "400 Bad Request",
1039 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
1040 );
1041 }
1042
1043 let outcome_enum = match outcome.to_lowercase().as_str() {
1044 "good" => RoutingOutcome::Good,
1045 "bad" => RoutingOutcome::Bad,
1046 "neutral" => RoutingOutcome::Neutral,
1047 _ => {
1048 return (
1049 "400 Bad Request",
1050 serde_json::json!({"ok": false, "error": "outcome must be 'good', 'bad', or 'neutral'"}),
1051 )
1052 }
1053 };
1054
1055 let profile = QueryProfile::from_query(query);
1056 let router = RetrievalRouter::default();
1057 let decision = router.route(&profile);
1058
1059 let store = &bridge.store;
1060 let mut policy = match block_in_place(|| handle.block_on(store.load_routing_policy())) {
1062 Ok(policy) => policy.unwrap_or_default(),
1063 Err(e) => {
1064 return (
1065 "500 Internal Server Error",
1066 serde_json::json!({"ok": false, "error": format!("load routing policy error: {e}")}),
1067 )
1068 }
1069 };
1070 record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
1071 if let Err(e) = block_in_place(|| handle.block_on(store.save_routing_policy(&policy))) {
1073 return (
1074 "500 Internal Server Error",
1075 serde_json::json!({"ok": false, "error": format!("persist routing policy error: {e}")}),
1076 );
1077 }
1078
1079 (
1080 "200 OK",
1081 serde_json::json!({
1082 "ok": true,
1083 "mutating": true,
1084 "recorded": true,
1085 "feedback": {"kind": "ProxyLabel", "label": outcome},
1086 "routing_decision": {
1087 "bm25_coarse": decision.bm25_coarse,
1088 "vector_medium": decision.vector_medium,
1089 "rerank_fine": decision.rerank_fine,
1090 "graph_expansion": decision.graph_expansion,
1091 "decoder": decision.decoder,
1092 "discord": decision.discord,
1093 "no_retrieval": decision.no_retrieval,
1094 "reasoning": decision.reasoning,
1095 },
1096 "policy_state": {
1097 "trained_examples": policy.trained_examples,
1098 "baseline": policy.baseline,
1099 },
1100 }),
1101 )
1102}
1103
1104fn handle_verify_integrity(
1106 bridge: &MemoryBridge,
1107 handle: &Handle,
1108) -> (&'static str, serde_json::Value) {
1109 let store = &bridge.store;
1110 let result = block_in_place(|| {
1111 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
1112 });
1113
1114 match result {
1115 Ok(report) => (
1116 "200 OK",
1117 serde_json::json!({
1118 "ok": report.ok,
1119 "integrity": report.ok,
1120 "schema_version": report.schema_version,
1121 "fact_count": report.fact_count,
1122 "chunk_count": report.chunk_count,
1123 "message_count": report.message_count,
1124 "facts_missing_embeddings": report.facts_missing_embeddings,
1125 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1126 "issues": report.issues,
1127 "issue_count": report.issues.len(),
1128 "message": if report.ok { "All integrity checks passed".to_string() } else { format!("{} integrity issues found", report.issues.len()) },
1129 }),
1130 ),
1131 Err(e) => (
1132 "500 Internal Server Error",
1133 serde_json::json!({"ok": false, "integrity": false, "error": format!("verify_integrity error: {e}")}),
1134 ),
1135 }
1136}
1137
1138fn handle_discord(
1143 body: &str,
1144 bridge: &MemoryBridge,
1145 handle: &Handle,
1146) -> (&'static str, serde_json::Value) {
1147 use semantic_memory::discord::DiscordScorer;
1148
1149 let params: serde_json::Value = match serde_json::from_str(body) {
1150 Ok(v) => v,
1151 Err(e) => {
1152 return (
1153 "400 Bad Request",
1154 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1155 )
1156 }
1157 };
1158
1159 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
1160 let top_k = match bounded_top_k(¶ms, 5) {
1161 Ok(k) => k,
1162 Err(response) => return response,
1163 };
1164
1165 let direct_ids: Vec<String> = match params.get("direct_ids").and_then(|v| v.as_array()) {
1167 Some(arr) => {
1168 if arr.len() > MAX_DIRECT_IDS || arr.iter().any(|v| !v.is_string()) {
1169 return (
1170 "400 Bad Request",
1171 serde_json::json!({"ok": false, "error": format!("direct_ids must contain at most {MAX_DIRECT_IDS} strings")}),
1172 );
1173 }
1174 arr.iter()
1175 .filter_map(|v| v.as_str().map(str::to_owned))
1176 .collect()
1177 }
1178 None => {
1179 if query.is_empty() {
1181 return (
1182 "400 Bad Request",
1183 serde_json::json!({"ok": false, "error": "either 'direct_ids' or 'query' must be provided"}),
1184 );
1185 }
1186 let store = &bridge.store;
1187 let search_result =
1188 block_in_place(|| handle.block_on(store.search(query, Some(top_k), None, None)));
1189 match search_result {
1190 Ok(results) => results.iter().map(|r| r.source.result_id()).collect(),
1191 Err(e) => {
1192 return (
1193 "500 Internal Server Error",
1194 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
1195 )
1196 }
1197 }
1198 }
1199 };
1200
1201 if direct_ids.is_empty() {
1202 return (
1203 "200 OK",
1204 serde_json::json!({"ok": true, "discord_results": [], "count": 0, "edges_loaded": 0}),
1205 );
1206 }
1207
1208 let store = &bridge.store;
1209 let edges_result = block_in_place(|| {
1211 handle.block_on(store.list_graph_edges_for_neighborhood(direct_ids.clone(), 2, 200))
1212 });
1213
1214 let edges: Vec<semantic_memory::discord::GraphEdgeRef> = match edges_result {
1215 Ok(raw_edges) => raw_edges
1216 .iter()
1217 .map(|edge| {
1218 let parsed_type = edge
1219 .edge_type_parsed
1220 .clone()
1221 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1222 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1223 relation: "unknown".to_string(),
1224 });
1225 let type_str = match parsed_type {
1226 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
1227 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
1228 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
1229 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
1230 };
1231 semantic_memory::discord::GraphEdgeRef {
1232 source: edge.source.clone(),
1233 target: edge.target.clone(),
1234 edge_type: type_str.to_string(),
1235 weight: edge.weight,
1236 }
1237 })
1238 .collect(),
1239 Err(e) => {
1240 return (
1241 "500 Internal Server Error",
1242 serde_json::json!({"ok": false, "error": format!("failed to load graph edges: {e}")}),
1243 )
1244 }
1245 };
1246
1247 let edges_loaded = edges.len();
1248 let scorer = DiscordScorer::with_defaults();
1249 let discord_hits = scorer.score(&direct_ids, &edges);
1250
1251 let existing: std::collections::HashSet<String> = direct_ids.iter().cloned().collect();
1253 let filtered_hits: Vec<serde_json::Value> = discord_hits
1254 .iter()
1255 .filter(|hit| !existing.contains(&hit.item_id))
1256 .map(|hit| {
1257 serde_json::json!({
1258 "result_id": hit.item_id,
1259 "discord_score": hit.discord_score,
1260 "anchor_ids": hit.anchor_ids,
1261 "relationship_types": hit.relationship_types,
1262 })
1263 })
1264 .collect();
1265
1266 (
1267 "200 OK",
1268 serde_json::json!({
1269 "ok": true,
1270 "discord_results": filtered_hits,
1271 "count": filtered_hits.len(),
1272 "edges_loaded": edges_loaded,
1273 "direct_ids": direct_ids,
1274 }),
1275 )
1276}
1277
1278fn bounded_top_k(
1279 params: &serde_json::Value,
1280 default: usize,
1281) -> Result<usize, (&'static str, serde_json::Value)> {
1282 let Some(value) = params.get("top_k") else {
1283 return Ok(default);
1284 };
1285 let Some(value) = value.as_u64() else {
1286 return Err((
1287 "400 Bad Request",
1288 serde_json::json!({"ok": false, "error": "top_k must be an unsigned integer"}),
1289 ));
1290 };
1291 if value == 0 || value > MAX_TOP_K {
1292 return Err((
1293 "400 Bad Request",
1294 serde_json::json!({"ok": false, "error": format!("top_k must be between 1 and {MAX_TOP_K}")}),
1295 ));
1296 }
1297 Ok(value as usize)
1298}
1299
1300fn handle_maintenance_check(
1308 bridge: &MemoryBridge,
1309 handle: &Handle,
1310) -> (&'static str, serde_json::Value) {
1311 let store = &bridge.store;
1312 let embeddings_dirty = block_in_place(|| handle.block_on(store.embeddings_are_dirty()));
1313 let embeddings_dirty = match embeddings_dirty {
1314 Ok(v) => v,
1315 Err(e) => {
1316 return (
1317 "500 Internal Server Error",
1318 serde_json::json!({"ok": false, "error": format!("embeddings_are_dirty error: {e}")}),
1319 )
1320 }
1321 };
1322 let integrity_result = block_in_place(|| {
1323 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
1324 });
1325
1326 match integrity_result {
1327 Ok(report) => (
1328 "200 OK",
1329 serde_json::json!({
1330 "ok": report.ok,
1331 "embeddings_are_dirty": embeddings_dirty,
1332 "integrity": {
1333 "ok": report.ok,
1334 "schema_version": report.schema_version,
1335 "fact_count": report.fact_count,
1336 "chunk_count": report.chunk_count,
1337 "message_count": report.message_count,
1338 "facts_missing_embeddings": report.facts_missing_embeddings,
1339 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1340 "issues": report.issues,
1341 "issue_count": report.issues.len(),
1342 },
1343 "message": if report.ok && !embeddings_dirty {
1344 "All checks passed".to_string()
1345 } else if report.ok && embeddings_dirty {
1346 "Integrity OK but embeddings need re-embedding".to_string()
1347 } else {
1348 format!("{} integrity issues found", report.issues.len())
1349 },
1350 }),
1351 ),
1352 Err(e) => (
1353 "500 Internal Server Error",
1354 serde_json::json!({
1355 "ok": false,
1356 "embeddings_are_dirty": embeddings_dirty,
1357 "error": format!("verify_integrity error: {e}"),
1358 }),
1359 ),
1360 }
1361}
1362
1363fn handle_maintenance_vacuum(
1365 bridge: &MemoryBridge,
1366 handle: &Handle,
1367) -> (&'static str, serde_json::Value) {
1368 let store = &bridge.store;
1369 let result = block_in_place(|| handle.block_on(store.vacuum()));
1370
1371 match result {
1372 Ok(()) => (
1373 "200 OK",
1374 serde_json::json!({"ok": true, "action": "vacuum", "message": "Database vacuumed successfully"}),
1375 ),
1376 Err(e) => (
1377 "500 Internal Server Error",
1378 serde_json::json!({"ok": false, "error": format!("vacuum error: {e}")}),
1379 ),
1380 }
1381}
1382
1383fn handle_maintenance_reembed(
1386 bridge: &MemoryBridge,
1387 handle: &Handle,
1388) -> (&'static str, serde_json::Value) {
1389 let store = &bridge.store;
1390 let result = block_in_place(|| handle.block_on(store.reembed_all()));
1391
1392 match result {
1393 Ok(count) => (
1394 "200 OK",
1395 serde_json::json!({"ok": true, "action": "reembed", "reembedded_count": count, "message": format!("Re-embedded {count} items")}),
1396 ),
1397 Err(e) => (
1398 "500 Internal Server Error",
1399 serde_json::json!({"ok": false, "error": format!("reembed_all error: {e}")}),
1400 ),
1401 }
1402}
1403
1404fn handle_maintenance_reconcile(
1407 body: &str,
1408 bridge: &MemoryBridge,
1409 handle: &Handle,
1410) -> (&'static str, serde_json::Value) {
1411 let params: serde_json::Value = match serde_json::from_str(body) {
1412 Ok(v) => v,
1413 Err(e) => {
1414 return (
1415 "400 Bad Request",
1416 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1417 )
1418 }
1419 };
1420
1421 let action_str = params
1422 .get("action")
1423 .and_then(|v| v.as_str())
1424 .unwrap_or("ReportOnly");
1425
1426 let action = match action_str {
1427 "RebuildFts" => semantic_memory::ReconcileAction::RebuildFts,
1428 "ReEmbed" => semantic_memory::ReconcileAction::ReEmbed,
1429 _ => semantic_memory::ReconcileAction::ReportOnly,
1430 };
1431
1432 let store = &bridge.store;
1433 let result = block_in_place(|| handle.block_on(store.reconcile(action)));
1434
1435 match result {
1436 Ok(report) => (
1437 "200 OK",
1438 serde_json::json!({
1439 "ok": report.ok,
1440 "action": "reconcile",
1441 "reconcile_action": action_str,
1442 "integrity": {
1443 "ok": report.ok,
1444 "schema_version": report.schema_version,
1445 "fact_count": report.fact_count,
1446 "chunk_count": report.chunk_count,
1447 "message_count": report.message_count,
1448 "facts_missing_embeddings": report.facts_missing_embeddings,
1449 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1450 "issues": report.issues,
1451 "issue_count": report.issues.len(),
1452 },
1453 "message": if report.ok {
1454 "Reconciliation completed, no issues found".to_string()
1455 } else {
1456 format!("Reconciliation completed with {} issues", report.issues.len())
1457 },
1458 }),
1459 ),
1460 Err(e) => (
1461 "500 Internal Server Error",
1462 serde_json::json!({"ok": false, "error": format!("reconcile error: {e}")}),
1463 ),
1464 }
1465}
1466
1467fn handle_maintenance_rebuild_hnsw(
1473 bridge: &MemoryBridge,
1474 handle: &Handle,
1475) -> (&'static str, serde_json::Value) {
1476 #[cfg(feature = "hnsw")]
1477 {
1478 let store = &bridge.store;
1479 let result = block_in_place(|| handle.block_on(store.rebuild_hnsw_index()));
1480
1481 match result {
1482 Ok(receipt) => (
1483 "200 OK",
1484 serde_json::json!({
1485 "ok": true,
1486 "action": "rebuild-hnsw",
1487 "message": "HNSW index rebuilt successfully",
1488 "generation_id": receipt.generation_id,
1489 "vector_count": receipt.source_row_count,
1490 }),
1491 ),
1492 Err(e) => (
1493 "500 Internal Server Error",
1494 serde_json::json!({"ok": false, "error": format!("rebuild_hnsw error: {e}")}),
1495 ),
1496 }
1497 }
1498
1499 #[cfg(not(feature = "hnsw"))]
1500 {
1501 let _ = bridge;
1502 let _ = handle;
1503 (
1504 "200 OK",
1505 serde_json::json!({
1506 "ok": true,
1507 "action": "rebuild-hnsw",
1508 "message": "HNSW rebuild not applicable — usearch backend does not require rebuild",
1509 "skipped": true,
1510 }),
1511 )
1512 }
1513}
1514
1515fn handle_maintenance_compact_hnsw(
1520 bridge: &MemoryBridge,
1521 handle: &Handle,
1522) -> (&'static str, serde_json::Value) {
1523 #[cfg(feature = "hnsw")]
1524 {
1525 let store = &bridge.store;
1526 let result = block_in_place(|| handle.block_on(store.compact_hnsw()));
1527
1528 match result {
1529 Ok(()) => (
1530 "200 OK",
1531 serde_json::json!({"ok": true, "action": "compact-hnsw", "message": "HNSW index compacted successfully"}),
1532 ),
1533 Err(e) => (
1534 "500 Internal Server Error",
1535 serde_json::json!({"ok": false, "error": format!("compact_hnsw error: {e}")}),
1536 ),
1537 }
1538 }
1539
1540 #[cfg(not(feature = "hnsw"))]
1541 {
1542 let _ = bridge;
1543 let _ = handle;
1544 (
1545 "200 OK",
1546 serde_json::json!({
1547 "ok": true,
1548 "action": "compact-hnsw",
1549 "message": "HNSW compaction not applicable — usearch backend does not require compaction",
1550 "skipped": true,
1551 }),
1552 )
1553 }
1554}
1555
1556#[cfg(test)]
1557mod connection_slot_tests {
1558 use super::*;
1559
1560 #[test]
1561 fn connection_slot_is_released_when_handler_unwinds() {
1562 let active = Arc::new(AtomicUsize::new(1));
1563 let result = std::panic::catch_unwind({
1564 let active = active.clone();
1565 move || {
1566 let _slot = ConnectionSlot::new(active);
1567 panic!("injected handler panic");
1568 }
1569 });
1570 assert!(result.is_err());
1571 assert_eq!(active.load(Ordering::Acquire), 0);
1572 }
1573
1574 #[test]
1575 fn rerank_failure_preserves_original_order_and_scores() {
1576 let results = vec![
1577 serde_json::json!({"result_id": "fact:first", "score": 0.9, "content": "first"}),
1578 serde_json::json!({"result_id": "fact:second", "score": 0.8, "content": "second"}),
1579 ];
1580 let (reranked, status) =
1581 rerank_results_at("query", &results, RERANK_MODEL, "http://127.0.0.1:0");
1582
1583 assert_eq!(status, "degraded");
1584 assert_eq!(reranked, results);
1585 assert!(reranked
1586 .iter()
1587 .all(|result| result.get("rerank_score").is_none()));
1588 }
1589
1590 #[test]
1591 fn rerank_limits_use_utf8_bytes_and_bound_search_candidates() {
1592 let content = "🦀".repeat(MAX_RERANK_CONTENT_BYTES);
1593 let truncated = truncate_rerank_content(&content);
1594 assert!(truncated.len() <= MAX_RERANK_CONTENT_BYTES);
1595 assert!(truncated.is_char_boundary(truncated.len()));
1596
1597 assert_eq!(rerank_candidate_limit(1), 2);
1598 assert_eq!(
1599 rerank_candidate_limit(MAX_TOP_K as usize),
1600 MAX_RERANK_RESULTS
1601 );
1602 }
1603}