1#![forbid(unsafe_code)]
2
3mod admission;
4mod observability;
5
6use std::net::SocketAddr;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use admission::{
11 AdmissionClass, AdmissionConfig, AdmissionController, AdmissionError, RuntimeAdmissionStatus,
12};
13use axum::extract::{Path, Query, Request as AxumRequest};
14use axum::http::HeaderMap;
15use axum::http::StatusCode;
16use axum::middleware::{self, Next};
17use axum::response::IntoResponse;
18use axum::routing::{get, post};
19use axum::{Json, Router};
20use mnemara_core::{
21 AffectiveAnnotation, AffectiveAnnotationProvenance, ArchiveReceipt, ArchiveRequest,
22 ArtifactPointer, BatchUpsertRequest, ChangefeedReport, ChangefeedRequest, CompactionReport,
23 CompactionRequest, ConflictAnnotation, ConflictResolutionKind, ConflictReviewState,
24 DeleteReceipt, DeleteRequest, EPISODE_SCHEMA_VERSION, EmbeddingProviderKind, EngineTuningInfo,
25 EpisodeContext, EpisodeContinuityState, EpisodeSalience, ExportRequest, GraphInspectionEdge,
26 GraphInspectionEdgeKind, GraphInspectionNode, GraphInspectionReport, GraphInspectionRequest,
27 ImportMode, ImportReport, ImportRequest, IntegrityCheckReport, IntegrityCheckRequest,
28 LineageLink, LineageRelationKind, MaintenanceRunReport, MaintenanceRunRequest,
29 MaintenanceStats, MemoryHistoricalState, MemoryQualityState, MemoryRecord, MemoryRecordKind,
30 MemoryScope, MemoryStore, MemoryTrustLevel, NamespaceStats, OperationTrace,
31 OperationTraceSummary, PortableRecord, PortableStorePackage, RecallCandidateSource,
32 RecallExplanation, RecallFilters, RecallHistoricalMode, RecallPlannerStage,
33 RecallPlanningProfile, RecallPlanningTrace, RecallPolicyProfile, RecallQuery, RecallResult,
34 RecallScorerKind, RecallScoringProfile, RecallTemporalOrder, RecallTraceCandidate,
35 RecoverReceipt, RecoverRequest, RepairReport, RepairRequest, SnapshotManifest,
36 SnapshotShipReport, SnapshotShipRequest, StoreStatsReport, StoreStatsRequest, SuppressReceipt,
37 SuppressRequest, SynthesisProposal, SynthesisReport, SynthesisRequest, TimeTravelRecallRequest,
38 TraceListRequest, TraceOperationKind, TraceStatus, UpsertReceipt, UpsertRequest,
39};
40use mnemara_protocol::v1::memory_service_server::{MemoryService, MemoryServiceServer};
41use mnemara_protocol::v1::{
42 AffectiveAnnotation as ProtoAffectiveAnnotation, ArchiveReply,
43 ArchiveRequest as ProtoArchiveRequest, ArtifactPointer as ProtoArtifactPointer,
44 BatchUpsertMemoryRecordsReply, BatchUpsertMemoryRecordsRequest, CompactReply,
45 CompactRequest as ProtoCompactRequest, ConflictAnnotation as ProtoConflictAnnotation,
46 DeleteReply, DeleteRequest as ProtoDeleteRequest,
47 EmbeddingProviderKind as ProtoEmbeddingProviderKind, EngineTuningInfo as ProtoEngineTuningInfo,
48 EpisodeContext as ProtoEpisodeContext, EpisodeSalience as ProtoEpisodeSalience,
49 ExportReply as ProtoExportReply, ExportRequest as ProtoExportRequest,
50 GetTraceRequest as ProtoGetTraceRequest, GraphInspectionEdge as ProtoGraphInspectionEdge,
51 GraphInspectionNode as ProtoGraphInspectionNode, GraphInspectionReply,
52 GraphInspectionRequest as ProtoGraphInspectionRequest, ImportMode as ProtoImportMode,
53 ImportReply as ProtoImportReply, ImportRequest as ProtoImportRequest,
54 IntegrityCheckReply as ProtoIntegrityCheckReply,
55 IntegrityCheckRequest as ProtoIntegrityCheckRequest, LineageLink as ProtoLineageLink,
56 ListTracesReply as ProtoListTracesReply, ListTracesRequest as ProtoListTracesRequest,
57 MaintenanceRunReply, MaintenanceRunRequest as ProtoMaintenanceRunRequest,
58 MaintenanceStats as ProtoMaintenanceStats, MemoryRecord as ProtoMemoryRecord,
59 MemoryScope as ProtoMemoryScope, NamespaceStats as ProtoNamespaceStats,
60 OperationTrace as ProtoOperationTrace, OperationTraceSummary as ProtoOperationTraceSummary,
61 PortableRecord as ProtoPortableRecord, RecallExplanation as ProtoRecallExplanation,
62 RecallFilters as ProtoRecallFilters, RecallHit as ProtoRecallHit,
63 RecallPlanningTrace as ProtoRecallPlanningTrace,
64 RecallPolicyProfile as ProtoRecallPolicyProfile, RecallReply,
65 RecallRequest as ProtoRecallRequest, RecallScoreBreakdown as ProtoRecallScoreBreakdown,
66 RecallScorerKind as ProtoRecallScorerKind, RecallScoringProfile as ProtoRecallScoringProfile,
67 RecallTraceCandidate as ProtoRecallTraceCandidate, RecoverReply,
68 RecoverRequest as ProtoRecoverRequest, RepairReply as ProtoRepairReply,
69 RepairRequest as ProtoRepairRequest, SnapshotReply, SnapshotRequest, SnapshotShipReply,
70 SnapshotShipRequest as ProtoSnapshotShipRequest, StoreStatsReply as ProtoStoreStatsReply,
71 StoreStatsRequest as ProtoStoreStatsRequest, SuppressReply,
72 SuppressRequest as ProtoSuppressRequest, SynthesisProposal as ProtoSynthesisProposal,
73 SynthesisReply, SynthesisRequest as ProtoSynthesisRequest,
74 TimeTravelRecallRequest as ProtoTimeTravelRecallRequest,
75 TraceOperationKind as ProtoTraceOperationKind, TraceStatus as ProtoTraceStatus,
76 UpsertMemoryRecordReply, UpsertMemoryRecordRequest as ProtoUpsertMemoryRecordRequest,
77};
78use observability::{TraceRegistry, TraceRegistrySnapshot, now_unix_ms};
79use tonic::{Request, Response, Status};
80
81#[derive(Debug, Clone, serde::Serialize)]
82struct HealthStatus {
83 status: &'static str,
84}
85
86#[derive(Debug, Clone, serde::Serialize)]
87struct ReadyStatus {
88 status: &'static str,
89 record_count: u64,
90 namespaces: Vec<String>,
91}
92
93#[derive(Debug, Clone, serde::Serialize)]
94struct ServerRuntimeStatus {
95 backend: String,
96 admission: RuntimeAdmissionStatus,
97 traces: TraceRegistrySnapshot,
98}
99
100#[derive(Debug, Clone, serde::Serialize)]
101struct HttpErrorBody {
102 error: String,
103}
104
105#[derive(Debug, Default)]
106struct MethodMetrics {
107 started: AtomicU64,
108 ok: AtomicU64,
109 invalid_argument: AtomicU64,
110 conflict: AtomicU64,
111 unimplemented: AtomicU64,
112 internal: AtomicU64,
113}
114
115impl MethodMetrics {
116 fn record_started(&self) {
117 self.started.fetch_add(1, Ordering::Relaxed);
118 }
119
120 fn record_status(&self, status: &Status) {
121 match status.code() {
122 tonic::Code::Ok => {
123 self.ok.fetch_add(1, Ordering::Relaxed);
124 }
125 tonic::Code::InvalidArgument => {
126 self.invalid_argument.fetch_add(1, Ordering::Relaxed);
127 }
128 tonic::Code::AlreadyExists => {
129 self.conflict.fetch_add(1, Ordering::Relaxed);
130 }
131 tonic::Code::Unimplemented => {
132 self.unimplemented.fetch_add(1, Ordering::Relaxed);
133 }
134 _ => {
135 self.internal.fetch_add(1, Ordering::Relaxed);
136 }
137 }
138 }
139}
140
141#[derive(Debug, Default)]
142pub struct ServerMetrics {
143 grpc_upsert: MethodMetrics,
144 grpc_batch_upsert: MethodMetrics,
145 grpc_recall: MethodMetrics,
146 grpc_compact: MethodMetrics,
147 grpc_synthesis: MethodMetrics,
148 grpc_snapshot: MethodMetrics,
149 grpc_delete: MethodMetrics,
150 grpc_archive: MethodMetrics,
151 grpc_suppress: MethodMetrics,
152 grpc_recover: MethodMetrics,
153 grpc_stats: MethodMetrics,
154 grpc_integrity: MethodMetrics,
155 grpc_repair: MethodMetrics,
156 grpc_maintenance: MethodMetrics,
157 grpc_snapshot_ship: MethodMetrics,
158 http_healthz: AtomicU64,
159 http_readyz: AtomicU64,
160 http_snapshot: AtomicU64,
161 http_compact: AtomicU64,
162 http_synthesis: AtomicU64,
163 http_delete: AtomicU64,
164 http_archive: AtomicU64,
165 http_suppress: AtomicU64,
166 http_recover: AtomicU64,
167 http_metrics: AtomicU64,
168 http_traces: AtomicU64,
169 http_runtime: AtomicU64,
170 http_export: AtomicU64,
171 http_import: AtomicU64,
172 http_maintenance: AtomicU64,
173 http_snapshot_ship: AtomicU64,
174 http_rejected_body_too_large: AtomicU64,
175 admission_rejected: AtomicU64,
176 admission_timed_out: AtomicU64,
177 trace_records: AtomicU64,
178 trace_evictions: AtomicU64,
179 trace_reads: AtomicU64,
180}
181
182impl ServerMetrics {
183 pub fn render(&self) -> String {
184 let mut output = String::new();
185 append_counter(
186 &mut output,
187 "mnemara_grpc_upsert_requests_started_total",
188 self.grpc_upsert.started.load(Ordering::Relaxed),
189 );
190 append_counter(
191 &mut output,
192 "mnemara_grpc_upsert_requests_ok_total",
193 self.grpc_upsert.ok.load(Ordering::Relaxed),
194 );
195 append_counter(
196 &mut output,
197 "mnemara_grpc_upsert_requests_invalid_argument_total",
198 self.grpc_upsert.invalid_argument.load(Ordering::Relaxed),
199 );
200 append_counter(
201 &mut output,
202 "mnemara_grpc_upsert_requests_conflict_total",
203 self.grpc_upsert.conflict.load(Ordering::Relaxed),
204 );
205 append_counter(
206 &mut output,
207 "mnemara_grpc_upsert_requests_unimplemented_total",
208 self.grpc_upsert.unimplemented.load(Ordering::Relaxed),
209 );
210 append_counter(
211 &mut output,
212 "mnemara_grpc_upsert_requests_internal_total",
213 self.grpc_upsert.internal.load(Ordering::Relaxed),
214 );
215 append_counter(
216 &mut output,
217 "mnemara_grpc_batch_upsert_requests_started_total",
218 self.grpc_batch_upsert.started.load(Ordering::Relaxed),
219 );
220 append_counter(
221 &mut output,
222 "mnemara_grpc_batch_upsert_requests_ok_total",
223 self.grpc_batch_upsert.ok.load(Ordering::Relaxed),
224 );
225 append_counter(
226 &mut output,
227 "mnemara_grpc_batch_upsert_requests_invalid_argument_total",
228 self.grpc_batch_upsert
229 .invalid_argument
230 .load(Ordering::Relaxed),
231 );
232 append_counter(
233 &mut output,
234 "mnemara_grpc_recall_requests_started_total",
235 self.grpc_recall.started.load(Ordering::Relaxed),
236 );
237 append_counter(
238 &mut output,
239 "mnemara_grpc_recall_requests_ok_total",
240 self.grpc_recall.ok.load(Ordering::Relaxed),
241 );
242 append_counter(
243 &mut output,
244 "mnemara_grpc_recall_requests_invalid_argument_total",
245 self.grpc_recall.invalid_argument.load(Ordering::Relaxed),
246 );
247 append_counter(
248 &mut output,
249 "mnemara_grpc_compact_requests_started_total",
250 self.grpc_compact.started.load(Ordering::Relaxed),
251 );
252 append_counter(
253 &mut output,
254 "mnemara_grpc_compact_requests_ok_total",
255 self.grpc_compact.ok.load(Ordering::Relaxed),
256 );
257 append_counter(
258 &mut output,
259 "mnemara_grpc_synthesis_requests_started_total",
260 self.grpc_synthesis.started.load(Ordering::Relaxed),
261 );
262 append_counter(
263 &mut output,
264 "mnemara_grpc_synthesis_requests_ok_total",
265 self.grpc_synthesis.ok.load(Ordering::Relaxed),
266 );
267 append_counter(
268 &mut output,
269 "mnemara_grpc_snapshot_requests_started_total",
270 self.grpc_snapshot.started.load(Ordering::Relaxed),
271 );
272 append_counter(
273 &mut output,
274 "mnemara_grpc_snapshot_requests_ok_total",
275 self.grpc_snapshot.ok.load(Ordering::Relaxed),
276 );
277 append_counter(
278 &mut output,
279 "mnemara_grpc_delete_requests_started_total",
280 self.grpc_delete.started.load(Ordering::Relaxed),
281 );
282 append_counter(
283 &mut output,
284 "mnemara_grpc_delete_requests_ok_total",
285 self.grpc_delete.ok.load(Ordering::Relaxed),
286 );
287 append_counter(
288 &mut output,
289 "mnemara_grpc_archive_requests_started_total",
290 self.grpc_archive.started.load(Ordering::Relaxed),
291 );
292 append_counter(
293 &mut output,
294 "mnemara_grpc_archive_requests_ok_total",
295 self.grpc_archive.ok.load(Ordering::Relaxed),
296 );
297 append_counter(
298 &mut output,
299 "mnemara_grpc_suppress_requests_started_total",
300 self.grpc_suppress.started.load(Ordering::Relaxed),
301 );
302 append_counter(
303 &mut output,
304 "mnemara_grpc_suppress_requests_ok_total",
305 self.grpc_suppress.ok.load(Ordering::Relaxed),
306 );
307 append_counter(
308 &mut output,
309 "mnemara_grpc_recover_requests_started_total",
310 self.grpc_recover.started.load(Ordering::Relaxed),
311 );
312 append_counter(
313 &mut output,
314 "mnemara_grpc_recover_requests_ok_total",
315 self.grpc_recover.ok.load(Ordering::Relaxed),
316 );
317 append_counter(
318 &mut output,
319 "mnemara_grpc_maintenance_requests_started_total",
320 self.grpc_maintenance.started.load(Ordering::Relaxed),
321 );
322 append_counter(
323 &mut output,
324 "mnemara_grpc_maintenance_requests_ok_total",
325 self.grpc_maintenance.ok.load(Ordering::Relaxed),
326 );
327 append_counter(
328 &mut output,
329 "mnemara_grpc_snapshot_ship_requests_started_total",
330 self.grpc_snapshot_ship.started.load(Ordering::Relaxed),
331 );
332 append_counter(
333 &mut output,
334 "mnemara_grpc_snapshot_ship_requests_ok_total",
335 self.grpc_snapshot_ship.ok.load(Ordering::Relaxed),
336 );
337 append_counter(
338 &mut output,
339 "mnemara_http_healthz_requests_total",
340 self.http_healthz.load(Ordering::Relaxed),
341 );
342 append_counter(
343 &mut output,
344 "mnemara_http_readyz_requests_total",
345 self.http_readyz.load(Ordering::Relaxed),
346 );
347 append_counter(
348 &mut output,
349 "mnemara_http_snapshot_requests_total",
350 self.http_snapshot.load(Ordering::Relaxed),
351 );
352 append_counter(
353 &mut output,
354 "mnemara_http_compact_requests_total",
355 self.http_compact.load(Ordering::Relaxed),
356 );
357 append_counter(
358 &mut output,
359 "mnemara_http_synthesis_requests_total",
360 self.http_synthesis.load(Ordering::Relaxed),
361 );
362 append_counter(
363 &mut output,
364 "mnemara_http_delete_requests_total",
365 self.http_delete.load(Ordering::Relaxed),
366 );
367 append_counter(
368 &mut output,
369 "mnemara_http_archive_requests_total",
370 self.http_archive.load(Ordering::Relaxed),
371 );
372 append_counter(
373 &mut output,
374 "mnemara_http_suppress_requests_total",
375 self.http_suppress.load(Ordering::Relaxed),
376 );
377 append_counter(
378 &mut output,
379 "mnemara_http_recover_requests_total",
380 self.http_recover.load(Ordering::Relaxed),
381 );
382 append_counter(
383 &mut output,
384 "mnemara_http_metrics_requests_total",
385 self.http_metrics.load(Ordering::Relaxed),
386 );
387 append_counter(
388 &mut output,
389 "mnemara_http_traces_requests_total",
390 self.http_traces.load(Ordering::Relaxed),
391 );
392 append_counter(
393 &mut output,
394 "mnemara_http_runtime_requests_total",
395 self.http_runtime.load(Ordering::Relaxed),
396 );
397 append_counter(
398 &mut output,
399 "mnemara_http_export_requests_total",
400 self.http_export.load(Ordering::Relaxed),
401 );
402 append_counter(
403 &mut output,
404 "mnemara_http_import_requests_total",
405 self.http_import.load(Ordering::Relaxed),
406 );
407 append_counter(
408 &mut output,
409 "mnemara_http_maintenance_requests_total",
410 self.http_maintenance.load(Ordering::Relaxed),
411 );
412 append_counter(
413 &mut output,
414 "mnemara_http_snapshot_ship_requests_total",
415 self.http_snapshot_ship.load(Ordering::Relaxed),
416 );
417 append_counter(
418 &mut output,
419 "mnemara_http_rejected_body_too_large_total",
420 self.http_rejected_body_too_large.load(Ordering::Relaxed),
421 );
422 append_counter(
423 &mut output,
424 "mnemara_admission_rejected_total",
425 self.admission_rejected.load(Ordering::Relaxed),
426 );
427 append_counter(
428 &mut output,
429 "mnemara_admission_timed_out_total",
430 self.admission_timed_out.load(Ordering::Relaxed),
431 );
432 append_counter(
433 &mut output,
434 "mnemara_trace_records_total",
435 self.trace_records.load(Ordering::Relaxed),
436 );
437 append_counter(
438 &mut output,
439 "mnemara_trace_evictions_total",
440 self.trace_evictions.load(Ordering::Relaxed),
441 );
442 append_counter(
443 &mut output,
444 "mnemara_trace_reads_total",
445 self.trace_reads.load(Ordering::Relaxed),
446 );
447 output
448 }
449}
450
451fn append_counter(output: &mut String, name: &str, value: u64) {
452 output.push_str("# TYPE ");
453 output.push_str(name);
454 output.push_str(" counter\n");
455 output.push_str(name);
456 output.push(' ');
457 output.push_str(&value.to_string());
458 output.push('\n');
459}
460
461#[derive(Debug, Clone)]
462pub struct ServerLimits {
463 pub max_http_body_bytes: usize,
464 pub max_batch_upsert_requests: usize,
465 pub max_recall_items: usize,
466 pub max_query_text_bytes: usize,
467 pub max_record_content_bytes: usize,
468 pub max_labels_per_scope: usize,
469 pub max_inflight_reads: usize,
470 pub max_inflight_writes: usize,
471 pub max_inflight_admin: usize,
472 pub max_queued_requests: usize,
473 pub max_tenant_inflight: usize,
474 pub queue_wait_timeout_ms: u64,
475 pub trace_retention: usize,
476}
477
478impl Default for ServerLimits {
479 fn default() -> Self {
480 Self {
481 max_http_body_bytes: 64 * 1024,
482 max_batch_upsert_requests: 128,
483 max_recall_items: 64,
484 max_query_text_bytes: 4 * 1024,
485 max_record_content_bytes: 32 * 1024,
486 max_labels_per_scope: 32,
487 max_inflight_reads: 64,
488 max_inflight_writes: 32,
489 max_inflight_admin: 8,
490 max_queued_requests: 256,
491 max_tenant_inflight: 16,
492 queue_wait_timeout_ms: 2_000,
493 trace_retention: 256,
494 }
495 }
496}
497
498#[derive(Debug, Clone, Default)]
499pub struct AuthConfig {
500 pub bearer_token: Option<String>,
501 pub protect_metrics: bool,
502 pub token_policies: Vec<TokenPolicy>,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506pub enum AuthPermission {
507 Read,
508 Write,
509 Admin,
510 Metrics,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct TokenPolicy {
515 pub token: String,
516 pub permissions: Vec<AuthPermission>,
517}
518
519impl AuthConfig {
520 fn has_authentication(&self) -> bool {
521 self.bearer_token.is_some() || !self.token_policies.is_empty()
522 }
523
524 fn authorize(&self, authorization_header: Option<&str>, permission: AuthPermission) -> bool {
525 if !self.has_authentication() {
526 return true;
527 }
528
529 let Some(token) = bearer_token_from_header(authorization_header) else {
530 return false;
531 };
532
533 if self.bearer_token.as_deref() == Some(token) {
534 return true;
535 }
536
537 self.token_policies
538 .iter()
539 .any(|policy| policy.token == token && policy.permissions.contains(&permission))
540 }
541}
542
543#[derive(Debug, Clone, serde::Deserialize)]
544pub struct DeleteHttpRequest {
545 pub tenant_id: String,
546 pub namespace: String,
547 pub record_id: String,
548 pub hard_delete: bool,
549 pub audit_reason: String,
550}
551
552#[derive(Debug, Clone, serde::Deserialize)]
553pub struct ArchiveHttpRequest {
554 pub tenant_id: String,
555 pub namespace: String,
556 pub record_id: String,
557 pub dry_run: bool,
558 pub audit_reason: String,
559}
560
561#[derive(Debug, Clone, serde::Deserialize)]
562pub struct SuppressHttpRequest {
563 pub tenant_id: String,
564 pub namespace: String,
565 pub record_id: String,
566 pub dry_run: bool,
567 pub audit_reason: String,
568}
569
570#[derive(Debug, Clone, serde::Deserialize)]
571pub struct RecoverHttpRequest {
572 pub tenant_id: String,
573 pub namespace: String,
574 pub record_id: String,
575 pub dry_run: bool,
576 pub audit_reason: String,
577 pub quality_state: MemoryQualityState,
578 pub historical_state: Option<MemoryHistoricalState>,
579}
580
581pub struct GrpcMemoryService<S> {
582 store: Arc<S>,
583 runtime: ServerRuntime,
584}
585
586impl<S> Clone for GrpcMemoryService<S> {
587 fn clone(&self) -> Self {
588 Self {
589 store: Arc::clone(&self.store),
590 runtime: self.runtime.clone(),
591 }
592 }
593}
594
595#[derive(Debug)]
596struct ServerRuntimeInner {
597 backend: Arc<String>,
598 limits: Arc<ServerLimits>,
599 metrics: Arc<ServerMetrics>,
600 auth: Arc<AuthConfig>,
601 traces: Arc<TraceRegistry>,
602 admission: Arc<AdmissionController>,
603}
604
605#[derive(Debug, Clone)]
606pub struct ServerRuntime {
607 inner: Arc<ServerRuntimeInner>,
608}
609
610impl ServerRuntime {
611 pub fn new(
612 backend: impl Into<String>,
613 limits: ServerLimits,
614 metrics: Arc<ServerMetrics>,
615 auth: AuthConfig,
616 ) -> Self {
617 let admission = AdmissionController::new(AdmissionConfig {
618 max_inflight_reads: limits.max_inflight_reads,
619 max_inflight_writes: limits.max_inflight_writes,
620 max_inflight_admin: limits.max_inflight_admin,
621 max_queued_requests: limits.max_queued_requests,
622 max_tenant_inflight: limits.max_tenant_inflight,
623 queue_wait_timeout_ms: limits.queue_wait_timeout_ms,
624 });
625 Self {
626 inner: Arc::new(ServerRuntimeInner {
627 backend: Arc::new(backend.into()),
628 traces: Arc::new(TraceRegistry::new(limits.trace_retention)),
629 admission: Arc::new(admission),
630 auth: Arc::new(auth),
631 metrics,
632 limits: Arc::new(limits),
633 }),
634 }
635 }
636
637 fn limits(&self) -> &Arc<ServerLimits> {
638 &self.inner.limits
639 }
640
641 fn metrics(&self) -> &Arc<ServerMetrics> {
642 &self.inner.metrics
643 }
644
645 fn backend(&self) -> &Arc<String> {
646 &self.inner.backend
647 }
648
649 fn auth(&self) -> &Arc<AuthConfig> {
650 &self.inner.auth
651 }
652
653 fn traces(&self) -> &Arc<TraceRegistry> {
654 &self.inner.traces
655 }
656
657 fn admission(&self) -> &Arc<AdmissionController> {
658 &self.inner.admission
659 }
660}
661
662impl<S: MemoryStore> GrpcMemoryService<S> {
663 pub fn new(store: Arc<S>) -> Self {
664 let backend = store.as_ref().backend_kind().to_string();
665 Self::with_runtime(
666 store,
667 ServerRuntime::new(
668 backend,
669 ServerLimits::default(),
670 Arc::new(ServerMetrics::default()),
671 AuthConfig::default(),
672 ),
673 )
674 }
675
676 pub fn with_limits(store: Arc<S>, limits: ServerLimits) -> Self {
677 let backend = store.as_ref().backend_kind().to_string();
678 Self::with_runtime(
679 store,
680 ServerRuntime::new(
681 backend,
682 limits,
683 Arc::new(ServerMetrics::default()),
684 AuthConfig::default(),
685 ),
686 )
687 }
688
689 pub fn with_observability(
690 store: Arc<S>,
691 limits: ServerLimits,
692 metrics: Arc<ServerMetrics>,
693 ) -> Self {
694 let backend = store.as_ref().backend_kind().to_string();
695 Self::with_runtime(
696 store,
697 ServerRuntime::new(backend, limits, metrics, AuthConfig::default()),
698 )
699 }
700
701 pub fn with_runtime_config(
702 store: Arc<S>,
703 limits: ServerLimits,
704 metrics: Arc<ServerMetrics>,
705 auth: AuthConfig,
706 ) -> Self {
707 let backend = store.as_ref().backend_kind().to_string();
708 Self::with_runtime(store, ServerRuntime::new(backend, limits, metrics, auth))
709 }
710
711 pub fn with_runtime(store: Arc<S>, runtime: ServerRuntime) -> Self {
712 Self { store, runtime }
713 }
714
715 pub fn into_service(self) -> MemoryServiceServer<Self>
716 where
717 Self: MemoryService,
718 {
719 MemoryServiceServer::new(self)
720 }
721}
722
723pub async fn serve<S>(
724 addr: SocketAddr,
725 store: Arc<S>,
726 limits: ServerLimits,
727 auth: AuthConfig,
728) -> Result<(), tonic::transport::Error>
729where
730 S: MemoryStore + 'static,
731{
732 let backend = store.as_ref().backend_kind().to_string();
733 serve_with_runtime(
734 addr,
735 store,
736 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
737 )
738 .await
739}
740
741pub async fn serve_with_runtime<S>(
742 addr: SocketAddr,
743 store: Arc<S>,
744 runtime: ServerRuntime,
745) -> Result<(), tonic::transport::Error>
746where
747 S: MemoryStore + 'static,
748{
749 tonic::transport::Server::builder()
750 .add_service(GrpcMemoryService::with_runtime(store, runtime).into_service())
751 .serve(addr)
752 .await
753}
754
755pub fn http_app<S>(store: Arc<S>, limits: ServerLimits, auth: AuthConfig) -> Router
756where
757 S: MemoryStore + 'static,
758{
759 let backend = store.as_ref().backend_kind().to_string();
760 http_app_with_runtime(
761 store,
762 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
763 )
764}
765
766pub fn http_app_with_metrics<S>(
767 store: Arc<S>,
768 limits: ServerLimits,
769 metrics: Arc<ServerMetrics>,
770 auth: AuthConfig,
771) -> Router
772where
773 S: MemoryStore + 'static,
774{
775 let backend = store.as_ref().backend_kind().to_string();
776 http_app_with_runtime(store, ServerRuntime::new(backend, limits, metrics, auth))
777}
778
779pub fn http_app_with_runtime<S>(store: Arc<S>, runtime: ServerRuntime) -> Router
780where
781 S: MemoryStore + 'static,
782{
783 let limits = Arc::clone(runtime.limits());
784 let ready_store = Arc::clone(&store);
785 let upsert_store = Arc::clone(&store);
786 let batch_upsert_store = Arc::clone(&store);
787 let recall_store = Arc::clone(&store);
788 let recall_as_of_store = Arc::clone(&store);
789 let snapshot_store = Arc::clone(&store);
790 let stats_store = Arc::clone(&store);
791 let graph_store = Arc::clone(&store);
792 let changefeed_store = Arc::clone(&store);
793 let integrity_store = Arc::clone(&store);
794 let repair_store = Arc::clone(&store);
795 let compact_store = Arc::clone(&store);
796 let synthesis_store = Arc::clone(&store);
797 let delete_store = Arc::clone(&store);
798 let archive_store = Arc::clone(&store);
799 let suppress_store = Arc::clone(&store);
800 let recover_store = Arc::clone(&store);
801 let maintenance_store = Arc::clone(&store);
802 let export_store = Arc::clone(&store);
803 let ship_store = Arc::clone(&store);
804 let import_store = Arc::clone(&store);
805 let ready_runtime = runtime.clone();
806 let upsert_runtime = runtime.clone();
807 let batch_upsert_runtime = runtime.clone();
808 let recall_runtime = runtime.clone();
809 let recall_as_of_runtime = runtime.clone();
810 let snapshot_runtime = runtime.clone();
811 let stats_runtime = runtime.clone();
812 let graph_runtime = runtime.clone();
813 let changefeed_runtime = runtime.clone();
814 let integrity_runtime = runtime.clone();
815 let repair_runtime = runtime.clone();
816 let compact_runtime = runtime.clone();
817 let synthesis_runtime = runtime.clone();
818 let delete_runtime = runtime.clone();
819 let archive_runtime = runtime.clone();
820 let suppress_runtime = runtime.clone();
821 let recover_runtime = runtime.clone();
822 let maintenance_runtime = runtime.clone();
823 let traces_runtime = runtime.clone();
824 let trace_runtime = runtime.clone();
825 let export_runtime = runtime.clone();
826 let ship_runtime = runtime.clone();
827 let import_runtime = runtime.clone();
828 let runtime_status_runtime = runtime.clone();
829 let metrics_metrics = Arc::clone(runtime.metrics());
830 let middleware_limits = Arc::clone(&limits);
831 let middleware_metrics = Arc::clone(runtime.metrics());
832 let middleware_auth = Arc::clone(runtime.auth());
833
834 let health_metrics = Arc::clone(runtime.metrics());
835
836 Router::new()
837 .route(
838 "/healthz",
839 get(move || {
840 let metrics = Arc::clone(&health_metrics);
841 async move { healthz(metrics).await }
842 }),
843 )
844 .route(
845 "/readyz",
846 get(move || {
847 let store = Arc::clone(&ready_store);
848 let runtime = ready_runtime.clone();
849 async move { readyz(store, runtime).await }
850 }),
851 )
852 .route(
853 "/memory/upsert",
854 post(
855 move |headers: HeaderMap, Json(request): Json<UpsertRequest>| {
856 let store = Arc::clone(&upsert_store);
857 let runtime = upsert_runtime.clone();
858 async move { upsert_http(store, request, headers, runtime).await }
859 },
860 ),
861 )
862 .route(
863 "/memory/batch-upsert",
864 post(
865 move |headers: HeaderMap, Json(request): Json<BatchUpsertRequest>| {
866 let store = Arc::clone(&batch_upsert_store);
867 let runtime = batch_upsert_runtime.clone();
868 async move { batch_upsert_http(store, request, headers, runtime).await }
869 },
870 ),
871 )
872 .route(
873 "/memory/recall",
874 post(
875 move |headers: HeaderMap, Json(request): Json<RecallQuery>| {
876 let store = Arc::clone(&recall_store);
877 let runtime = recall_runtime.clone();
878 async move { recall_http(store, request, headers, runtime).await }
879 },
880 ),
881 )
882 .route(
883 "/memory/recall-as-of",
884 post(
885 move |headers: HeaderMap, Json(request): Json<TimeTravelRecallRequest>| {
886 let store = Arc::clone(&recall_as_of_store);
887 let runtime = recall_as_of_runtime.clone();
888 async move { recall_as_of_http(store, request, headers, runtime).await }
889 },
890 ),
891 )
892 .route(
893 "/admin/snapshot",
894 get(move |headers: HeaderMap| {
895 let store = Arc::clone(&snapshot_store);
896 let runtime = snapshot_runtime.clone();
897 async move { snapshot_http(store, headers, runtime).await }
898 }),
899 )
900 .route(
901 "/admin/stats",
902 get(
903 move |headers: HeaderMap, Query(request): Query<StoreStatsRequest>| {
904 let store = Arc::clone(&stats_store);
905 let runtime = stats_runtime.clone();
906 async move { stats_http(store, request, headers, runtime).await }
907 },
908 ),
909 )
910 .route(
911 "/admin/graph",
912 post(
913 move |headers: HeaderMap, Json(request): Json<GraphInspectionRequest>| {
914 let store = Arc::clone(&graph_store);
915 let runtime = graph_runtime.clone();
916 async move { graph_http(store, request, headers, runtime).await }
917 },
918 ),
919 )
920 .route(
921 "/admin/changefeed",
922 get(
923 move |headers: HeaderMap, Query(request): Query<ChangefeedRequest>| {
924 let store = Arc::clone(&changefeed_store);
925 let runtime = changefeed_runtime.clone();
926 async move { changefeed_http(store, request, headers, runtime).await }
927 },
928 ),
929 )
930 .route(
931 "/admin/integrity",
932 get(
933 move |headers: HeaderMap, Query(request): Query<IntegrityCheckRequest>| {
934 let store = Arc::clone(&integrity_store);
935 let runtime = integrity_runtime.clone();
936 async move { integrity_http(store, request, headers, runtime).await }
937 },
938 ),
939 )
940 .route(
941 "/admin/repair",
942 post(
943 move |headers: HeaderMap, Json(request): Json<RepairRequest>| {
944 let store = Arc::clone(&repair_store);
945 let runtime = repair_runtime.clone();
946 async move { repair_http(store, request, headers, runtime).await }
947 },
948 ),
949 )
950 .route(
951 "/admin/compact",
952 post(
953 move |headers: HeaderMap, Json(request): Json<CompactionRequest>| {
954 let store = Arc::clone(&compact_store);
955 let runtime = compact_runtime.clone();
956 async move { compact_http(store, request, headers, runtime).await }
957 },
958 ),
959 )
960 .route(
961 "/admin/synthesize",
962 post(
963 move |headers: HeaderMap, Json(request): Json<SynthesisRequest>| {
964 let store = Arc::clone(&synthesis_store);
965 let runtime = synthesis_runtime.clone();
966 async move { synthesize_http(store, request, headers, runtime).await }
967 },
968 ),
969 )
970 .route(
971 "/admin/delete",
972 post(
973 move |headers: HeaderMap, Json(request): Json<DeleteHttpRequest>| {
974 let store = Arc::clone(&delete_store);
975 let runtime = delete_runtime.clone();
976 async move { delete_http(store, request, headers, runtime).await }
977 },
978 ),
979 )
980 .route(
981 "/admin/archive",
982 post(
983 move |headers: HeaderMap, Json(request): Json<ArchiveHttpRequest>| {
984 let store = Arc::clone(&archive_store);
985 let runtime = archive_runtime.clone();
986 async move { archive_http(store, request, headers, runtime).await }
987 },
988 ),
989 )
990 .route(
991 "/admin/suppress",
992 post(
993 move |headers: HeaderMap, Json(request): Json<SuppressHttpRequest>| {
994 let store = Arc::clone(&suppress_store);
995 let runtime = suppress_runtime.clone();
996 async move { suppress_http(store, request, headers, runtime).await }
997 },
998 ),
999 )
1000 .route(
1001 "/admin/recover",
1002 post(
1003 move |headers: HeaderMap, Json(request): Json<RecoverHttpRequest>| {
1004 let store = Arc::clone(&recover_store);
1005 let runtime = recover_runtime.clone();
1006 async move { recover_http(store, request, headers, runtime).await }
1007 },
1008 ),
1009 )
1010 .route(
1011 "/admin/maintenance/run",
1012 post(
1013 move |headers: HeaderMap, Json(request): Json<MaintenanceRunRequest>| {
1014 let store = Arc::clone(&maintenance_store);
1015 let runtime = maintenance_runtime.clone();
1016 async move { maintenance_run_http(store, request, headers, runtime).await }
1017 },
1018 ),
1019 )
1020 .route(
1021 "/admin/traces",
1022 get(move |Query(request): Query<TraceListRequest>| {
1023 let runtime = traces_runtime.clone();
1024 async move { traces_http(request, runtime).await }
1025 }),
1026 )
1027 .route(
1028 "/admin/traces/{trace_id}",
1029 get(move |Path(trace_id): Path<String>| {
1030 let runtime = trace_runtime.clone();
1031 async move { trace_http(trace_id, runtime).await }
1032 }),
1033 )
1034 .route(
1035 "/admin/runtime",
1036 get(move || {
1037 let runtime = runtime_status_runtime.clone();
1038 async move { runtime_status_http(runtime).await }
1039 }),
1040 )
1041 .route(
1042 "/admin/export",
1043 post(
1044 move |headers: HeaderMap, Json(request): Json<ExportRequest>| {
1045 let store = Arc::clone(&export_store);
1046 let runtime = export_runtime.clone();
1047 async move { export_http(store, request, headers, runtime).await }
1048 },
1049 ),
1050 )
1051 .route(
1052 "/admin/replication/ship",
1053 post(
1054 move |headers: HeaderMap, Json(request): Json<SnapshotShipRequest>| {
1055 let store = Arc::clone(&ship_store);
1056 let runtime = ship_runtime.clone();
1057 async move { snapshot_ship_http(store, request, headers, runtime).await }
1058 },
1059 ),
1060 )
1061 .route(
1062 "/admin/import",
1063 post(
1064 move |headers: HeaderMap, Json(request): Json<ImportRequest>| {
1065 let store = Arc::clone(&import_store);
1066 let runtime = import_runtime.clone();
1067 async move { import_http(store, request, headers, runtime).await }
1068 },
1069 ),
1070 )
1071 .route(
1072 "/metrics",
1073 get(move || {
1074 let metrics = Arc::clone(&metrics_metrics);
1075 async move { metrics_http(metrics).await }
1076 }),
1077 )
1078 .layer(middleware::from_fn(move |request, next| {
1079 let limits = Arc::clone(&middleware_limits);
1080 let metrics = Arc::clone(&middleware_metrics);
1081 let auth = Arc::clone(&middleware_auth);
1082 async move { enforce_http_guardrails(request, next, limits, metrics, auth).await }
1083 }))
1084}
1085
1086pub async fn serve_http<S>(
1087 addr: SocketAddr,
1088 store: Arc<S>,
1089 limits: ServerLimits,
1090 auth: AuthConfig,
1091) -> std::io::Result<()>
1092where
1093 S: MemoryStore + 'static,
1094{
1095 let backend = store.as_ref().backend_kind().to_string();
1096 serve_http_with_runtime(
1097 addr,
1098 store,
1099 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
1100 )
1101 .await
1102}
1103
1104pub async fn serve_http_with_runtime<S>(
1105 addr: SocketAddr,
1106 store: Arc<S>,
1107 runtime: ServerRuntime,
1108) -> std::io::Result<()>
1109where
1110 S: MemoryStore + 'static,
1111{
1112 let listener = tokio::net::TcpListener::bind(addr).await?;
1113 axum::serve(listener, http_app_with_runtime(store, runtime)).await
1114}
1115
1116async fn healthz(metrics: Arc<ServerMetrics>) -> Json<HealthStatus> {
1117 metrics.http_healthz.fetch_add(1, Ordering::Relaxed);
1118 Json(HealthStatus { status: "ok" })
1119}
1120
1121async fn readyz<S>(
1122 store: Arc<S>,
1123 runtime: ServerRuntime,
1124) -> Result<Json<ReadyStatus>, (StatusCode, Json<HttpErrorBody>)>
1125where
1126 S: MemoryStore + 'static,
1127{
1128 runtime
1129 .metrics()
1130 .http_readyz
1131 .fetch_add(1, Ordering::Relaxed);
1132 let manifest = store.snapshot().await.map_err(map_http_store_error)?;
1133 Ok(Json(ReadyStatus {
1134 status: "ready",
1135 record_count: manifest.record_count,
1136 namespaces: manifest.namespaces,
1137 }))
1138}
1139
1140async fn upsert_http<S>(
1141 store: Arc<S>,
1142 request: UpsertRequest,
1143 headers: HeaderMap,
1144 runtime: ServerRuntime,
1145) -> Result<Json<UpsertReceipt>, (StatusCode, Json<HttpErrorBody>)>
1146where
1147 S: MemoryStore + 'static,
1148{
1149 runtime
1150 .metrics()
1151 .http_maintenance
1152 .fetch_add(1, Ordering::Relaxed);
1153 let started_at_unix_ms = now_unix_ms();
1154 let correlation_id = runtime.traces().next_id("corr");
1155 validate_record_limits(&request.record, runtime.limits().as_ref())
1156 .map_err(map_http_validation_error)?;
1157 let _permit = runtime
1158 .admission()
1159 .acquire(AdmissionClass::Write, Some(&request.record.scope.tenant_id))
1160 .await
1161 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1162 let receipt = store
1163 .upsert(request.clone())
1164 .await
1165 .map_err(map_http_store_error)?;
1166 record_trace(
1167 &runtime,
1168 TraceOperationKind::Upsert,
1169 "http",
1170 Some(request.record.scope.tenant_id),
1171 Some(request.record.scope.namespace),
1172 http_principal(&headers),
1173 started_at_unix_ms,
1174 TraceStatus::Ok,
1175 None,
1176 OperationTraceSummary {
1177 record_id: Some(request.record.id),
1178 ..OperationTraceSummary::default()
1179 },
1180 None,
1181 correlation_id,
1182 );
1183 Ok(Json(receipt))
1184}
1185
1186async fn batch_upsert_http<S>(
1187 store: Arc<S>,
1188 request: BatchUpsertRequest,
1189 headers: HeaderMap,
1190 runtime: ServerRuntime,
1191) -> Result<Json<Vec<UpsertReceipt>>, (StatusCode, Json<HttpErrorBody>)>
1192where
1193 S: MemoryStore + 'static,
1194{
1195 runtime
1196 .metrics()
1197 .http_snapshot_ship
1198 .fetch_add(1, Ordering::Relaxed);
1199 let started_at_unix_ms = now_unix_ms();
1200 let correlation_id = runtime.traces().next_id("corr");
1201 let tenant_id = request
1202 .requests
1203 .first()
1204 .map(|item| item.record.scope.tenant_id.clone());
1205 let namespace = request
1206 .requests
1207 .first()
1208 .map(|item| item.record.scope.namespace.clone());
1209 if request.requests.len() > runtime.limits().max_batch_upsert_requests {
1210 return Err((
1211 StatusCode::BAD_REQUEST,
1212 Json(HttpErrorBody {
1213 error: format!(
1214 "batch upsert request count exceeds configured max of {}",
1215 runtime.limits().max_batch_upsert_requests
1216 ),
1217 }),
1218 ));
1219 }
1220 for item in &request.requests {
1221 validate_record_limits(&item.record, runtime.limits().as_ref())
1222 .map_err(map_http_validation_error)?;
1223 }
1224 let _permit = runtime
1225 .admission()
1226 .acquire(AdmissionClass::Write, tenant_id.as_deref())
1227 .await
1228 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1229 let receipts = store
1230 .batch_upsert(request.clone())
1231 .await
1232 .map_err(map_http_store_error)?;
1233 record_trace(
1234 &runtime,
1235 TraceOperationKind::BatchUpsert,
1236 "http",
1237 tenant_id,
1238 namespace,
1239 http_principal(&headers),
1240 started_at_unix_ms,
1241 TraceStatus::Ok,
1242 None,
1243 OperationTraceSummary {
1244 request_count: Some(request.requests.len() as u32),
1245 ..OperationTraceSummary::default()
1246 },
1247 None,
1248 correlation_id,
1249 );
1250 Ok(Json(receipts))
1251}
1252
1253async fn recall_http<S>(
1254 store: Arc<S>,
1255 request: RecallQuery,
1256 headers: HeaderMap,
1257 runtime: ServerRuntime,
1258) -> Result<Json<RecallResult>, (StatusCode, Json<HttpErrorBody>)>
1259where
1260 S: MemoryStore + 'static,
1261{
1262 let started_at_unix_ms = now_unix_ms();
1263 let correlation_id = runtime.traces().next_id("corr");
1264 validate_recall_limits(&request, runtime.limits().as_ref())
1265 .map_err(map_http_validation_error)?;
1266 let _permit = runtime
1267 .admission()
1268 .acquire(AdmissionClass::Read, Some(&request.scope.tenant_id))
1269 .await
1270 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1271 let mut result = store
1272 .recall(request.clone())
1273 .await
1274 .map_err(map_http_store_error)?;
1275 attach_correlation_id(&mut result, &correlation_id);
1276 record_trace(
1277 &runtime,
1278 TraceOperationKind::Recall,
1279 "http",
1280 Some(request.scope.tenant_id),
1281 Some(request.scope.namespace),
1282 http_principal(&headers),
1283 started_at_unix_ms,
1284 TraceStatus::Ok,
1285 None,
1286 OperationTraceSummary {
1287 query_text: Some(request.query_text),
1288 max_items: Some(request.max_items as u32),
1289 token_budget: request.token_budget.map(|value| value as u32),
1290 ..OperationTraceSummary::default()
1291 },
1292 result.explanation.clone(),
1293 correlation_id,
1294 );
1295 Ok(Json(result))
1296}
1297
1298async fn recall_as_of_http<S>(
1299 store: Arc<S>,
1300 request: TimeTravelRecallRequest,
1301 headers: HeaderMap,
1302 runtime: ServerRuntime,
1303) -> Result<Json<RecallResult>, (StatusCode, Json<HttpErrorBody>)>
1304where
1305 S: MemoryStore + 'static,
1306{
1307 let started_at_unix_ms = now_unix_ms();
1308 let correlation_id = runtime.traces().next_id("corr");
1309 validate_recall_limits(&request.query, runtime.limits().as_ref())
1310 .map_err(map_http_validation_error)?;
1311 let tenant_id = request.query.scope.tenant_id.clone();
1312 let namespace = request.query.scope.namespace.clone();
1313 let query_text = request.query.query_text.clone();
1314 let max_items = request.query.max_items as u32;
1315 let token_budget = request.query.token_budget.map(|value| value as u32);
1316 let _permit = runtime
1317 .admission()
1318 .acquire(AdmissionClass::Read, Some(&tenant_id))
1319 .await
1320 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1321 let mut result = store
1322 .recall_as_of(request)
1323 .await
1324 .map_err(map_http_store_error)?;
1325 attach_correlation_id(&mut result, &correlation_id);
1326 record_trace(
1327 &runtime,
1328 TraceOperationKind::Recall,
1329 "http",
1330 Some(tenant_id),
1331 Some(namespace),
1332 http_principal(&headers),
1333 started_at_unix_ms,
1334 TraceStatus::Ok,
1335 None,
1336 OperationTraceSummary {
1337 query_text: Some(query_text),
1338 max_items: Some(max_items),
1339 token_budget,
1340 ..OperationTraceSummary::default()
1341 },
1342 result.explanation.clone(),
1343 correlation_id,
1344 );
1345 Ok(Json(result))
1346}
1347
1348async fn snapshot_http<S>(
1349 store: Arc<S>,
1350 headers: HeaderMap,
1351 runtime: ServerRuntime,
1352) -> Result<Json<SnapshotManifest>, (StatusCode, Json<HttpErrorBody>)>
1353where
1354 S: MemoryStore + 'static,
1355{
1356 runtime
1357 .metrics()
1358 .http_snapshot
1359 .fetch_add(1, Ordering::Relaxed);
1360 let started_at_unix_ms = now_unix_ms();
1361 let correlation_id = runtime.traces().next_id("corr");
1362 let _permit = runtime
1363 .admission()
1364 .acquire(AdmissionClass::Admin, None)
1365 .await
1366 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1367 let manifest = store.snapshot().await.map_err(map_http_store_error)?;
1368 record_trace(
1369 &runtime,
1370 TraceOperationKind::Snapshot,
1371 "http",
1372 None,
1373 None,
1374 http_principal(&headers),
1375 started_at_unix_ms,
1376 TraceStatus::Ok,
1377 None,
1378 OperationTraceSummary::default(),
1379 None,
1380 correlation_id,
1381 );
1382 Ok(Json(manifest))
1383}
1384
1385async fn stats_http<S>(
1386 store: Arc<S>,
1387 request: StoreStatsRequest,
1388 headers: HeaderMap,
1389 runtime: ServerRuntime,
1390) -> Result<Json<StoreStatsReport>, (StatusCode, Json<HttpErrorBody>)>
1391where
1392 S: MemoryStore + 'static,
1393{
1394 let started_at_unix_ms = now_unix_ms();
1395 let correlation_id = runtime.traces().next_id("corr");
1396 let _permit = runtime
1397 .admission()
1398 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1399 .await
1400 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1401 let report = store
1402 .stats(request.clone())
1403 .await
1404 .map_err(map_http_store_error)?;
1405 record_trace(
1406 &runtime,
1407 TraceOperationKind::Stats,
1408 "http",
1409 request.tenant_id,
1410 request.namespace,
1411 http_principal(&headers),
1412 started_at_unix_ms,
1413 TraceStatus::Ok,
1414 None,
1415 OperationTraceSummary::default(),
1416 None,
1417 correlation_id,
1418 );
1419 Ok(Json(report))
1420}
1421
1422async fn graph_http<S>(
1423 store: Arc<S>,
1424 request: GraphInspectionRequest,
1425 headers: HeaderMap,
1426 runtime: ServerRuntime,
1427) -> Result<Json<GraphInspectionReport>, (StatusCode, Json<HttpErrorBody>)>
1428where
1429 S: MemoryStore + 'static,
1430{
1431 let started_at_unix_ms = now_unix_ms();
1432 let correlation_id = runtime.traces().next_id("corr");
1433 let _permit = runtime
1434 .admission()
1435 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1436 .await
1437 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1438 let report = store
1439 .inspect_graph(request.clone())
1440 .await
1441 .map_err(map_http_store_error)?;
1442 record_trace(
1443 &runtime,
1444 TraceOperationKind::Stats,
1445 "http",
1446 request.tenant_id,
1447 request.namespace,
1448 http_principal(&headers),
1449 started_at_unix_ms,
1450 TraceStatus::Ok,
1451 None,
1452 OperationTraceSummary {
1453 request_count: Some(report.nodes.len() as u32),
1454 max_items: Some(report.edges.len() as u32),
1455 ..Default::default()
1456 },
1457 None,
1458 correlation_id,
1459 );
1460 Ok(Json(report))
1461}
1462
1463async fn changefeed_http<S>(
1464 store: Arc<S>,
1465 request: ChangefeedRequest,
1466 headers: HeaderMap,
1467 runtime: ServerRuntime,
1468) -> Result<Json<ChangefeedReport>, (StatusCode, Json<HttpErrorBody>)>
1469where
1470 S: MemoryStore + 'static,
1471{
1472 let started_at_unix_ms = now_unix_ms();
1473 let correlation_id = runtime.traces().next_id("corr");
1474 let _permit = runtime
1475 .admission()
1476 .acquire(AdmissionClass::Read, request.tenant_id.as_deref())
1477 .await
1478 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1479 let report = store
1480 .changefeed(request.clone())
1481 .await
1482 .map_err(map_http_store_error)?;
1483 record_trace(
1484 &runtime,
1485 TraceOperationKind::Changefeed,
1486 "http",
1487 request.tenant_id,
1488 request.namespace,
1489 http_principal(&headers),
1490 started_at_unix_ms,
1491 TraceStatus::Ok,
1492 None,
1493 OperationTraceSummary {
1494 request_count: Some(report.events.len() as u32),
1495 ..OperationTraceSummary::default()
1496 },
1497 None,
1498 correlation_id,
1499 );
1500 Ok(Json(report))
1501}
1502
1503async fn integrity_http<S>(
1504 store: Arc<S>,
1505 request: IntegrityCheckRequest,
1506 headers: HeaderMap,
1507 runtime: ServerRuntime,
1508) -> Result<Json<IntegrityCheckReport>, (StatusCode, Json<HttpErrorBody>)>
1509where
1510 S: MemoryStore + 'static,
1511{
1512 let started_at_unix_ms = now_unix_ms();
1513 let correlation_id = runtime.traces().next_id("corr");
1514 let _permit = runtime
1515 .admission()
1516 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1517 .await
1518 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1519 let report = store
1520 .integrity_check(request.clone())
1521 .await
1522 .map_err(map_http_store_error)?;
1523 record_trace(
1524 &runtime,
1525 TraceOperationKind::IntegrityCheck,
1526 "http",
1527 request.tenant_id,
1528 request.namespace,
1529 http_principal(&headers),
1530 started_at_unix_ms,
1531 TraceStatus::Ok,
1532 None,
1533 OperationTraceSummary::default(),
1534 None,
1535 correlation_id,
1536 );
1537 Ok(Json(report))
1538}
1539
1540async fn repair_http<S>(
1541 store: Arc<S>,
1542 request: RepairRequest,
1543 headers: HeaderMap,
1544 runtime: ServerRuntime,
1545) -> Result<Json<RepairReport>, (StatusCode, Json<HttpErrorBody>)>
1546where
1547 S: MemoryStore + 'static,
1548{
1549 let started_at_unix_ms = now_unix_ms();
1550 let correlation_id = runtime.traces().next_id("corr");
1551 let _permit = runtime
1552 .admission()
1553 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1554 .await
1555 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1556 let report = store
1557 .repair(request.clone())
1558 .await
1559 .map_err(map_http_store_error)?;
1560 record_trace(
1561 &runtime,
1562 TraceOperationKind::Repair,
1563 "http",
1564 request.tenant_id,
1565 request.namespace,
1566 http_principal(&headers),
1567 started_at_unix_ms,
1568 TraceStatus::Ok,
1569 None,
1570 OperationTraceSummary {
1571 dry_run: Some(request.dry_run),
1572 ..OperationTraceSummary::default()
1573 },
1574 None,
1575 correlation_id,
1576 );
1577 Ok(Json(report))
1578}
1579
1580async fn compact_http<S>(
1581 store: Arc<S>,
1582 request: CompactionRequest,
1583 headers: HeaderMap,
1584 runtime: ServerRuntime,
1585) -> Result<Json<CompactionReport>, (StatusCode, Json<HttpErrorBody>)>
1586where
1587 S: MemoryStore + 'static,
1588{
1589 runtime
1590 .metrics()
1591 .http_compact
1592 .fetch_add(1, Ordering::Relaxed);
1593 let started_at_unix_ms = now_unix_ms();
1594 let correlation_id = runtime.traces().next_id("corr");
1595 let _permit = runtime
1596 .admission()
1597 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1598 .await
1599 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1600 let report = store
1601 .compact(request.clone())
1602 .await
1603 .map_err(map_http_store_error)?;
1604 record_trace(
1605 &runtime,
1606 TraceOperationKind::Compact,
1607 "http",
1608 Some(request.tenant_id),
1609 request.namespace,
1610 http_principal(&headers),
1611 started_at_unix_ms,
1612 TraceStatus::Ok,
1613 None,
1614 OperationTraceSummary {
1615 dry_run: Some(request.dry_run),
1616 ..OperationTraceSummary::default()
1617 },
1618 None,
1619 correlation_id,
1620 );
1621 Ok(Json(report))
1622}
1623
1624async fn synthesize_http<S>(
1625 store: Arc<S>,
1626 request: SynthesisRequest,
1627 headers: HeaderMap,
1628 runtime: ServerRuntime,
1629) -> Result<Json<SynthesisReport>, (StatusCode, Json<HttpErrorBody>)>
1630where
1631 S: MemoryStore + 'static,
1632{
1633 runtime
1634 .metrics()
1635 .http_synthesis
1636 .fetch_add(1, Ordering::Relaxed);
1637 let started_at_unix_ms = now_unix_ms();
1638 let correlation_id = runtime.traces().next_id("corr");
1639 let _permit = runtime
1640 .admission()
1641 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1642 .await
1643 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1644 let report = store
1645 .synthesize(request.clone())
1646 .await
1647 .map_err(map_http_store_error)?;
1648 record_trace(
1649 &runtime,
1650 TraceOperationKind::Synthesis,
1651 "http",
1652 Some(request.tenant_id),
1653 request.namespace,
1654 http_principal(&headers),
1655 started_at_unix_ms,
1656 TraceStatus::Ok,
1657 None,
1658 OperationTraceSummary {
1659 request_count: Some(report.proposed_records as u32),
1660 dry_run: Some(request.dry_run),
1661 ..OperationTraceSummary::default()
1662 },
1663 None,
1664 correlation_id,
1665 );
1666 Ok(Json(report))
1667}
1668
1669async fn delete_http<S>(
1670 store: Arc<S>,
1671 request: DeleteHttpRequest,
1672 headers: HeaderMap,
1673 runtime: ServerRuntime,
1674) -> Result<Json<DeleteReceipt>, (StatusCode, Json<HttpErrorBody>)>
1675where
1676 S: MemoryStore + 'static,
1677{
1678 runtime
1679 .metrics()
1680 .http_delete
1681 .fetch_add(1, Ordering::Relaxed);
1682 let started_at_unix_ms = now_unix_ms();
1683 let correlation_id = runtime.traces().next_id("corr");
1684 let _permit = runtime
1685 .admission()
1686 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1687 .await
1688 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1689 let receipt = store
1690 .delete(DeleteRequest {
1691 tenant_id: request.tenant_id.clone(),
1692 namespace: request.namespace.clone(),
1693 record_id: request.record_id,
1694 hard_delete: request.hard_delete,
1695 audit_reason: request.audit_reason,
1696 })
1697 .await
1698 .map_err(map_http_store_error)?;
1699 record_trace(
1700 &runtime,
1701 TraceOperationKind::Delete,
1702 "http",
1703 Some(request.tenant_id),
1704 Some(request.namespace),
1705 http_principal(&headers),
1706 started_at_unix_ms,
1707 TraceStatus::Ok,
1708 None,
1709 OperationTraceSummary {
1710 record_id: Some(receipt.record_id.clone()),
1711 ..OperationTraceSummary::default()
1712 },
1713 None,
1714 correlation_id,
1715 );
1716 Ok(Json(receipt))
1717}
1718
1719async fn archive_http<S>(
1720 store: Arc<S>,
1721 request: ArchiveHttpRequest,
1722 headers: HeaderMap,
1723 runtime: ServerRuntime,
1724) -> Result<Json<ArchiveReceipt>, (StatusCode, Json<HttpErrorBody>)>
1725where
1726 S: MemoryStore + 'static,
1727{
1728 runtime
1729 .metrics()
1730 .http_archive
1731 .fetch_add(1, Ordering::Relaxed);
1732 let started_at_unix_ms = now_unix_ms();
1733 let correlation_id = runtime.traces().next_id("corr");
1734 let _permit = runtime
1735 .admission()
1736 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1737 .await
1738 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1739 let receipt = store
1740 .archive(ArchiveRequest {
1741 tenant_id: request.tenant_id.clone(),
1742 namespace: request.namespace.clone(),
1743 record_id: request.record_id,
1744 dry_run: request.dry_run,
1745 audit_reason: request.audit_reason,
1746 })
1747 .await
1748 .map_err(map_http_store_error)?;
1749 record_trace(
1750 &runtime,
1751 TraceOperationKind::Archive,
1752 "http",
1753 Some(request.tenant_id),
1754 Some(request.namespace),
1755 http_principal(&headers),
1756 started_at_unix_ms,
1757 TraceStatus::Ok,
1758 None,
1759 OperationTraceSummary {
1760 record_id: Some(receipt.record_id.clone()),
1761 dry_run: Some(receipt.dry_run),
1762 ..OperationTraceSummary::default()
1763 },
1764 None,
1765 correlation_id,
1766 );
1767 Ok(Json(receipt))
1768}
1769
1770async fn suppress_http<S>(
1771 store: Arc<S>,
1772 request: SuppressHttpRequest,
1773 headers: HeaderMap,
1774 runtime: ServerRuntime,
1775) -> Result<Json<SuppressReceipt>, (StatusCode, Json<HttpErrorBody>)>
1776where
1777 S: MemoryStore + 'static,
1778{
1779 runtime
1780 .metrics()
1781 .http_suppress
1782 .fetch_add(1, Ordering::Relaxed);
1783 let started_at_unix_ms = now_unix_ms();
1784 let correlation_id = runtime.traces().next_id("corr");
1785 let _permit = runtime
1786 .admission()
1787 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1788 .await
1789 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1790 let receipt = store
1791 .suppress(SuppressRequest {
1792 tenant_id: request.tenant_id.clone(),
1793 namespace: request.namespace.clone(),
1794 record_id: request.record_id,
1795 dry_run: request.dry_run,
1796 audit_reason: request.audit_reason,
1797 })
1798 .await
1799 .map_err(map_http_store_error)?;
1800 record_trace(
1801 &runtime,
1802 TraceOperationKind::Suppress,
1803 "http",
1804 Some(request.tenant_id),
1805 Some(request.namespace),
1806 http_principal(&headers),
1807 started_at_unix_ms,
1808 TraceStatus::Ok,
1809 None,
1810 OperationTraceSummary {
1811 record_id: Some(receipt.record_id.clone()),
1812 dry_run: Some(receipt.dry_run),
1813 ..OperationTraceSummary::default()
1814 },
1815 None,
1816 correlation_id,
1817 );
1818 Ok(Json(receipt))
1819}
1820
1821async fn recover_http<S>(
1822 store: Arc<S>,
1823 request: RecoverHttpRequest,
1824 headers: HeaderMap,
1825 runtime: ServerRuntime,
1826) -> Result<Json<RecoverReceipt>, (StatusCode, Json<HttpErrorBody>)>
1827where
1828 S: MemoryStore + 'static,
1829{
1830 runtime
1831 .metrics()
1832 .http_recover
1833 .fetch_add(1, Ordering::Relaxed);
1834 let started_at_unix_ms = now_unix_ms();
1835 let correlation_id = runtime.traces().next_id("corr");
1836 let _permit = runtime
1837 .admission()
1838 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1839 .await
1840 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1841 let receipt = store
1842 .recover(RecoverRequest {
1843 tenant_id: request.tenant_id.clone(),
1844 namespace: request.namespace.clone(),
1845 record_id: request.record_id,
1846 dry_run: request.dry_run,
1847 audit_reason: request.audit_reason,
1848 quality_state: request.quality_state,
1849 historical_state: request.historical_state,
1850 })
1851 .await
1852 .map_err(map_http_store_error)?;
1853 record_trace(
1854 &runtime,
1855 TraceOperationKind::Recover,
1856 "http",
1857 Some(request.tenant_id),
1858 Some(request.namespace),
1859 http_principal(&headers),
1860 started_at_unix_ms,
1861 TraceStatus::Ok,
1862 None,
1863 OperationTraceSummary {
1864 record_id: Some(receipt.record_id.clone()),
1865 dry_run: Some(receipt.dry_run),
1866 ..OperationTraceSummary::default()
1867 },
1868 None,
1869 correlation_id,
1870 );
1871 Ok(Json(receipt))
1872}
1873
1874async fn traces_http(
1875 request: TraceListRequest,
1876 runtime: ServerRuntime,
1877) -> Result<Json<Vec<OperationTrace>>, (StatusCode, Json<HttpErrorBody>)> {
1878 runtime
1879 .metrics()
1880 .http_traces
1881 .fetch_add(1, Ordering::Relaxed);
1882 runtime
1883 .metrics()
1884 .trace_reads
1885 .fetch_add(1, Ordering::Relaxed);
1886 Ok(Json(runtime.traces().list(&request)))
1887}
1888
1889async fn trace_http(
1890 trace_id: String,
1891 runtime: ServerRuntime,
1892) -> Result<Json<OperationTrace>, (StatusCode, Json<HttpErrorBody>)> {
1893 runtime
1894 .metrics()
1895 .http_traces
1896 .fetch_add(1, Ordering::Relaxed);
1897 runtime
1898 .metrics()
1899 .trace_reads
1900 .fetch_add(1, Ordering::Relaxed);
1901 runtime.traces().get(&trace_id).map(Json).ok_or_else(|| {
1902 (
1903 StatusCode::NOT_FOUND,
1904 Json(HttpErrorBody {
1905 error: format!("trace {trace_id} not found"),
1906 }),
1907 )
1908 })
1909}
1910
1911async fn runtime_status_http(runtime: ServerRuntime) -> Json<ServerRuntimeStatus> {
1912 runtime
1913 .metrics()
1914 .http_runtime
1915 .fetch_add(1, Ordering::Relaxed);
1916 Json(ServerRuntimeStatus {
1917 backend: runtime.backend().as_ref().clone(),
1918 admission: runtime.admission().snapshot(),
1919 traces: runtime.traces().snapshot(),
1920 })
1921}
1922
1923async fn maintenance_run_http<S>(
1924 store: Arc<S>,
1925 request: MaintenanceRunRequest,
1926 headers: HeaderMap,
1927 runtime: ServerRuntime,
1928) -> Result<Json<MaintenanceRunReport>, (StatusCode, Json<HttpErrorBody>)>
1929where
1930 S: MemoryStore + 'static,
1931{
1932 let started_at_unix_ms = now_unix_ms();
1933 let correlation_id = runtime.traces().next_id("corr");
1934 let _permit = runtime
1935 .admission()
1936 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1937 .await
1938 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1939 let report = store
1940 .run_maintenance(request.clone())
1941 .await
1942 .map_err(map_http_store_error)?;
1943 record_trace(
1944 &runtime,
1945 TraceOperationKind::MaintenanceRun,
1946 "http",
1947 request.tenant_id,
1948 request.namespace,
1949 http_principal(&headers),
1950 started_at_unix_ms,
1951 TraceStatus::Ok,
1952 None,
1953 OperationTraceSummary {
1954 dry_run: Some(report.dry_run),
1955 ..Default::default()
1956 },
1957 None,
1958 correlation_id,
1959 );
1960 Ok(Json(report))
1961}
1962
1963async fn export_http<S>(
1964 store: Arc<S>,
1965 request: ExportRequest,
1966 headers: HeaderMap,
1967 runtime: ServerRuntime,
1968) -> Result<Json<PortableStorePackage>, (StatusCode, Json<HttpErrorBody>)>
1969where
1970 S: MemoryStore + 'static,
1971{
1972 runtime
1973 .metrics()
1974 .http_export
1975 .fetch_add(1, Ordering::Relaxed);
1976 let started_at_unix_ms = now_unix_ms();
1977 let correlation_id = runtime.traces().next_id("corr");
1978 let _permit = runtime
1979 .admission()
1980 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1981 .await
1982 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1983 let package = store
1984 .export(request.clone())
1985 .await
1986 .map_err(map_http_store_error)?;
1987 record_trace(
1988 &runtime,
1989 TraceOperationKind::Export,
1990 "http",
1991 request.tenant_id,
1992 request.namespace,
1993 http_principal(&headers),
1994 started_at_unix_ms,
1995 TraceStatus::Ok,
1996 None,
1997 OperationTraceSummary::default(),
1998 None,
1999 correlation_id,
2000 );
2001 Ok(Json(package))
2002}
2003
2004async fn snapshot_ship_http<S>(
2005 store: Arc<S>,
2006 request: SnapshotShipRequest,
2007 headers: HeaderMap,
2008 runtime: ServerRuntime,
2009) -> Result<Json<SnapshotShipReport>, (StatusCode, Json<HttpErrorBody>)>
2010where
2011 S: MemoryStore + 'static,
2012{
2013 let started_at_unix_ms = now_unix_ms();
2014 let correlation_id = runtime.traces().next_id("corr");
2015 let _permit = runtime
2016 .admission()
2017 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
2018 .await
2019 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
2020
2021 let package = store
2022 .export(ExportRequest {
2023 tenant_id: request.tenant_id.clone(),
2024 namespace: request.namespace.clone(),
2025 include_archived: request.include_archived,
2026 })
2027 .await
2028 .map_err(map_http_store_error)?;
2029 let exported_records = package.records.len() as u64;
2030 let import_request = ImportRequest {
2031 package,
2032 mode: request.mode,
2033 dry_run: request.dry_run,
2034 };
2035 let (remote_status, remote_report) = post_remote_import(
2036 &request.target_url,
2037 request.bearer_token.as_deref(),
2038 &import_request,
2039 )
2040 .await?;
2041 let report = SnapshotShipReport {
2042 target_url: request.target_url.clone(),
2043 exported_records,
2044 imported_records: remote_report.imported_records,
2045 skipped_records: remote_report.skipped_records,
2046 dry_run: request.dry_run,
2047 compatible_package: remote_report.compatible_package,
2048 remote_status,
2049 remote_snapshot_id: Some(remote_report.snapshot_id),
2050 };
2051 record_trace(
2052 &runtime,
2053 TraceOperationKind::SnapshotShip,
2054 "http",
2055 request.tenant_id,
2056 request.namespace,
2057 http_principal(&headers),
2058 started_at_unix_ms,
2059 TraceStatus::Ok,
2060 None,
2061 OperationTraceSummary {
2062 request_count: Some(exported_records as u32),
2063 dry_run: Some(request.dry_run),
2064 ..Default::default()
2065 },
2066 None,
2067 correlation_id,
2068 );
2069 Ok(Json(report))
2070}
2071
2072async fn import_http<S>(
2073 store: Arc<S>,
2074 request: ImportRequest,
2075 headers: HeaderMap,
2076 runtime: ServerRuntime,
2077) -> Result<Json<ImportReport>, (StatusCode, Json<HttpErrorBody>)>
2078where
2079 S: MemoryStore + 'static,
2080{
2081 runtime
2082 .metrics()
2083 .http_import
2084 .fetch_add(1, Ordering::Relaxed);
2085 let started_at_unix_ms = now_unix_ms();
2086 let correlation_id = runtime.traces().next_id("corr");
2087 let tenant_id = request
2088 .package
2089 .records
2090 .first()
2091 .map(|entry| entry.record.scope.tenant_id.clone());
2092 let namespace = request
2093 .package
2094 .records
2095 .first()
2096 .map(|entry| entry.record.scope.namespace.clone());
2097 let _permit = runtime
2098 .admission()
2099 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
2100 .await
2101 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
2102 let report = store
2103 .import(request.clone())
2104 .await
2105 .map_err(map_http_store_error)?;
2106 record_trace(
2107 &runtime,
2108 TraceOperationKind::Import,
2109 "http",
2110 tenant_id,
2111 namespace,
2112 http_principal(&headers),
2113 started_at_unix_ms,
2114 if report.failed_records.is_empty() {
2115 TraceStatus::Ok
2116 } else {
2117 TraceStatus::Error
2118 },
2119 (!report.failed_records.is_empty())
2120 .then(|| format!("{} import validation failures", report.failed_records.len())),
2121 OperationTraceSummary {
2122 request_count: Some(request.package.records.len() as u32),
2123 dry_run: Some(request.dry_run),
2124 ..OperationTraceSummary::default()
2125 },
2126 None,
2127 correlation_id,
2128 );
2129 Ok(Json(report))
2130}
2131
2132#[derive(Debug, Clone)]
2133struct HttpTarget {
2134 host: String,
2135 port: u16,
2136 path: String,
2137}
2138
2139fn parse_http_target(raw: &str) -> Result<HttpTarget, String> {
2140 let without_scheme = raw
2141 .strip_prefix("http://")
2142 .ok_or_else(|| "snapshot shipping currently supports http:// targets".to_string())?;
2143 let (authority, path) = without_scheme
2144 .split_once('/')
2145 .map(|(authority, path)| (authority, format!("/{path}")))
2146 .unwrap_or((without_scheme, "/admin/import".to_string()));
2147 let path = if path.ends_with("/admin/import") {
2148 path
2149 } else {
2150 format!("{}/admin/import", path.trim_end_matches('/'))
2151 };
2152 let (host, port) = authority
2153 .rsplit_once(':')
2154 .map(|(host, port)| {
2155 port.parse::<u16>()
2156 .map(|port| (host.to_string(), port))
2157 .map_err(|err| format!("invalid snapshot shipping target port '{port}': {err}"))
2158 })
2159 .unwrap_or_else(|| Ok((authority.to_string(), 80)))?;
2160 if host.trim().is_empty() {
2161 return Err("snapshot shipping target host is required".to_string());
2162 }
2163 Ok(HttpTarget { host, port, path })
2164}
2165
2166async fn post_remote_import(
2167 target_url: &str,
2168 bearer_token: Option<&str>,
2169 request: &ImportRequest,
2170) -> Result<(u16, ImportReport), (StatusCode, Json<HttpErrorBody>)> {
2171 post_remote_import_raw(target_url, bearer_token, request)
2172 .await
2173 .map_err(|error| (StatusCode::BAD_GATEWAY, Json(HttpErrorBody { error })))
2174}
2175
2176async fn post_remote_import_raw(
2177 target_url: &str,
2178 bearer_token: Option<&str>,
2179 request: &ImportRequest,
2180) -> Result<(u16, ImportReport), String> {
2181 let target = parse_http_target(target_url)
2182 .map_err(|error| format!("invalid snapshot shipping target: {error}"))?;
2183 let body = serde_json::to_string(request)
2184 .map_err(|err| format!("failed to encode snapshot shipment: {err}"))?;
2185 let bearer_token = bearer_token.map(str::to_string);
2186 tokio::task::spawn_blocking(move || {
2187 use std::io::{Read, Write};
2188 use std::net::TcpStream;
2189
2190 let mut stream = TcpStream::connect((target.host.as_str(), target.port))
2191 .map_err(|err| format!("failed to connect to snapshot target: {err}"))?;
2192 let mut request_head = format!(
2193 "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
2194 target.path,
2195 target.host,
2196 body.len()
2197 );
2198 if let Some(token) = bearer_token {
2199 request_head.push_str(&format!("Authorization: Bearer {token}\r\n"));
2200 }
2201 request_head.push_str("\r\n");
2202 stream
2203 .write_all(request_head.as_bytes())
2204 .and_then(|_| stream.write_all(body.as_bytes()))
2205 .map_err(|err| format!("failed to write snapshot shipment: {err}"))?;
2206 let mut response = String::new();
2207 stream
2208 .read_to_string(&mut response)
2209 .map_err(|err| format!("failed to read snapshot shipment response: {err}"))?;
2210 let (head, body) = response
2211 .split_once("\r\n\r\n")
2212 .ok_or_else(|| "snapshot target returned an invalid HTTP response".to_string())?;
2213 let status = head
2214 .lines()
2215 .next()
2216 .and_then(|line| line.split_whitespace().nth(1))
2217 .and_then(|value| value.parse::<u16>().ok())
2218 .ok_or_else(|| "snapshot target returned an invalid HTTP status".to_string())?;
2219 if !(200..300).contains(&status) {
2220 return Err(format!("snapshot target returned HTTP {status}: {body}"));
2221 }
2222 serde_json::from_str::<ImportReport>(body)
2223 .map(|report| (status, report))
2224 .map_err(|err| format!("failed to decode snapshot target import report: {err}"))
2225 })
2226 .await
2227 .map_err(|err| format!("snapshot shipping worker failed: {err}"))?
2228}
2229
2230async fn metrics_http(metrics: Arc<ServerMetrics>) -> impl IntoResponse {
2231 metrics.http_metrics.fetch_add(1, Ordering::Relaxed);
2232 (
2233 [(
2234 axum::http::header::CONTENT_TYPE,
2235 "text/plain; version=0.0.4",
2236 )],
2237 metrics.render(),
2238 )
2239}
2240
2241async fn enforce_http_guardrails(
2242 request: AxumRequest,
2243 next: Next,
2244 limits: Arc<ServerLimits>,
2245 metrics: Arc<ServerMetrics>,
2246 auth: Arc<AuthConfig>,
2247) -> Result<axum::response::Response, StatusCode> {
2248 let path = request.uri().path().to_string();
2249 if let Some(content_length) = request.headers().get(axum::http::header::CONTENT_LENGTH)
2250 && let Ok(content_length) = content_length.to_str()
2251 && let Ok(content_length) = content_length.parse::<usize>()
2252 && content_length > limits.max_http_body_bytes
2253 {
2254 metrics
2255 .http_rejected_body_too_large
2256 .fetch_add(1, Ordering::Relaxed);
2257 return Err(StatusCode::PAYLOAD_TOO_LARGE);
2258 }
2259 validate_http_auth(&request, &path, auth.as_ref())?;
2260 Ok(next.run(request).await)
2261}
2262
2263fn http_principal(headers: &HeaderMap) -> Option<String> {
2264 headers
2265 .get(axum::http::header::AUTHORIZATION)
2266 .and_then(|value| value.to_str().ok())
2267 .map(|_| "bearer".to_string())
2268}
2269
2270fn grpc_principal<T>(request: &Request<T>) -> Option<String> {
2271 request
2272 .metadata()
2273 .get("authorization")
2274 .and_then(|value| value.to_str().ok())
2275 .map(|_| "bearer".to_string())
2276}
2277
2278fn map_http_admission_error(
2279 metrics: &ServerMetrics,
2280 error: AdmissionError,
2281) -> (StatusCode, Json<HttpErrorBody>) {
2282 match error {
2283 AdmissionError::QueueFull | AdmissionError::TenantBusy => {
2284 metrics.admission_rejected.fetch_add(1, Ordering::Relaxed);
2285 (
2286 StatusCode::TOO_MANY_REQUESTS,
2287 Json(HttpErrorBody {
2288 error: "request admission rejected".to_string(),
2289 }),
2290 )
2291 }
2292 AdmissionError::TimedOut => {
2293 metrics.admission_timed_out.fetch_add(1, Ordering::Relaxed);
2294 (
2295 StatusCode::TOO_MANY_REQUESTS,
2296 Json(HttpErrorBody {
2297 error: "request admission timed out".to_string(),
2298 }),
2299 )
2300 }
2301 }
2302}
2303
2304fn map_grpc_admission_error(metrics: &ServerMetrics, error: AdmissionError) -> Status {
2305 match error {
2306 AdmissionError::QueueFull | AdmissionError::TenantBusy => {
2307 metrics.admission_rejected.fetch_add(1, Ordering::Relaxed);
2308 Status::resource_exhausted("request admission rejected")
2309 }
2310 AdmissionError::TimedOut => {
2311 metrics.admission_timed_out.fetch_add(1, Ordering::Relaxed);
2312 Status::resource_exhausted("request admission timed out")
2313 }
2314 }
2315}
2316
2317fn attach_correlation_id(result: &mut RecallResult, correlation_id: &str) {
2318 if let Some(explanation) = result.explanation.as_mut() {
2319 if let Some(existing) = explanation.trace_id.replace(correlation_id.to_string()) {
2320 explanation
2321 .policy_notes
2322 .push(format!("planning_trace_id={existing}"));
2323 }
2324 explanation
2325 .policy_notes
2326 .push(format!("correlation_id={correlation_id}"));
2327 }
2328 for hit in &mut result.hits {
2329 if let Some(explanation) = hit.explanation.as_mut() {
2330 if let Some(existing) = explanation.trace_id.replace(correlation_id.to_string()) {
2331 explanation
2332 .policy_notes
2333 .push(format!("planning_trace_id={existing}"));
2334 }
2335 explanation
2336 .policy_notes
2337 .push(format!("correlation_id={correlation_id}"));
2338 }
2339 }
2340}
2341
2342#[allow(clippy::too_many_arguments)]
2343fn record_trace(
2344 runtime: &ServerRuntime,
2345 operation: TraceOperationKind,
2346 transport: &str,
2347 tenant_id: Option<String>,
2348 namespace: Option<String>,
2349 principal: Option<String>,
2350 started_at_unix_ms: u64,
2351 status: TraceStatus,
2352 status_message: Option<String>,
2353 summary: OperationTraceSummary,
2354 recall_explanation: Option<RecallExplanation>,
2355 correlation_id: String,
2356) {
2357 let completed_at_unix_ms = now_unix_ms();
2358 let admission_class = admission_class_for_operation(operation.clone()).to_string();
2359 let planning_trace_id = recall_explanation.as_ref().and_then(|value| {
2360 value
2361 .planning_trace
2362 .as_ref()
2363 .map(|trace| trace.trace_id.clone())
2364 });
2365 let store_span_id = runtime.traces().next_id("store-span");
2366 let evicted = runtime.traces().record(OperationTrace {
2367 trace_id: runtime.traces().next_id("trace"),
2368 correlation_id,
2369 operation,
2370 transport: transport.to_string(),
2371 backend: Some(runtime.backend().as_ref().clone()),
2372 admission_class: Some(admission_class),
2373 tenant_id,
2374 namespace,
2375 principal,
2376 store_span_id: Some(store_span_id),
2377 planning_trace_id,
2378 started_at_unix_ms,
2379 completed_at_unix_ms,
2380 latency_ms: completed_at_unix_ms.saturating_sub(started_at_unix_ms),
2381 status,
2382 status_message,
2383 summary,
2384 recall_explanation,
2385 });
2386 runtime
2387 .metrics()
2388 .trace_records
2389 .fetch_add(1, Ordering::Relaxed);
2390 if evicted {
2391 runtime
2392 .metrics()
2393 .trace_evictions
2394 .fetch_add(1, Ordering::Relaxed);
2395 }
2396}
2397
2398fn admission_class_for_operation(operation: TraceOperationKind) -> &'static str {
2399 match operation {
2400 TraceOperationKind::Recall
2401 | TraceOperationKind::Snapshot
2402 | TraceOperationKind::Stats
2403 | TraceOperationKind::Changefeed => "read",
2404 TraceOperationKind::Upsert
2405 | TraceOperationKind::BatchUpsert
2406 | TraceOperationKind::Compact
2407 | TraceOperationKind::Synthesis
2408 | TraceOperationKind::Delete
2409 | TraceOperationKind::Archive
2410 | TraceOperationKind::Suppress
2411 | TraceOperationKind::Recover => "write",
2412 TraceOperationKind::IntegrityCheck
2413 | TraceOperationKind::Repair
2414 | TraceOperationKind::Export
2415 | TraceOperationKind::Import
2416 | TraceOperationKind::MaintenanceRun
2417 | TraceOperationKind::SnapshotShip => "admin",
2418 }
2419}
2420
2421fn validate_http_auth(
2422 request: &AxumRequest,
2423 path: &str,
2424 auth: &AuthConfig,
2425) -> Result<(), StatusCode> {
2426 if path == "/healthz" || path == "/readyz" || (path == "/metrics" && !auth.protect_metrics) {
2427 return Ok(());
2428 }
2429 let provided = request
2430 .headers()
2431 .get(axum::http::header::AUTHORIZATION)
2432 .and_then(|value| value.to_str().ok());
2433 let permission = match path {
2434 "/metrics" => AuthPermission::Metrics,
2435 "/memory/recall" | "/memory/recall-as-of" => AuthPermission::Read,
2436 "/memory/upsert" | "/memory/batch-upsert" => AuthPermission::Write,
2437 "/admin/stats"
2438 | "/admin/graph"
2439 | "/admin/changefeed"
2440 | "/admin/integrity"
2441 | "/admin/repair"
2442 | "/admin/snapshot"
2443 | "/admin/compact"
2444 | "/admin/synthesize"
2445 | "/admin/delete"
2446 | "/admin/archive"
2447 | "/admin/suppress"
2448 | "/admin/recover"
2449 | "/admin/maintenance/run"
2450 | "/admin/runtime"
2451 | "/admin/export"
2452 | "/admin/replication/ship"
2453 | "/admin/import" => AuthPermission::Admin,
2454 _ => AuthPermission::Admin,
2455 };
2456 if path.starts_with("/admin/traces") {
2457 return auth
2458 .authorize(provided, AuthPermission::Admin)
2459 .then_some(())
2460 .ok_or(StatusCode::UNAUTHORIZED);
2461 }
2462 if auth.authorize(provided, permission) {
2463 Ok(())
2464 } else {
2465 Err(StatusCode::UNAUTHORIZED)
2466 }
2467}
2468
2469fn validate_grpc_auth<T>(
2470 request: &Request<T>,
2471 auth: &AuthConfig,
2472 permission: AuthPermission,
2473) -> Result<(), Status> {
2474 let provided = request
2475 .metadata()
2476 .get("authorization")
2477 .and_then(|value| value.to_str().ok());
2478 if auth.authorize(provided, permission) {
2479 Ok(())
2480 } else {
2481 Err(Status::unauthenticated("missing or invalid bearer token"))
2482 }
2483}
2484
2485fn bearer_token_from_header(header: Option<&str>) -> Option<&str> {
2486 header.and_then(|value| value.strip_prefix("Bearer "))
2487}
2488
2489fn invalid_argument(message: impl Into<String>) -> Status {
2490 Status::invalid_argument(message.into())
2491}
2492
2493fn internal_status(message: impl Into<String>) -> Status {
2494 Status::internal(message.into())
2495}
2496
2497fn trust_level_from_proto(value: &str) -> Result<MemoryTrustLevel, Status> {
2498 match value {
2499 "" | "derived" => Ok(MemoryTrustLevel::Derived),
2500 "untrusted" => Ok(MemoryTrustLevel::Untrusted),
2501 "observed" => Ok(MemoryTrustLevel::Observed),
2502 "verified" => Ok(MemoryTrustLevel::Verified),
2503 "pinned" => Ok(MemoryTrustLevel::Pinned),
2504 other => Err(invalid_argument(format!("unknown trust level: {other}"))),
2505 }
2506}
2507
2508fn trust_level_to_proto(value: MemoryTrustLevel) -> String {
2509 match value {
2510 MemoryTrustLevel::Untrusted => "untrusted",
2511 MemoryTrustLevel::Observed => "observed",
2512 MemoryTrustLevel::Derived => "derived",
2513 MemoryTrustLevel::Verified => "verified",
2514 MemoryTrustLevel::Pinned => "pinned",
2515 }
2516 .to_string()
2517}
2518
2519fn record_kind_from_proto(value: &str) -> Result<MemoryRecordKind, Status> {
2520 match value {
2521 "episodic" => Ok(MemoryRecordKind::Episodic),
2522 "summary" => Ok(MemoryRecordKind::Summary),
2523 "fact" => Ok(MemoryRecordKind::Fact),
2524 "preference" => Ok(MemoryRecordKind::Preference),
2525 "task" => Ok(MemoryRecordKind::Task),
2526 "artifact" => Ok(MemoryRecordKind::Artifact),
2527 "hypothesis" => Ok(MemoryRecordKind::Hypothesis),
2528 other => Err(invalid_argument(format!("unknown record kind: {other}"))),
2529 }
2530}
2531
2532fn record_kind_to_proto(value: MemoryRecordKind) -> String {
2533 match value {
2534 MemoryRecordKind::Episodic => "episodic",
2535 MemoryRecordKind::Summary => "summary",
2536 MemoryRecordKind::Fact => "fact",
2537 MemoryRecordKind::Preference => "preference",
2538 MemoryRecordKind::Task => "task",
2539 MemoryRecordKind::Artifact => "artifact",
2540 MemoryRecordKind::Hypothesis => "hypothesis",
2541 }
2542 .to_string()
2543}
2544
2545fn quality_state_from_proto(value: &str) -> Result<MemoryQualityState, Status> {
2546 match value {
2547 "draft" => Ok(MemoryQualityState::Draft),
2548 "active" => Ok(MemoryQualityState::Active),
2549 "verified" => Ok(MemoryQualityState::Verified),
2550 "archived" => Ok(MemoryQualityState::Archived),
2551 "suppressed" => Ok(MemoryQualityState::Suppressed),
2552 "deleted" => Ok(MemoryQualityState::Deleted),
2553 other => Err(invalid_argument(format!("unknown quality state: {other}"))),
2554 }
2555}
2556
2557fn quality_state_to_proto(value: MemoryQualityState) -> String {
2558 match value {
2559 MemoryQualityState::Draft => "draft",
2560 MemoryQualityState::Active => "active",
2561 MemoryQualityState::Verified => "verified",
2562 MemoryQualityState::Archived => "archived",
2563 MemoryQualityState::Suppressed => "suppressed",
2564 MemoryQualityState::Deleted => "deleted",
2565 }
2566 .to_string()
2567}
2568
2569fn scope_from_proto(scope: ProtoMemoryScope) -> Result<MemoryScope, Status> {
2570 Ok(MemoryScope {
2571 tenant_id: scope.tenant_id,
2572 namespace: scope.namespace,
2573 actor_id: scope.actor_id,
2574 conversation_id: scope.conversation_id,
2575 session_id: scope.session_id,
2576 source: scope.source,
2577 labels: scope.labels,
2578 trust_level: trust_level_from_proto(&scope.trust_level)?,
2579 })
2580}
2581
2582fn scope_to_proto(scope: MemoryScope) -> ProtoMemoryScope {
2583 ProtoMemoryScope {
2584 tenant_id: scope.tenant_id,
2585 namespace: scope.namespace,
2586 actor_id: scope.actor_id,
2587 conversation_id: scope.conversation_id,
2588 session_id: scope.session_id,
2589 source: scope.source,
2590 labels: scope.labels,
2591 trust_level: trust_level_to_proto(scope.trust_level),
2592 }
2593}
2594
2595fn artifact_from_proto(value: ProtoArtifactPointer) -> ArtifactPointer {
2596 ArtifactPointer {
2597 uri: value.uri,
2598 media_type: value.media_type,
2599 checksum: value.checksum,
2600 }
2601}
2602
2603fn artifact_to_proto(value: ArtifactPointer) -> ProtoArtifactPointer {
2604 ProtoArtifactPointer {
2605 uri: value.uri,
2606 media_type: value.media_type,
2607 checksum: value.checksum,
2608 }
2609}
2610
2611fn continuity_state_from_proto(value: &str) -> Result<EpisodeContinuityState, Status> {
2612 match value {
2613 "" | "open" => Ok(EpisodeContinuityState::Open),
2614 "resolved" => Ok(EpisodeContinuityState::Resolved),
2615 "superseded" => Ok(EpisodeContinuityState::Superseded),
2616 "abandoned" => Ok(EpisodeContinuityState::Abandoned),
2617 other => Err(invalid_argument(format!(
2618 "unknown continuity state: {other}"
2619 ))),
2620 }
2621}
2622
2623fn continuity_state_to_proto(value: EpisodeContinuityState) -> String {
2624 match value {
2625 EpisodeContinuityState::Open => "open",
2626 EpisodeContinuityState::Resolved => "resolved",
2627 EpisodeContinuityState::Superseded => "superseded",
2628 EpisodeContinuityState::Abandoned => "abandoned",
2629 }
2630 .to_string()
2631}
2632
2633fn affective_provenance_from_proto(value: &str) -> Result<AffectiveAnnotationProvenance, Status> {
2634 match value {
2635 "" | "authored" => Ok(AffectiveAnnotationProvenance::Authored),
2636 "imported" => Ok(AffectiveAnnotationProvenance::Imported),
2637 "derived" => Ok(AffectiveAnnotationProvenance::Derived),
2638 other => Err(invalid_argument(format!(
2639 "unknown affective provenance: {other}"
2640 ))),
2641 }
2642}
2643
2644fn affective_provenance_to_proto(value: AffectiveAnnotationProvenance) -> String {
2645 match value {
2646 AffectiveAnnotationProvenance::Authored => "authored",
2647 AffectiveAnnotationProvenance::Imported => "imported",
2648 AffectiveAnnotationProvenance::Derived => "derived",
2649 }
2650 .to_string()
2651}
2652
2653fn temporal_order_from_proto(value: &str) -> Result<RecallTemporalOrder, Status> {
2654 match value {
2655 "" | "relevance" => Ok(RecallTemporalOrder::Relevance),
2656 "chronological_asc" => Ok(RecallTemporalOrder::ChronologicalAsc),
2657 "chronological_desc" => Ok(RecallTemporalOrder::ChronologicalDesc),
2658 other => Err(invalid_argument(format!("unknown temporal order: {other}"))),
2659 }
2660}
2661
2662fn planning_profile_from_proto(value: &str) -> Result<RecallPlanningProfile, Status> {
2663 match value {
2664 "" | "fast_path" => Ok(RecallPlanningProfile::FastPath),
2665 "continuity_aware" => Ok(RecallPlanningProfile::ContinuityAware),
2666 other => Err(invalid_argument(format!(
2667 "unknown planning profile: {other}"
2668 ))),
2669 }
2670}
2671
2672fn planning_profile_to_proto(value: RecallPlanningProfile) -> String {
2673 match value {
2674 RecallPlanningProfile::FastPath => "fast_path",
2675 RecallPlanningProfile::ContinuityAware => "continuity_aware",
2676 }
2677 .to_string()
2678}
2679
2680fn planner_stage_to_proto(value: RecallPlannerStage) -> String {
2681 match value {
2682 RecallPlannerStage::CandidateGeneration => "candidate_generation",
2683 RecallPlannerStage::GraphExpansion => "graph_expansion",
2684 RecallPlannerStage::Selection => "selection",
2685 }
2686 .to_string()
2687}
2688
2689fn candidate_source_to_proto(value: RecallCandidateSource) -> String {
2690 match value {
2691 RecallCandidateSource::Lexical => "lexical",
2692 RecallCandidateSource::Semantic => "semantic",
2693 RecallCandidateSource::Metadata => "metadata",
2694 RecallCandidateSource::Episode => "episode",
2695 RecallCandidateSource::Graph => "graph",
2696 RecallCandidateSource::Temporal => "temporal",
2697 RecallCandidateSource::Provenance => "provenance",
2698 }
2699 .to_string()
2700}
2701
2702fn affective_annotation_from_proto(
2703 value: ProtoAffectiveAnnotation,
2704) -> Result<AffectiveAnnotation, Status> {
2705 Ok(AffectiveAnnotation {
2706 tone: value.tone,
2707 sentiment: value.sentiment,
2708 urgency: value.urgency,
2709 confidence: value.confidence,
2710 tension: value.tension,
2711 provenance: affective_provenance_from_proto(&value.provenance)?,
2712 })
2713}
2714
2715fn affective_annotation_to_proto(value: AffectiveAnnotation) -> ProtoAffectiveAnnotation {
2716 ProtoAffectiveAnnotation {
2717 tone: value.tone,
2718 sentiment: value.sentiment,
2719 urgency: value.urgency,
2720 confidence: value.confidence,
2721 tension: value.tension,
2722 provenance: affective_provenance_to_proto(value.provenance),
2723 }
2724}
2725
2726fn historical_state_from_proto(value: &str) -> Result<MemoryHistoricalState, Status> {
2727 match value {
2728 "" | "current" => Ok(MemoryHistoricalState::Current),
2729 "historical" => Ok(MemoryHistoricalState::Historical),
2730 "superseded" => Ok(MemoryHistoricalState::Superseded),
2731 other => Err(invalid_argument(format!(
2732 "unknown historical state: {other}"
2733 ))),
2734 }
2735}
2736
2737fn historical_state_to_proto(value: MemoryHistoricalState) -> String {
2738 match value {
2739 MemoryHistoricalState::Current => "current",
2740 MemoryHistoricalState::Historical => "historical",
2741 MemoryHistoricalState::Superseded => "superseded",
2742 }
2743 .to_string()
2744}
2745
2746fn lineage_relation_from_proto(value: &str) -> Result<LineageRelationKind, Status> {
2747 match value {
2748 "" | "derived_from" => Ok(LineageRelationKind::DerivedFrom),
2749 "consolidated_from" => Ok(LineageRelationKind::ConsolidatedFrom),
2750 "supersedes" => Ok(LineageRelationKind::Supersedes),
2751 "superseded_by" => Ok(LineageRelationKind::SupersededBy),
2752 "conflicts_with" => Ok(LineageRelationKind::ConflictsWith),
2753 other => Err(invalid_argument(format!(
2754 "unknown lineage relation: {other}"
2755 ))),
2756 }
2757}
2758
2759fn lineage_relation_to_proto(value: LineageRelationKind) -> String {
2760 match value {
2761 LineageRelationKind::DerivedFrom => "derived_from",
2762 LineageRelationKind::ConsolidatedFrom => "consolidated_from",
2763 LineageRelationKind::Supersedes => "supersedes",
2764 LineageRelationKind::SupersededBy => "superseded_by",
2765 LineageRelationKind::ConflictsWith => "conflicts_with",
2766 }
2767 .to_string()
2768}
2769
2770fn lineage_link_from_proto(value: ProtoLineageLink) -> Result<LineageLink, Status> {
2771 Ok(LineageLink {
2772 record_id: value.record_id,
2773 relation: lineage_relation_from_proto(&value.relation)?,
2774 confidence: value.confidence,
2775 })
2776}
2777
2778fn lineage_link_to_proto(value: LineageLink) -> ProtoLineageLink {
2779 ProtoLineageLink {
2780 record_id: value.record_id,
2781 relation: lineage_relation_to_proto(value.relation),
2782 confidence: value.confidence,
2783 }
2784}
2785
2786fn conflict_review_state_from_proto(value: &str) -> Result<ConflictReviewState, Status> {
2787 match value {
2788 "" | "none" => Ok(ConflictReviewState::None),
2789 "potential_conflict" => Ok(ConflictReviewState::PotentialConflict),
2790 "under_review" => Ok(ConflictReviewState::UnderReview),
2791 "resolved" => Ok(ConflictReviewState::Resolved),
2792 "dismissed" => Ok(ConflictReviewState::Dismissed),
2793 other => Err(invalid_argument(format!(
2794 "unknown conflict review state: {other}"
2795 ))),
2796 }
2797}
2798
2799fn conflict_review_state_to_proto(value: ConflictReviewState) -> String {
2800 match value {
2801 ConflictReviewState::None => "none",
2802 ConflictReviewState::PotentialConflict => "potential_conflict",
2803 ConflictReviewState::UnderReview => "under_review",
2804 ConflictReviewState::Resolved => "resolved",
2805 ConflictReviewState::Dismissed => "dismissed",
2806 }
2807 .to_string()
2808}
2809
2810fn conflict_resolution_kind_from_proto(value: &str) -> Result<ConflictResolutionKind, Status> {
2811 match value {
2812 "" | "none" => Ok(ConflictResolutionKind::None),
2813 "accepted" => Ok(ConflictResolutionKind::Accepted),
2814 "rejected" => Ok(ConflictResolutionKind::Rejected),
2815 "superseded" => Ok(ConflictResolutionKind::Superseded),
2816 "merged" => Ok(ConflictResolutionKind::Merged),
2817 other => Err(invalid_argument(format!(
2818 "unknown conflict resolution kind: {other}"
2819 ))),
2820 }
2821}
2822
2823fn conflict_resolution_kind_to_proto(value: ConflictResolutionKind) -> String {
2824 match value {
2825 ConflictResolutionKind::None => "none",
2826 ConflictResolutionKind::Accepted => "accepted",
2827 ConflictResolutionKind::Rejected => "rejected",
2828 ConflictResolutionKind::Superseded => "superseded",
2829 ConflictResolutionKind::Merged => "merged",
2830 }
2831 .to_string()
2832}
2833
2834fn conflict_annotation_from_proto(
2835 value: ProtoConflictAnnotation,
2836) -> Result<ConflictAnnotation, Status> {
2837 Ok(ConflictAnnotation {
2838 state: conflict_review_state_from_proto(&value.state)?,
2839 conflicting_record_ids: value.conflicting_record_ids,
2840 drift_score: value.drift_score,
2841 resolution: conflict_resolution_kind_from_proto(&value.resolution)?,
2842 resolved_by: value.resolved_by,
2843 resolved_at_unix_ms: value.resolved_at_unix_ms,
2844 note: value.note,
2845 })
2846}
2847
2848fn conflict_annotation_to_proto(value: ConflictAnnotation) -> ProtoConflictAnnotation {
2849 ProtoConflictAnnotation {
2850 state: conflict_review_state_to_proto(value.state),
2851 conflicting_record_ids: value.conflicting_record_ids,
2852 drift_score: value.drift_score,
2853 resolution: conflict_resolution_kind_to_proto(value.resolution),
2854 resolved_by: value.resolved_by,
2855 resolved_at_unix_ms: value.resolved_at_unix_ms,
2856 note: value.note,
2857 }
2858}
2859
2860fn historical_mode_from_proto(value: &str) -> Result<RecallHistoricalMode, Status> {
2861 match value {
2862 "" | "current_only" => Ok(RecallHistoricalMode::CurrentOnly),
2863 "include_historical" => Ok(RecallHistoricalMode::IncludeHistorical),
2864 "historical_only" => Ok(RecallHistoricalMode::HistoricalOnly),
2865 other => Err(invalid_argument(format!(
2866 "unknown historical mode: {other}"
2867 ))),
2868 }
2869}
2870
2871fn episode_context_from_proto(value: ProtoEpisodeContext) -> Result<EpisodeContext, Status> {
2872 Ok(EpisodeContext {
2873 schema_version: if value.schema_version == 0 {
2874 EPISODE_SCHEMA_VERSION
2875 } else {
2876 value.schema_version
2877 },
2878 episode_id: value.episode_id,
2879 summary: value.summary,
2880 continuity_state: continuity_state_from_proto(&value.continuity_state)?,
2881 actor_ids: value.actor_ids,
2882 goal: value.goal,
2883 outcome: value.outcome,
2884 started_at_unix_ms: value.started_at_unix_ms,
2885 ended_at_unix_ms: value.ended_at_unix_ms,
2886 last_active_unix_ms: value.last_active_unix_ms,
2887 recurrence_key: value.recurrence_key,
2888 recurrence_interval_ms: value.recurrence_interval_ms,
2889 boundary_label: value.boundary_label,
2890 previous_record_id: value.previous_record_id,
2891 next_record_id: value.next_record_id,
2892 causal_record_ids: value.causal_record_ids,
2893 related_record_ids: value.related_record_ids,
2894 linked_artifact_uris: value.linked_artifact_uris,
2895 salience: value
2896 .salience
2897 .map_or_else(EpisodeSalience::default, |salience| EpisodeSalience {
2898 reuse_count: salience.reuse_count,
2899 novelty_score: salience.novelty_score,
2900 goal_relevance: salience.goal_relevance,
2901 unresolved_weight: salience.unresolved_weight,
2902 }),
2903 affective: value
2904 .affective
2905 .map(affective_annotation_from_proto)
2906 .transpose()?,
2907 })
2908}
2909
2910fn episode_context_to_proto(value: EpisodeContext) -> ProtoEpisodeContext {
2911 ProtoEpisodeContext {
2912 schema_version: value.schema_version,
2913 episode_id: value.episode_id,
2914 summary: value.summary,
2915 continuity_state: continuity_state_to_proto(value.continuity_state),
2916 actor_ids: value.actor_ids,
2917 goal: value.goal,
2918 outcome: value.outcome,
2919 started_at_unix_ms: value.started_at_unix_ms,
2920 ended_at_unix_ms: value.ended_at_unix_ms,
2921 last_active_unix_ms: value.last_active_unix_ms,
2922 recurrence_key: value.recurrence_key,
2923 recurrence_interval_ms: value.recurrence_interval_ms,
2924 boundary_label: value.boundary_label,
2925 previous_record_id: value.previous_record_id,
2926 next_record_id: value.next_record_id,
2927 causal_record_ids: value.causal_record_ids,
2928 related_record_ids: value.related_record_ids,
2929 linked_artifact_uris: value.linked_artifact_uris,
2930 salience: Some(ProtoEpisodeSalience {
2931 reuse_count: value.salience.reuse_count,
2932 novelty_score: value.salience.novelty_score,
2933 goal_relevance: value.salience.goal_relevance,
2934 unresolved_weight: value.salience.unresolved_weight,
2935 }),
2936 affective: value.affective.map(affective_annotation_to_proto),
2937 }
2938}
2939
2940fn record_from_proto(record: ProtoMemoryRecord) -> Result<MemoryRecord, Status> {
2941 Ok(MemoryRecord {
2942 id: record.id,
2943 scope: scope_from_proto(
2944 record
2945 .scope
2946 .ok_or_else(|| invalid_argument("memory record scope is required"))?,
2947 )?,
2948 kind: record_kind_from_proto(&record.kind)?,
2949 content: record.content,
2950 summary: record.summary,
2951 source_id: record.source_id,
2952 metadata: record.metadata.into_iter().collect(),
2953 quality_state: quality_state_from_proto(&record.quality_state)?,
2954 created_at_unix_ms: record.created_at_unix_ms,
2955 updated_at_unix_ms: record.updated_at_unix_ms,
2956 expires_at_unix_ms: record.expires_at_unix_ms,
2957 importance_score: record.importance_score,
2958 artifact: record.artifact.map(artifact_from_proto),
2959 episode: record.episode.map(episode_context_from_proto).transpose()?,
2960 historical_state: historical_state_from_proto(
2961 record.historical_state.as_deref().unwrap_or(""),
2962 )?,
2963 lineage: record
2964 .lineage
2965 .into_iter()
2966 .map(lineage_link_from_proto)
2967 .collect::<Result<Vec<_>, _>>()?,
2968 conflict: record
2969 .conflict
2970 .map(conflict_annotation_from_proto)
2971 .transpose()?,
2972 })
2973}
2974
2975fn record_to_proto(record: MemoryRecord) -> ProtoMemoryRecord {
2976 ProtoMemoryRecord {
2977 id: record.id,
2978 scope: Some(scope_to_proto(record.scope)),
2979 kind: record_kind_to_proto(record.kind),
2980 content: record.content,
2981 summary: record.summary,
2982 metadata: record.metadata.into_iter().collect(),
2983 quality_state: quality_state_to_proto(record.quality_state),
2984 created_at_unix_ms: record.created_at_unix_ms,
2985 updated_at_unix_ms: record.updated_at_unix_ms,
2986 expires_at_unix_ms: record.expires_at_unix_ms,
2987 importance_score: record.importance_score,
2988 source_id: record.source_id,
2989 artifact: record.artifact.map(artifact_to_proto),
2990 episode: record.episode.map(episode_context_to_proto),
2991 historical_state: Some(historical_state_to_proto(record.historical_state)),
2992 lineage: record
2993 .lineage
2994 .into_iter()
2995 .map(lineage_link_to_proto)
2996 .collect(),
2997 conflict: record.conflict.map(conflict_annotation_to_proto),
2998 }
2999}
3000
3001fn recall_explanation_to_proto(value: RecallExplanation) -> ProtoRecallExplanation {
3002 ProtoRecallExplanation {
3003 selected_channels: value.selected_channels,
3004 policy_notes: value.policy_notes,
3005 trace_id: value.trace_id,
3006 planning_trace: value.planning_trace.map(recall_planning_trace_to_proto),
3007 scorer_kind: value
3008 .scorer_kind
3009 .map(recall_scorer_kind_to_proto)
3010 .unwrap_or(ProtoRecallScorerKind::Unspecified as i32),
3011 scoring_profile: value
3012 .scoring_profile
3013 .map(recall_scoring_profile_to_proto)
3014 .unwrap_or(ProtoRecallScoringProfile::Unspecified as i32),
3015 planning_profile: value.planning_profile.map(planning_profile_to_proto),
3016 policy_profile: value
3017 .policy_profile
3018 .map(recall_policy_profile_to_proto)
3019 .unwrap_or(ProtoRecallPolicyProfile::Unspecified as i32),
3020 }
3021}
3022
3023fn recall_scorer_kind_to_proto(value: RecallScorerKind) -> i32 {
3024 match value {
3025 RecallScorerKind::Profile => ProtoRecallScorerKind::Profile as i32,
3026 RecallScorerKind::Curated => ProtoRecallScorerKind::Curated as i32,
3027 }
3028}
3029
3030fn recall_scoring_profile_to_proto(value: RecallScoringProfile) -> i32 {
3031 match value {
3032 RecallScoringProfile::Balanced => ProtoRecallScoringProfile::Balanced as i32,
3033 RecallScoringProfile::LexicalFirst => ProtoRecallScoringProfile::LexicalFirst as i32,
3034 RecallScoringProfile::ImportanceFirst => ProtoRecallScoringProfile::ImportanceFirst as i32,
3035 }
3036}
3037
3038fn recall_policy_profile_to_proto(value: RecallPolicyProfile) -> i32 {
3039 match value {
3040 RecallPolicyProfile::General => ProtoRecallPolicyProfile::General as i32,
3041 RecallPolicyProfile::Support => ProtoRecallPolicyProfile::Support as i32,
3042 RecallPolicyProfile::Research => ProtoRecallPolicyProfile::Research as i32,
3043 RecallPolicyProfile::Assistant => ProtoRecallPolicyProfile::Assistant as i32,
3044 RecallPolicyProfile::AutonomousAgent => ProtoRecallPolicyProfile::AutonomousAgent as i32,
3045 }
3046}
3047
3048fn embedding_provider_kind_to_proto(value: EmbeddingProviderKind) -> i32 {
3049 match value {
3050 EmbeddingProviderKind::Disabled => ProtoEmbeddingProviderKind::Disabled as i32,
3051 EmbeddingProviderKind::DeterministicLocal => {
3052 ProtoEmbeddingProviderKind::DeterministicLocal as i32
3053 }
3054 }
3055}
3056
3057fn engine_tuning_info_to_proto(value: EngineTuningInfo) -> ProtoEngineTuningInfo {
3058 ProtoEngineTuningInfo {
3059 recall_scorer_kind: recall_scorer_kind_to_proto(value.recall_scorer_kind),
3060 recall_scoring_profile: recall_scoring_profile_to_proto(value.recall_scoring_profile),
3061 embedding_provider_kind: embedding_provider_kind_to_proto(value.embedding_provider_kind),
3062 embedding_dimensions: value.embedding_dimensions as u64,
3063 graph_expansion_max_hops: u32::from(value.graph_expansion_max_hops),
3064 compaction_summarize_after_record_count: value.compaction_summarize_after_record_count
3065 as u64,
3066 compaction_cold_archive_after_days: value.compaction_cold_archive_after_days,
3067 compaction_cold_archive_importance_threshold_per_mille: u32::from(
3068 value.compaction_cold_archive_importance_threshold_per_mille,
3069 ),
3070 recall_planning_profile: Some(planning_profile_to_proto(value.recall_planning_profile)),
3071 recall_policy_profile: recall_policy_profile_to_proto(value.recall_policy_profile),
3072 }
3073}
3074
3075fn engine_tuning_info_from_proto(value: ProtoEngineTuningInfo) -> Result<EngineTuningInfo, Status> {
3076 Ok(EngineTuningInfo {
3077 recall_scorer_kind: match ProtoRecallScorerKind::try_from(value.recall_scorer_kind)
3078 .unwrap_or(ProtoRecallScorerKind::Unspecified)
3079 {
3080 ProtoRecallScorerKind::Profile => RecallScorerKind::Profile,
3081 ProtoRecallScorerKind::Curated => RecallScorerKind::Curated,
3082 ProtoRecallScorerKind::Unspecified => {
3083 return Err(invalid_argument("engine recall scorer kind is required"));
3084 }
3085 },
3086 recall_scoring_profile: match ProtoRecallScoringProfile::try_from(
3087 value.recall_scoring_profile,
3088 )
3089 .unwrap_or(ProtoRecallScoringProfile::Unspecified)
3090 {
3091 ProtoRecallScoringProfile::Balanced => RecallScoringProfile::Balanced,
3092 ProtoRecallScoringProfile::LexicalFirst => RecallScoringProfile::LexicalFirst,
3093 ProtoRecallScoringProfile::ImportanceFirst => RecallScoringProfile::ImportanceFirst,
3094 ProtoRecallScoringProfile::Unspecified => {
3095 return Err(invalid_argument(
3096 "engine recall scoring profile is required",
3097 ));
3098 }
3099 },
3100 recall_planning_profile: planning_profile_from_proto(
3101 value.recall_planning_profile.as_deref().unwrap_or(""),
3102 )?,
3103 recall_policy_profile: match ProtoRecallPolicyProfile::try_from(value.recall_policy_profile)
3104 .unwrap_or(ProtoRecallPolicyProfile::Unspecified)
3105 {
3106 ProtoRecallPolicyProfile::General => RecallPolicyProfile::General,
3107 ProtoRecallPolicyProfile::Support => RecallPolicyProfile::Support,
3108 ProtoRecallPolicyProfile::Research => RecallPolicyProfile::Research,
3109 ProtoRecallPolicyProfile::Assistant => RecallPolicyProfile::Assistant,
3110 ProtoRecallPolicyProfile::AutonomousAgent => RecallPolicyProfile::AutonomousAgent,
3111 ProtoRecallPolicyProfile::Unspecified => RecallPolicyProfile::General,
3112 },
3113 embedding_provider_kind: match ProtoEmbeddingProviderKind::try_from(
3114 value.embedding_provider_kind,
3115 )
3116 .unwrap_or(ProtoEmbeddingProviderKind::Unspecified)
3117 {
3118 ProtoEmbeddingProviderKind::Disabled => EmbeddingProviderKind::Disabled,
3119 ProtoEmbeddingProviderKind::DeterministicLocal => {
3120 EmbeddingProviderKind::DeterministicLocal
3121 }
3122 ProtoEmbeddingProviderKind::Unspecified => {
3123 return Err(invalid_argument(
3124 "engine embedding provider kind is required",
3125 ));
3126 }
3127 },
3128 embedding_dimensions: value.embedding_dimensions as usize,
3129 graph_expansion_max_hops: value.graph_expansion_max_hops as u8,
3130 compaction_summarize_after_record_count: value.compaction_summarize_after_record_count
3131 as usize,
3132 compaction_cold_archive_after_days: value.compaction_cold_archive_after_days,
3133 compaction_cold_archive_importance_threshold_per_mille: value
3134 .compaction_cold_archive_importance_threshold_per_mille
3135 as u16,
3136 })
3137}
3138
3139fn recall_planning_trace_to_proto(value: RecallPlanningTrace) -> ProtoRecallPlanningTrace {
3140 ProtoRecallPlanningTrace {
3141 trace_id: value.trace_id,
3142 token_budget_applied: value.token_budget_applied,
3143 candidates: value
3144 .candidates
3145 .into_iter()
3146 .map(recall_trace_candidate_to_proto)
3147 .collect(),
3148 }
3149}
3150
3151fn recall_trace_candidate_to_proto(value: RecallTraceCandidate) -> ProtoRecallTraceCandidate {
3152 ProtoRecallTraceCandidate {
3153 record_id: value.record_id,
3154 kind: record_kind_to_proto(value.kind),
3155 selected: value.selected,
3156 matched_terms: value.matched_terms,
3157 decision_reason: value.decision_reason,
3158 breakdown: Some(recall_score_breakdown_to_proto(value.breakdown)),
3159 selection_rank: value.selection_rank,
3160 selected_channels: value.selected_channels,
3161 filter_reasons: value.filter_reasons,
3162 relation_reasons: value.relation_reasons,
3163 candidate_sources: value
3164 .candidate_sources
3165 .into_iter()
3166 .map(candidate_source_to_proto)
3167 .collect(),
3168 planner_stage: Some(planner_stage_to_proto(value.planner_stage)),
3169 }
3170}
3171
3172fn recall_score_breakdown_to_proto(
3173 value: mnemara_core::RecallScoreBreakdown,
3174) -> ProtoRecallScoreBreakdown {
3175 ProtoRecallScoreBreakdown {
3176 lexical: value.lexical,
3177 semantic: value.semantic,
3178 graph: value.graph,
3179 temporal: value.temporal,
3180 policy: value.policy,
3181 total: value.total,
3182 metadata: value.metadata,
3183 curation: value.curation,
3184 episodic: value.episodic,
3185 salience: value.salience,
3186 }
3187}
3188
3189fn namespace_stats_to_proto(value: NamespaceStats) -> ProtoNamespaceStats {
3190 ProtoNamespaceStats {
3191 tenant_id: value.tenant_id,
3192 namespace: value.namespace,
3193 active_records: value.active_records,
3194 archived_records: value.archived_records,
3195 deleted_records: value.deleted_records,
3196 suppressed_records: value.suppressed_records,
3197 pinned_records: value.pinned_records,
3198 }
3199}
3200
3201fn maintenance_stats_to_proto(value: MaintenanceStats) -> ProtoMaintenanceStats {
3202 ProtoMaintenanceStats {
3203 duplicate_candidate_groups: value.duplicate_candidate_groups,
3204 duplicate_candidate_records: value.duplicate_candidate_records,
3205 tombstoned_records: value.tombstoned_records,
3206 expired_records: value.expired_records,
3207 stale_idempotency_keys: value.stale_idempotency_keys,
3208 historical_records: value.historical_records,
3209 superseded_records: value.superseded_records,
3210 lineage_links: value.lineage_links,
3211 }
3212}
3213
3214fn graph_edge_kind_to_wire(value: GraphInspectionEdgeKind) -> String {
3215 match value {
3216 GraphInspectionEdgeKind::EpisodeMembership => "EpisodeMembership",
3217 GraphInspectionEdgeKind::ChronologyPrevious => "ChronologyPrevious",
3218 GraphInspectionEdgeKind::ChronologyNext => "ChronologyNext",
3219 GraphInspectionEdgeKind::Causal => "Causal",
3220 GraphInspectionEdgeKind::Related => "Related",
3221 GraphInspectionEdgeKind::Lineage => "Lineage",
3222 GraphInspectionEdgeKind::Conflict => "Conflict",
3223 }
3224 .to_string()
3225}
3226
3227fn graph_node_to_proto(value: GraphInspectionNode) -> ProtoGraphInspectionNode {
3228 ProtoGraphInspectionNode {
3229 record_id: value.record_id,
3230 tenant_id: value.tenant_id,
3231 namespace: value.namespace,
3232 actor_id: value.actor_id,
3233 kind: record_kind_to_proto(value.kind),
3234 summary: value.summary,
3235 quality_state: quality_state_to_proto(value.quality_state),
3236 historical_state: historical_state_to_proto(value.historical_state),
3237 episode_id: value.episode_id,
3238 continuity_state: value.continuity_state.map(continuity_state_to_proto),
3239 conflict_state: value.conflict_state.map(conflict_review_state_to_proto),
3240 importance_per_mille: u32::from(value.importance_per_mille),
3241 updated_at_unix_ms: value.updated_at_unix_ms,
3242 }
3243}
3244
3245fn graph_edge_to_proto(value: GraphInspectionEdge) -> ProtoGraphInspectionEdge {
3246 ProtoGraphInspectionEdge {
3247 source_id: value.source_id,
3248 target_id: value.target_id,
3249 kind: graph_edge_kind_to_wire(value.kind),
3250 details: value.details,
3251 }
3252}
3253
3254fn graph_report_to_proto(value: GraphInspectionReport) -> GraphInspectionReply {
3255 GraphInspectionReply {
3256 generated_at_unix_ms: value.generated_at_unix_ms,
3257 total_records_scanned: value.total_records_scanned,
3258 nodes: value.nodes.into_iter().map(graph_node_to_proto).collect(),
3259 edges: value.edges.into_iter().map(graph_edge_to_proto).collect(),
3260 truncated: value.truncated,
3261 }
3262}
3263
3264fn portable_record_to_proto(value: PortableRecord) -> ProtoPortableRecord {
3265 ProtoPortableRecord {
3266 record: Some(record_to_proto(value.record)),
3267 idempotency_key: value.idempotency_key,
3268 }
3269}
3270
3271fn snapshot_manifest_to_proto(value: SnapshotManifest) -> SnapshotReply {
3272 SnapshotReply {
3273 snapshot_id: value.snapshot_id,
3274 created_at_unix_ms: value.created_at_unix_ms,
3275 namespaces: value.namespaces,
3276 record_count: value.record_count,
3277 storage_bytes: value.storage_bytes,
3278 engine: Some(engine_tuning_info_to_proto(value.engine)),
3279 }
3280}
3281
3282fn portable_package_to_proto(value: PortableStorePackage) -> ProtoExportReply {
3283 ProtoExportReply {
3284 package_version: value.package_version,
3285 exported_at_unix_ms: value.exported_at_unix_ms,
3286 manifest: Some(snapshot_manifest_to_proto(value.manifest)),
3287 records: value
3288 .records
3289 .into_iter()
3290 .map(portable_record_to_proto)
3291 .collect(),
3292 }
3293}
3294
3295fn import_mode_from_proto(value: i32) -> Result<ImportMode, Status> {
3296 match ProtoImportMode::try_from(value).unwrap_or(ProtoImportMode::Unspecified) {
3297 ProtoImportMode::Validate => Ok(ImportMode::Validate),
3298 ProtoImportMode::Merge => Ok(ImportMode::Merge),
3299 ProtoImportMode::Replace => Ok(ImportMode::Replace),
3300 ProtoImportMode::Unspecified => Err(invalid_argument("import mode is required")),
3301 }
3302}
3303
3304fn integrity_report_to_proto(value: IntegrityCheckReport) -> ProtoIntegrityCheckReply {
3305 ProtoIntegrityCheckReply {
3306 generated_at_unix_ms: value.generated_at_unix_ms,
3307 healthy: value.healthy,
3308 scanned_records: value.scanned_records,
3309 scanned_idempotency_keys: value.scanned_idempotency_keys,
3310 stale_idempotency_keys: value.stale_idempotency_keys,
3311 missing_idempotency_keys: value.missing_idempotency_keys,
3312 duplicate_active_records: value.duplicate_active_records,
3313 }
3314}
3315
3316fn repair_report_to_proto(value: RepairReport) -> ProtoRepairReply {
3317 ProtoRepairReply {
3318 dry_run: value.dry_run,
3319 scanned_records: value.scanned_records,
3320 scanned_idempotency_keys: value.scanned_idempotency_keys,
3321 removed_stale_idempotency_keys: value.removed_stale_idempotency_keys,
3322 rebuilt_missing_idempotency_keys: value.rebuilt_missing_idempotency_keys,
3323 healthy_after: value.healthy_after,
3324 }
3325}
3326
3327fn compaction_report_to_proto(value: CompactionReport) -> CompactReply {
3328 CompactReply {
3329 deduplicated_records: value.deduplicated_records,
3330 archived_records: value.archived_records,
3331 summarized_clusters: value.summarized_clusters,
3332 pruned_graph_edges: value.pruned_graph_edges,
3333 superseded_records: value.superseded_records,
3334 lineage_links_created: value.lineage_links_created,
3335 dry_run: value.dry_run,
3336 }
3337}
3338
3339fn synthesis_request_from_proto(value: ProtoSynthesisRequest) -> SynthesisRequest {
3340 let defaults = SynthesisRequest::default();
3341 SynthesisRequest {
3342 tenant_id: value.tenant_id,
3343 namespace: value.namespace,
3344 actor_id: value.actor_id,
3345 conversation_id: value.conversation_id,
3346 session_id: value.session_id,
3347 from_unix_ms: value.from_unix_ms,
3348 to_unix_ms: value.to_unix_ms,
3349 min_source_records: if value.min_source_records == 0 {
3350 defaults.min_source_records
3351 } else {
3352 value.min_source_records as usize
3353 },
3354 max_source_records: if value.max_source_records == 0 {
3355 defaults.max_source_records
3356 } else {
3357 value.max_source_records as usize
3358 },
3359 max_proposals: if value.max_proposals == 0 {
3360 defaults.max_proposals
3361 } else {
3362 value.max_proposals as usize
3363 },
3364 dry_run: value.dry_run,
3365 reason: if value.reason.trim().is_empty() {
3366 defaults.reason
3367 } else {
3368 value.reason
3369 },
3370 }
3371}
3372
3373fn synthesis_proposal_to_proto(value: SynthesisProposal) -> ProtoSynthesisProposal {
3374 ProtoSynthesisProposal {
3375 proposed_record: Some(record_to_proto(value.proposed_record)),
3376 source_record_ids: value.source_record_ids,
3377 confidence: value.confidence,
3378 rationale: value.rationale,
3379 metadata: value.metadata.into_iter().collect(),
3380 }
3381}
3382
3383fn synthesis_report_to_proto(value: SynthesisReport) -> SynthesisReply {
3384 SynthesisReply {
3385 dry_run: value.dry_run,
3386 scanned_records: value.scanned_records,
3387 eligible_records: value.eligible_records,
3388 proposed_records: value.proposed_records,
3389 persisted_records: value.persisted_records,
3390 lineage_links_created: value.lineage_links_created,
3391 proposals: value
3392 .proposals
3393 .into_iter()
3394 .map(synthesis_proposal_to_proto)
3395 .collect(),
3396 }
3397}
3398
3399fn maintenance_report_to_proto(value: MaintenanceRunReport) -> MaintenanceRunReply {
3400 MaintenanceRunReply {
3401 dry_run: value.dry_run,
3402 integrity_before: value.integrity_before.map(integrity_report_to_proto),
3403 repair: value.repair.map(repair_report_to_proto),
3404 compaction: value.compaction.map(compaction_report_to_proto),
3405 synthesis: value.synthesis.map(synthesis_report_to_proto),
3406 integrity_after: value.integrity_after.map(integrity_report_to_proto),
3407 }
3408}
3409
3410fn snapshot_ship_report_to_proto(value: SnapshotShipReport) -> SnapshotShipReply {
3411 SnapshotShipReply {
3412 target_url: value.target_url,
3413 exported_records: value.exported_records,
3414 imported_records: value.imported_records,
3415 skipped_records: value.skipped_records,
3416 dry_run: value.dry_run,
3417 compatible_package: value.compatible_package,
3418 remote_status: u32::from(value.remote_status),
3419 remote_snapshot_id: value.remote_snapshot_id,
3420 }
3421}
3422
3423fn portable_record_from_proto(value: ProtoPortableRecord) -> Result<PortableRecord, Status> {
3424 Ok(PortableRecord {
3425 record: record_from_proto(
3426 value
3427 .record
3428 .ok_or_else(|| invalid_argument("portable record payload is required"))?,
3429 )?,
3430 idempotency_key: value.idempotency_key,
3431 })
3432}
3433
3434fn snapshot_manifest_from_proto(value: SnapshotReply) -> Result<SnapshotManifest, Status> {
3435 Ok(SnapshotManifest {
3436 snapshot_id: value.snapshot_id,
3437 created_at_unix_ms: value.created_at_unix_ms,
3438 namespaces: value.namespaces,
3439 record_count: value.record_count,
3440 storage_bytes: value.storage_bytes,
3441 engine: value
3442 .engine
3443 .map(engine_tuning_info_from_proto)
3444 .transpose()?
3445 .ok_or_else(|| invalid_argument("snapshot engine tuning info is required"))?,
3446 })
3447}
3448
3449fn portable_package_from_proto(value: ProtoExportReply) -> Result<PortableStorePackage, Status> {
3450 Ok(PortableStorePackage {
3451 package_version: value.package_version,
3452 exported_at_unix_ms: value.exported_at_unix_ms,
3453 manifest: snapshot_manifest_from_proto(
3454 value
3455 .manifest
3456 .ok_or_else(|| invalid_argument("portable export manifest is required"))?,
3457 )?,
3458 records: value
3459 .records
3460 .into_iter()
3461 .map(portable_record_from_proto)
3462 .collect::<Result<Vec<_>, _>>()?,
3463 })
3464}
3465
3466fn trace_operation_kind_to_proto(value: TraceOperationKind) -> i32 {
3467 match value {
3468 TraceOperationKind::Upsert => ProtoTraceOperationKind::Upsert as i32,
3469 TraceOperationKind::BatchUpsert => ProtoTraceOperationKind::BatchUpsert as i32,
3470 TraceOperationKind::Recall => ProtoTraceOperationKind::Recall as i32,
3471 TraceOperationKind::Snapshot => ProtoTraceOperationKind::Snapshot as i32,
3472 TraceOperationKind::Stats => ProtoTraceOperationKind::Stats as i32,
3473 TraceOperationKind::IntegrityCheck => ProtoTraceOperationKind::IntegrityCheck as i32,
3474 TraceOperationKind::Repair => ProtoTraceOperationKind::Repair as i32,
3475 TraceOperationKind::Compact => ProtoTraceOperationKind::Compact as i32,
3476 TraceOperationKind::Delete => ProtoTraceOperationKind::Delete as i32,
3477 TraceOperationKind::Archive => ProtoTraceOperationKind::Archive as i32,
3478 TraceOperationKind::Suppress => ProtoTraceOperationKind::Suppress as i32,
3479 TraceOperationKind::Recover => ProtoTraceOperationKind::Recover as i32,
3480 TraceOperationKind::Export => ProtoTraceOperationKind::Export as i32,
3481 TraceOperationKind::Import => ProtoTraceOperationKind::Import as i32,
3482 TraceOperationKind::MaintenanceRun => ProtoTraceOperationKind::MaintenanceRun as i32,
3483 TraceOperationKind::SnapshotShip => ProtoTraceOperationKind::SnapshotShip as i32,
3484 TraceOperationKind::Changefeed => ProtoTraceOperationKind::Changefeed as i32,
3485 TraceOperationKind::Synthesis => ProtoTraceOperationKind::Synthesis as i32,
3486 }
3487}
3488
3489fn trace_operation_kind_from_proto(value: ProtoTraceOperationKind) -> Option<TraceOperationKind> {
3490 match value {
3491 ProtoTraceOperationKind::Upsert => Some(TraceOperationKind::Upsert),
3492 ProtoTraceOperationKind::BatchUpsert => Some(TraceOperationKind::BatchUpsert),
3493 ProtoTraceOperationKind::Recall => Some(TraceOperationKind::Recall),
3494 ProtoTraceOperationKind::Snapshot => Some(TraceOperationKind::Snapshot),
3495 ProtoTraceOperationKind::Stats => Some(TraceOperationKind::Stats),
3496 ProtoTraceOperationKind::IntegrityCheck => Some(TraceOperationKind::IntegrityCheck),
3497 ProtoTraceOperationKind::Repair => Some(TraceOperationKind::Repair),
3498 ProtoTraceOperationKind::Compact => Some(TraceOperationKind::Compact),
3499 ProtoTraceOperationKind::Delete => Some(TraceOperationKind::Delete),
3500 ProtoTraceOperationKind::Archive => Some(TraceOperationKind::Archive),
3501 ProtoTraceOperationKind::Suppress => Some(TraceOperationKind::Suppress),
3502 ProtoTraceOperationKind::Recover => Some(TraceOperationKind::Recover),
3503 ProtoTraceOperationKind::Export => Some(TraceOperationKind::Export),
3504 ProtoTraceOperationKind::Import => Some(TraceOperationKind::Import),
3505 ProtoTraceOperationKind::MaintenanceRun => Some(TraceOperationKind::MaintenanceRun),
3506 ProtoTraceOperationKind::SnapshotShip => Some(TraceOperationKind::SnapshotShip),
3507 ProtoTraceOperationKind::Changefeed => Some(TraceOperationKind::Changefeed),
3508 ProtoTraceOperationKind::Synthesis => Some(TraceOperationKind::Synthesis),
3509 ProtoTraceOperationKind::Unspecified => None,
3510 }
3511}
3512
3513fn trace_status_to_proto(value: TraceStatus) -> i32 {
3514 match value {
3515 TraceStatus::Ok => ProtoTraceStatus::Ok as i32,
3516 TraceStatus::Rejected => ProtoTraceStatus::Rejected as i32,
3517 TraceStatus::Error => ProtoTraceStatus::Error as i32,
3518 }
3519}
3520
3521fn trace_status_from_proto(value: ProtoTraceStatus) -> Option<TraceStatus> {
3522 match value {
3523 ProtoTraceStatus::Ok => Some(TraceStatus::Ok),
3524 ProtoTraceStatus::Rejected => Some(TraceStatus::Rejected),
3525 ProtoTraceStatus::Error => Some(TraceStatus::Error),
3526 ProtoTraceStatus::Unspecified => None,
3527 }
3528}
3529
3530fn operation_trace_to_proto(value: OperationTrace) -> ProtoOperationTrace {
3531 ProtoOperationTrace {
3532 trace_id: value.trace_id,
3533 correlation_id: value.correlation_id,
3534 operation: trace_operation_kind_to_proto(value.operation),
3535 transport: value.transport,
3536 backend: value.backend,
3537 admission_class: value.admission_class,
3538 tenant_id: value.tenant_id,
3539 namespace: value.namespace,
3540 principal: value.principal,
3541 store_span_id: value.store_span_id,
3542 planning_trace_id: value.planning_trace_id,
3543 started_at_unix_ms: value.started_at_unix_ms,
3544 completed_at_unix_ms: value.completed_at_unix_ms,
3545 latency_ms: value.latency_ms,
3546 status: trace_status_to_proto(value.status),
3547 status_message: value.status_message,
3548 summary: Some(ProtoOperationTraceSummary {
3549 record_id: value.summary.record_id,
3550 request_count: value.summary.request_count,
3551 query_text: value.summary.query_text,
3552 max_items: value.summary.max_items,
3553 token_budget: value.summary.token_budget,
3554 dry_run: value.summary.dry_run,
3555 }),
3556 recall_explanation: value.recall_explanation.map(recall_explanation_to_proto),
3557 }
3558}
3559
3560fn kinds_from_proto(values: Vec<String>) -> Result<Vec<MemoryRecordKind>, Status> {
3561 values
3562 .into_iter()
3563 .map(|value| record_kind_from_proto(&value))
3564 .collect()
3565}
3566
3567fn trust_levels_from_proto(values: Vec<String>) -> Result<Vec<MemoryTrustLevel>, Status> {
3568 values
3569 .into_iter()
3570 .map(|value| trust_level_from_proto(&value))
3571 .collect()
3572}
3573
3574fn quality_states_from_proto(values: Vec<String>) -> Result<Vec<MemoryQualityState>, Status> {
3575 values
3576 .into_iter()
3577 .map(|value| quality_state_from_proto(&value))
3578 .collect()
3579}
3580
3581fn conflict_states_from_proto(values: Vec<String>) -> Result<Vec<ConflictReviewState>, Status> {
3582 values
3583 .into_iter()
3584 .map(|value| conflict_review_state_from_proto(&value))
3585 .collect()
3586}
3587
3588fn resolution_kinds_from_proto(values: Vec<String>) -> Result<Vec<ConflictResolutionKind>, Status> {
3589 values
3590 .into_iter()
3591 .map(|value| conflict_resolution_kind_from_proto(&value))
3592 .collect()
3593}
3594
3595fn recall_filters_from_proto(filters: Option<ProtoRecallFilters>) -> Result<RecallFilters, Status> {
3596 let Some(filters) = filters else {
3597 return Ok(RecallFilters::default());
3598 };
3599
3600 Ok(RecallFilters {
3601 kinds: kinds_from_proto(filters.kinds)?,
3602 required_labels: filters.required_labels,
3603 source: filters.source,
3604 from_unix_ms: filters.from_unix_ms,
3605 to_unix_ms: filters.to_unix_ms,
3606 min_importance_score: filters.min_importance_score,
3607 trust_levels: trust_levels_from_proto(filters.trust_levels)?,
3608 states: quality_states_from_proto(filters.states)?,
3609 include_archived: filters.include_archived,
3610 episode_id: filters.episode_id,
3611 continuity_states: filters
3612 .continuity_states
3613 .into_iter()
3614 .map(|value| continuity_state_from_proto(&value))
3615 .collect::<Result<Vec<_>, _>>()?,
3616 unresolved_only: filters.unresolved_only,
3617 temporal_order: temporal_order_from_proto(filters.temporal_order.as_deref().unwrap_or(""))?,
3618 historical_mode: historical_mode_from_proto(
3619 filters.historical_mode.as_deref().unwrap_or(""),
3620 )?,
3621 lineage_record_id: filters.lineage_record_id,
3622 before_record_id: filters.before_record_id,
3623 after_record_id: filters.after_record_id,
3624 boundary_labels: filters.boundary_labels,
3625 recurrence_key: filters.recurrence_key,
3626 conflict_states: conflict_states_from_proto(filters.conflict_states)?,
3627 resolution_kinds: resolution_kinds_from_proto(filters.resolution_kinds)?,
3628 unresolved_conflicts_only: filters.unresolved_conflicts_only,
3629 })
3630}
3631
3632fn validate_scope_limits(scope: &MemoryScope, limits: &ServerLimits) -> Result<(), Status> {
3633 if scope.labels.len() > limits.max_labels_per_scope {
3634 return Err(invalid_argument(format!(
3635 "scope labels exceed configured max of {}",
3636 limits.max_labels_per_scope
3637 )));
3638 }
3639 Ok(())
3640}
3641
3642fn validate_record_limits(record: &MemoryRecord, limits: &ServerLimits) -> Result<(), Status> {
3643 validate_scope_limits(&record.scope, limits)?;
3644 if record.content.len() > limits.max_record_content_bytes {
3645 return Err(invalid_argument(format!(
3646 "record content exceeds configured max of {} bytes",
3647 limits.max_record_content_bytes
3648 )));
3649 }
3650 if record.summary.as_deref().map(str::len).unwrap_or(0) > limits.max_query_text_bytes {
3651 return Err(invalid_argument(format!(
3652 "record summary exceeds configured max of {} bytes",
3653 limits.max_query_text_bytes
3654 )));
3655 }
3656 Ok(())
3657}
3658
3659fn validate_recall_limits(query: &RecallQuery, limits: &ServerLimits) -> Result<(), Status> {
3660 validate_scope_limits(&query.scope, limits)?;
3661 if query.max_items == 0 {
3662 return Err(invalid_argument("recall max_items must be greater than 0"));
3663 }
3664 if query.max_items > limits.max_recall_items {
3665 return Err(invalid_argument(format!(
3666 "recall max_items exceeds configured max of {}",
3667 limits.max_recall_items
3668 )));
3669 }
3670 if query.query_text.len() > limits.max_query_text_bytes {
3671 return Err(invalid_argument(format!(
3672 "query text exceeds configured max of {} bytes",
3673 limits.max_query_text_bytes
3674 )));
3675 }
3676 Ok(())
3677}
3678
3679fn map_store_error(error: mnemara_core::Error) -> Status {
3680 match error {
3681 mnemara_core::Error::InvalidConfig(message)
3682 | mnemara_core::Error::InvalidRequest(message) => invalid_argument(message),
3683 mnemara_core::Error::Conflict(message) => Status::already_exists(message),
3684 mnemara_core::Error::Unsupported(message) => Status::unimplemented(message),
3685 mnemara_core::Error::Backend(message) => internal_status(message),
3686 }
3687}
3688
3689fn map_http_store_error(error: mnemara_core::Error) -> (StatusCode, Json<HttpErrorBody>) {
3690 let status = match error {
3691 mnemara_core::Error::InvalidConfig(_) | mnemara_core::Error::InvalidRequest(_) => {
3692 StatusCode::BAD_REQUEST
3693 }
3694 mnemara_core::Error::Conflict(_) => StatusCode::CONFLICT,
3695 mnemara_core::Error::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
3696 mnemara_core::Error::Backend(_) => StatusCode::INTERNAL_SERVER_ERROR,
3697 };
3698 (
3699 status,
3700 Json(HttpErrorBody {
3701 error: error.to_string(),
3702 }),
3703 )
3704}
3705
3706fn map_http_validation_error(status: Status) -> (StatusCode, Json<HttpErrorBody>) {
3707 (
3708 StatusCode::BAD_REQUEST,
3709 Json(HttpErrorBody {
3710 error: status.message().to_string(),
3711 }),
3712 )
3713}
3714
3715fn record_grpc_result<T>(
3716 metrics: &MethodMetrics,
3717 result: Result<Response<T>, Status>,
3718) -> Result<Response<T>, Status> {
3719 match result {
3720 Ok(response) => {
3721 metrics.record_status(&Status::ok(""));
3722 Ok(response)
3723 }
3724 Err(status) => {
3725 metrics.record_status(&status);
3726 Err(status)
3727 }
3728 }
3729}
3730
3731#[tonic::async_trait]
3732impl<S> MemoryService for GrpcMemoryService<S>
3733where
3734 S: MemoryStore + 'static,
3735{
3736 async fn upsert_memory_record(
3737 &self,
3738 request: Request<ProtoUpsertMemoryRecordRequest>,
3739 ) -> Result<Response<UpsertMemoryRecordReply>, Status> {
3740 self.runtime.metrics().grpc_upsert.record_started();
3741 validate_grpc_auth(
3742 &request,
3743 self.runtime.auth().as_ref(),
3744 AuthPermission::Write,
3745 )?;
3746 let principal = grpc_principal(&request);
3747 let started_at_unix_ms = now_unix_ms();
3748 let correlation_id = self.runtime.traces().next_id("corr");
3749 let result = async {
3750 let request = request.into_inner();
3751 let record = record_from_proto(
3752 request
3753 .record
3754 .ok_or_else(|| invalid_argument("memory record is required"))?,
3755 )?;
3756 let tenant_id = record.scope.tenant_id.clone();
3757 let namespace = record.scope.namespace.clone();
3758 validate_record_limits(&record, self.runtime.limits().as_ref())?;
3759 let _permit = self
3760 .runtime
3761 .admission()
3762 .acquire(AdmissionClass::Write, Some(&tenant_id))
3763 .await
3764 .map_err(|error| {
3765 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3766 })?;
3767 let receipt = self
3768 .store
3769 .upsert(mnemara_core::UpsertRequest {
3770 record,
3771 idempotency_key: request.idempotency_key,
3772 })
3773 .await
3774 .map_err(map_store_error)?;
3775 record_trace(
3776 &self.runtime,
3777 TraceOperationKind::Upsert,
3778 "grpc",
3779 Some(tenant_id),
3780 Some(namespace),
3781 principal,
3782 started_at_unix_ms,
3783 TraceStatus::Ok,
3784 None,
3785 OperationTraceSummary {
3786 record_id: Some(receipt.record_id.clone()),
3787 ..OperationTraceSummary::default()
3788 },
3789 None,
3790 correlation_id,
3791 );
3792
3793 Ok(Response::new(UpsertMemoryRecordReply {
3794 record_id: receipt.record_id,
3795 deduplicated: receipt.deduplicated,
3796 summary_refreshed: receipt.summary_refreshed,
3797 }))
3798 }
3799 .await;
3800 record_grpc_result(&self.runtime.metrics().grpc_upsert, result)
3801 }
3802
3803 async fn batch_upsert_memory_records(
3804 &self,
3805 request: Request<BatchUpsertMemoryRecordsRequest>,
3806 ) -> Result<Response<BatchUpsertMemoryRecordsReply>, Status> {
3807 self.runtime.metrics().grpc_batch_upsert.record_started();
3808 validate_grpc_auth(
3809 &request,
3810 self.runtime.auth().as_ref(),
3811 AuthPermission::Write,
3812 )?;
3813 let principal = grpc_principal(&request);
3814 let started_at_unix_ms = now_unix_ms();
3815 let correlation_id = self.runtime.traces().next_id("corr");
3816 let result = async {
3817 let request = request.into_inner();
3818 if request.requests.len() > self.runtime.limits().max_batch_upsert_requests {
3819 return Err(invalid_argument(format!(
3820 "batch upsert request count exceeds configured max of {}",
3821 self.runtime.limits().max_batch_upsert_requests
3822 )));
3823 }
3824 let requests = request
3825 .requests
3826 .into_iter()
3827 .map(|request| {
3828 let record = record_from_proto(
3829 request
3830 .record
3831 .ok_or_else(|| invalid_argument("memory record is required"))?,
3832 )?;
3833 validate_record_limits(&record, self.runtime.limits().as_ref())?;
3834 Ok(mnemara_core::UpsertRequest {
3835 record,
3836 idempotency_key: request.idempotency_key,
3837 })
3838 })
3839 .collect::<Result<Vec<_>, Status>>()?;
3840 let tenant_id = requests
3841 .first()
3842 .map(|item| item.record.scope.tenant_id.clone());
3843 let namespace = requests
3844 .first()
3845 .map(|item| item.record.scope.namespace.clone());
3846 let _permit = self
3847 .runtime
3848 .admission()
3849 .acquire(AdmissionClass::Write, tenant_id.as_deref())
3850 .await
3851 .map_err(|error| {
3852 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3853 })?;
3854
3855 let receipts: Vec<UpsertMemoryRecordReply> = self
3856 .store
3857 .batch_upsert(BatchUpsertRequest { requests })
3858 .await
3859 .map_err(map_store_error)?
3860 .into_iter()
3861 .map(|receipt| UpsertMemoryRecordReply {
3862 record_id: receipt.record_id,
3863 deduplicated: receipt.deduplicated,
3864 summary_refreshed: receipt.summary_refreshed,
3865 })
3866 .collect();
3867 record_trace(
3868 &self.runtime,
3869 TraceOperationKind::BatchUpsert,
3870 "grpc",
3871 tenant_id,
3872 namespace,
3873 principal,
3874 started_at_unix_ms,
3875 TraceStatus::Ok,
3876 None,
3877 OperationTraceSummary {
3878 request_count: Some(receipts.len() as u32),
3879 ..OperationTraceSummary::default()
3880 },
3881 None,
3882 correlation_id,
3883 );
3884
3885 Ok(Response::new(BatchUpsertMemoryRecordsReply { receipts }))
3886 }
3887 .await;
3888 record_grpc_result(&self.runtime.metrics().grpc_batch_upsert, result)
3889 }
3890
3891 async fn recall(
3892 &self,
3893 request: Request<ProtoRecallRequest>,
3894 ) -> Result<Response<RecallReply>, Status> {
3895 self.runtime.metrics().grpc_recall.record_started();
3896 validate_grpc_auth(&request, self.runtime.auth().as_ref(), AuthPermission::Read)?;
3897 let principal = grpc_principal(&request);
3898 let started_at_unix_ms = now_unix_ms();
3899 let correlation_id = self.runtime.traces().next_id("corr");
3900 let result = async {
3901 let request = request.into_inner();
3902 let query = RecallQuery {
3903 scope: scope_from_proto(
3904 request
3905 .scope
3906 .ok_or_else(|| invalid_argument("recall scope is required"))?,
3907 )?,
3908 query_text: request.query_text,
3909 max_items: request.max_items as usize,
3910 token_budget: request.token_budget.map(|value| value as usize),
3911 filters: recall_filters_from_proto(request.filters)?,
3912 include_explanation: request.include_explanation,
3913 };
3914 validate_recall_limits(&query, self.runtime.limits().as_ref())?;
3915 let tenant_id = query.scope.tenant_id.clone();
3916 let namespace = query.scope.namespace.clone();
3917 let query_text = query.query_text.clone();
3918 let max_items = query.max_items as u32;
3919 let token_budget = query.token_budget.map(|value| value as u32);
3920 let _permit = self
3921 .runtime
3922 .admission()
3923 .acquire(AdmissionClass::Read, Some(&tenant_id))
3924 .await
3925 .map_err(|error| {
3926 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3927 })?;
3928 let mut result = self.store.recall(query).await.map_err(map_store_error)?;
3929 attach_correlation_id(&mut result, &correlation_id);
3930 let recall_explanation = result.explanation.clone();
3931
3932 let hits = result
3933 .hits
3934 .into_iter()
3935 .map(|hit| ProtoRecallHit {
3936 record: Some(record_to_proto(hit.record)),
3937 breakdown: Some(recall_score_breakdown_to_proto(hit.breakdown)),
3938 selected_channels: hit
3939 .explanation
3940 .as_ref()
3941 .map(|value| value.selected_channels.clone())
3942 .unwrap_or_default(),
3943 policy_notes: hit
3944 .explanation
3945 .as_ref()
3946 .map(|value| value.policy_notes.clone())
3947 .unwrap_or_default(),
3948 explanation: hit.explanation.map(recall_explanation_to_proto),
3949 })
3950 .collect();
3951 record_trace(
3952 &self.runtime,
3953 TraceOperationKind::Recall,
3954 "grpc",
3955 Some(tenant_id),
3956 Some(namespace),
3957 principal,
3958 started_at_unix_ms,
3959 TraceStatus::Ok,
3960 None,
3961 OperationTraceSummary {
3962 query_text: Some(query_text),
3963 max_items: Some(max_items),
3964 token_budget,
3965 ..OperationTraceSummary::default()
3966 },
3967 recall_explanation,
3968 correlation_id,
3969 );
3970
3971 Ok(Response::new(RecallReply {
3972 hits,
3973 total_candidates_examined: result.total_candidates_examined as u32,
3974 explanation: result.explanation.map(recall_explanation_to_proto),
3975 }))
3976 }
3977 .await;
3978 record_grpc_result(&self.runtime.metrics().grpc_recall, result)
3979 }
3980
3981 async fn recall_as_of(
3982 &self,
3983 request: Request<ProtoTimeTravelRecallRequest>,
3984 ) -> Result<Response<RecallReply>, Status> {
3985 self.runtime.metrics().grpc_recall.record_started();
3986 validate_grpc_auth(&request, self.runtime.auth().as_ref(), AuthPermission::Read)?;
3987 let principal = grpc_principal(&request);
3988 let started_at_unix_ms = now_unix_ms();
3989 let correlation_id = self.runtime.traces().next_id("corr");
3990 let result = async {
3991 let request = request.into_inner();
3992 let raw_query = request
3993 .query
3994 .ok_or_else(|| invalid_argument("time-travel recall query is required"))?;
3995 let query = RecallQuery {
3996 scope: scope_from_proto(
3997 raw_query
3998 .scope
3999 .ok_or_else(|| invalid_argument("recall scope is required"))?,
4000 )?,
4001 query_text: raw_query.query_text,
4002 max_items: raw_query.max_items as usize,
4003 token_budget: raw_query.token_budget.map(|value| value as usize),
4004 filters: recall_filters_from_proto(raw_query.filters)?,
4005 include_explanation: raw_query.include_explanation,
4006 };
4007 validate_recall_limits(&query, self.runtime.limits().as_ref())?;
4008 let tenant_id = query.scope.tenant_id.clone();
4009 let namespace = query.scope.namespace.clone();
4010 let query_text = query.query_text.clone();
4011 let max_items = query.max_items as u32;
4012 let token_budget = query.token_budget.map(|value| value as u32);
4013 let _permit = self
4014 .runtime
4015 .admission()
4016 .acquire(AdmissionClass::Read, Some(&tenant_id))
4017 .await
4018 .map_err(|error| {
4019 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4020 })?;
4021 let mut result = self
4022 .store
4023 .recall_as_of(TimeTravelRecallRequest {
4024 query,
4025 as_of_unix_ms: request.as_of_unix_ms,
4026 })
4027 .await
4028 .map_err(map_store_error)?;
4029 attach_correlation_id(&mut result, &correlation_id);
4030 let recall_explanation = result.explanation.clone();
4031
4032 let hits = result
4033 .hits
4034 .into_iter()
4035 .map(|hit| ProtoRecallHit {
4036 record: Some(record_to_proto(hit.record)),
4037 breakdown: Some(recall_score_breakdown_to_proto(hit.breakdown)),
4038 selected_channels: hit
4039 .explanation
4040 .as_ref()
4041 .map(|value| value.selected_channels.clone())
4042 .unwrap_or_default(),
4043 policy_notes: hit
4044 .explanation
4045 .as_ref()
4046 .map(|value| value.policy_notes.clone())
4047 .unwrap_or_default(),
4048 explanation: hit.explanation.map(recall_explanation_to_proto),
4049 })
4050 .collect();
4051 record_trace(
4052 &self.runtime,
4053 TraceOperationKind::Recall,
4054 "grpc",
4055 Some(tenant_id),
4056 Some(namespace),
4057 principal,
4058 started_at_unix_ms,
4059 TraceStatus::Ok,
4060 None,
4061 OperationTraceSummary {
4062 query_text: Some(query_text),
4063 max_items: Some(max_items),
4064 token_budget,
4065 ..OperationTraceSummary::default()
4066 },
4067 recall_explanation,
4068 correlation_id,
4069 );
4070
4071 Ok(Response::new(RecallReply {
4072 hits,
4073 total_candidates_examined: result.total_candidates_examined as u32,
4074 explanation: result.explanation.map(recall_explanation_to_proto),
4075 }))
4076 }
4077 .await;
4078 record_grpc_result(&self.runtime.metrics().grpc_recall, result)
4079 }
4080
4081 async fn compact(
4082 &self,
4083 request: Request<ProtoCompactRequest>,
4084 ) -> Result<Response<CompactReply>, Status> {
4085 self.runtime.metrics().grpc_compact.record_started();
4086 validate_grpc_auth(
4087 &request,
4088 self.runtime.auth().as_ref(),
4089 AuthPermission::Admin,
4090 )?;
4091 let principal = grpc_principal(&request);
4092 let started_at_unix_ms = now_unix_ms();
4093 let correlation_id = self.runtime.traces().next_id("corr");
4094 let result = async {
4095 let request = request.into_inner();
4096 let tenant_id = request.tenant_id.clone();
4097 let namespace = request.namespace.clone();
4098 let _permit = self
4099 .runtime
4100 .admission()
4101 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4102 .await
4103 .map_err(|error| {
4104 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4105 })?;
4106 let report = self
4107 .store
4108 .compact(CompactionRequest {
4109 tenant_id,
4110 namespace: namespace.clone(),
4111 dry_run: request.dry_run,
4112 reason: request.reason,
4113 })
4114 .await
4115 .map_err(map_store_error)?;
4116 record_trace(
4117 &self.runtime,
4118 TraceOperationKind::Compact,
4119 "grpc",
4120 Some(request.tenant_id),
4121 namespace,
4122 principal,
4123 started_at_unix_ms,
4124 TraceStatus::Ok,
4125 None,
4126 OperationTraceSummary {
4127 dry_run: Some(request.dry_run),
4128 ..OperationTraceSummary::default()
4129 },
4130 None,
4131 correlation_id,
4132 );
4133
4134 Ok(Response::new(CompactReply {
4135 deduplicated_records: report.deduplicated_records,
4136 archived_records: report.archived_records,
4137 summarized_clusters: report.summarized_clusters,
4138 pruned_graph_edges: report.pruned_graph_edges,
4139 superseded_records: report.superseded_records,
4140 lineage_links_created: report.lineage_links_created,
4141 dry_run: report.dry_run,
4142 }))
4143 }
4144 .await;
4145 record_grpc_result(&self.runtime.metrics().grpc_compact, result)
4146 }
4147
4148 async fn synthesize(
4149 &self,
4150 request: Request<ProtoSynthesisRequest>,
4151 ) -> Result<Response<SynthesisReply>, Status> {
4152 self.runtime.metrics().grpc_synthesis.record_started();
4153 validate_grpc_auth(
4154 &request,
4155 self.runtime.auth().as_ref(),
4156 AuthPermission::Admin,
4157 )?;
4158 let principal = grpc_principal(&request);
4159 let started_at_unix_ms = now_unix_ms();
4160 let correlation_id = self.runtime.traces().next_id("corr");
4161 let result = async {
4162 let request = synthesis_request_from_proto(request.into_inner());
4163 let tenant_id = request.tenant_id.clone();
4164 let namespace = request.namespace.clone();
4165 let _permit = self
4166 .runtime
4167 .admission()
4168 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4169 .await
4170 .map_err(|error| {
4171 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4172 })?;
4173 let report = self
4174 .store
4175 .synthesize(request.clone())
4176 .await
4177 .map_err(map_store_error)?;
4178 record_trace(
4179 &self.runtime,
4180 TraceOperationKind::Synthesis,
4181 "grpc",
4182 Some(tenant_id),
4183 namespace,
4184 principal,
4185 started_at_unix_ms,
4186 TraceStatus::Ok,
4187 None,
4188 OperationTraceSummary {
4189 request_count: Some(report.proposed_records as u32),
4190 dry_run: Some(request.dry_run),
4191 ..OperationTraceSummary::default()
4192 },
4193 None,
4194 correlation_id,
4195 );
4196
4197 Ok(Response::new(synthesis_report_to_proto(report)))
4198 }
4199 .await;
4200 record_grpc_result(&self.runtime.metrics().grpc_synthesis, result)
4201 }
4202
4203 async fn snapshot(
4204 &self,
4205 request: Request<SnapshotRequest>,
4206 ) -> Result<Response<SnapshotReply>, Status> {
4207 self.runtime.metrics().grpc_snapshot.record_started();
4208 validate_grpc_auth(
4209 &request,
4210 self.runtime.auth().as_ref(),
4211 AuthPermission::Admin,
4212 )?;
4213 let principal = grpc_principal(&request);
4214 let started_at_unix_ms = now_unix_ms();
4215 let correlation_id = self.runtime.traces().next_id("corr");
4216 let result = async {
4217 let _permit = self
4218 .runtime
4219 .admission()
4220 .acquire(AdmissionClass::Admin, None)
4221 .await
4222 .map_err(|error| {
4223 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4224 })?;
4225 let manifest = self.store.snapshot().await.map_err(map_store_error)?;
4226 record_trace(
4227 &self.runtime,
4228 TraceOperationKind::Snapshot,
4229 "grpc",
4230 None,
4231 None,
4232 principal,
4233 started_at_unix_ms,
4234 TraceStatus::Ok,
4235 None,
4236 OperationTraceSummary::default(),
4237 None,
4238 correlation_id,
4239 );
4240 Ok(Response::new(SnapshotReply {
4241 snapshot_id: manifest.snapshot_id,
4242 created_at_unix_ms: manifest.created_at_unix_ms,
4243 namespaces: manifest.namespaces,
4244 record_count: manifest.record_count,
4245 storage_bytes: manifest.storage_bytes,
4246 engine: Some(engine_tuning_info_to_proto(manifest.engine)),
4247 }))
4248 }
4249 .await;
4250 record_grpc_result(&self.runtime.metrics().grpc_snapshot, result)
4251 }
4252
4253 async fn delete(
4254 &self,
4255 request: Request<ProtoDeleteRequest>,
4256 ) -> Result<Response<DeleteReply>, Status> {
4257 self.runtime.metrics().grpc_delete.record_started();
4258 validate_grpc_auth(
4259 &request,
4260 self.runtime.auth().as_ref(),
4261 AuthPermission::Admin,
4262 )?;
4263 let principal = grpc_principal(&request);
4264 let started_at_unix_ms = now_unix_ms();
4265 let correlation_id = self.runtime.traces().next_id("corr");
4266 let result = async {
4267 let request = request.into_inner();
4268 let tenant_id = request.tenant_id.clone();
4269 let namespace = request.namespace.clone();
4270 let _permit = self
4271 .runtime
4272 .admission()
4273 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4274 .await
4275 .map_err(|error| {
4276 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4277 })?;
4278 let receipt = self
4279 .store
4280 .delete(DeleteRequest {
4281 tenant_id,
4282 namespace: namespace.clone(),
4283 record_id: request.record_id,
4284 hard_delete: request.hard_delete,
4285 audit_reason: request.audit_reason,
4286 })
4287 .await
4288 .map_err(map_store_error)?;
4289 record_trace(
4290 &self.runtime,
4291 TraceOperationKind::Delete,
4292 "grpc",
4293 Some(request.tenant_id),
4294 Some(namespace),
4295 principal,
4296 started_at_unix_ms,
4297 TraceStatus::Ok,
4298 None,
4299 OperationTraceSummary {
4300 record_id: Some(receipt.record_id.clone()),
4301 ..OperationTraceSummary::default()
4302 },
4303 None,
4304 correlation_id,
4305 );
4306
4307 Ok(Response::new(DeleteReply {
4308 record_id: receipt.record_id,
4309 tombstoned: receipt.tombstoned,
4310 hard_deleted: receipt.hard_deleted,
4311 }))
4312 }
4313 .await;
4314 record_grpc_result(&self.runtime.metrics().grpc_delete, result)
4315 }
4316
4317 async fn archive(
4318 &self,
4319 request: Request<ProtoArchiveRequest>,
4320 ) -> Result<Response<ArchiveReply>, Status> {
4321 self.runtime.metrics().grpc_archive.record_started();
4322 validate_grpc_auth(
4323 &request,
4324 self.runtime.auth().as_ref(),
4325 AuthPermission::Admin,
4326 )?;
4327 let principal = grpc_principal(&request);
4328 let started_at_unix_ms = now_unix_ms();
4329 let correlation_id = self.runtime.traces().next_id("corr");
4330 let result = async {
4331 let request = request.into_inner();
4332 let tenant_id = request.tenant_id.clone();
4333 let namespace = request.namespace.clone();
4334 let _permit = self
4335 .runtime
4336 .admission()
4337 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4338 .await
4339 .map_err(|error| {
4340 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4341 })?;
4342 let receipt = self
4343 .store
4344 .archive(ArchiveRequest {
4345 tenant_id,
4346 namespace: namespace.clone(),
4347 record_id: request.record_id,
4348 dry_run: request.dry_run,
4349 audit_reason: request.audit_reason,
4350 })
4351 .await
4352 .map_err(map_store_error)?;
4353 record_trace(
4354 &self.runtime,
4355 TraceOperationKind::Archive,
4356 "grpc",
4357 Some(request.tenant_id),
4358 Some(namespace),
4359 principal,
4360 started_at_unix_ms,
4361 TraceStatus::Ok,
4362 None,
4363 OperationTraceSummary {
4364 record_id: Some(receipt.record_id.clone()),
4365 dry_run: Some(receipt.dry_run),
4366 ..OperationTraceSummary::default()
4367 },
4368 None,
4369 correlation_id,
4370 );
4371
4372 Ok(Response::new(ArchiveReply {
4373 record_id: receipt.record_id,
4374 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
4375 previous_historical_state: historical_state_to_proto(
4376 receipt.previous_historical_state,
4377 ),
4378 quality_state: quality_state_to_proto(receipt.quality_state),
4379 historical_state: historical_state_to_proto(receipt.historical_state),
4380 changed: receipt.changed,
4381 dry_run: receipt.dry_run,
4382 }))
4383 }
4384 .await;
4385 record_grpc_result(&self.runtime.metrics().grpc_archive, result)
4386 }
4387
4388 async fn suppress(
4389 &self,
4390 request: Request<ProtoSuppressRequest>,
4391 ) -> Result<Response<SuppressReply>, Status> {
4392 self.runtime.metrics().grpc_suppress.record_started();
4393 validate_grpc_auth(
4394 &request,
4395 self.runtime.auth().as_ref(),
4396 AuthPermission::Admin,
4397 )?;
4398 let principal = grpc_principal(&request);
4399 let started_at_unix_ms = now_unix_ms();
4400 let correlation_id = self.runtime.traces().next_id("corr");
4401 let result = async {
4402 let request = request.into_inner();
4403 let tenant_id = request.tenant_id.clone();
4404 let namespace = request.namespace.clone();
4405 let _permit = self
4406 .runtime
4407 .admission()
4408 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4409 .await
4410 .map_err(|error| {
4411 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4412 })?;
4413 let receipt = self
4414 .store
4415 .suppress(SuppressRequest {
4416 tenant_id,
4417 namespace: namespace.clone(),
4418 record_id: request.record_id,
4419 dry_run: request.dry_run,
4420 audit_reason: request.audit_reason,
4421 })
4422 .await
4423 .map_err(map_store_error)?;
4424 record_trace(
4425 &self.runtime,
4426 TraceOperationKind::Suppress,
4427 "grpc",
4428 Some(request.tenant_id),
4429 Some(namespace),
4430 principal,
4431 started_at_unix_ms,
4432 TraceStatus::Ok,
4433 None,
4434 OperationTraceSummary {
4435 record_id: Some(receipt.record_id.clone()),
4436 dry_run: Some(receipt.dry_run),
4437 ..OperationTraceSummary::default()
4438 },
4439 None,
4440 correlation_id,
4441 );
4442
4443 Ok(Response::new(SuppressReply {
4444 record_id: receipt.record_id,
4445 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
4446 previous_historical_state: historical_state_to_proto(
4447 receipt.previous_historical_state,
4448 ),
4449 quality_state: quality_state_to_proto(receipt.quality_state),
4450 historical_state: historical_state_to_proto(receipt.historical_state),
4451 changed: receipt.changed,
4452 dry_run: receipt.dry_run,
4453 }))
4454 }
4455 .await;
4456 record_grpc_result(&self.runtime.metrics().grpc_suppress, result)
4457 }
4458
4459 async fn recover(
4460 &self,
4461 request: Request<ProtoRecoverRequest>,
4462 ) -> Result<Response<RecoverReply>, Status> {
4463 self.runtime.metrics().grpc_recover.record_started();
4464 validate_grpc_auth(
4465 &request,
4466 self.runtime.auth().as_ref(),
4467 AuthPermission::Admin,
4468 )?;
4469 let principal = grpc_principal(&request);
4470 let started_at_unix_ms = now_unix_ms();
4471 let correlation_id = self.runtime.traces().next_id("corr");
4472 let result = async {
4473 let request = request.into_inner();
4474 let tenant_id = request.tenant_id.clone();
4475 let namespace = request.namespace.clone();
4476 let _permit = self
4477 .runtime
4478 .admission()
4479 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4480 .await
4481 .map_err(|error| {
4482 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4483 })?;
4484 let receipt = self
4485 .store
4486 .recover(RecoverRequest {
4487 tenant_id,
4488 namespace: namespace.clone(),
4489 record_id: request.record_id,
4490 dry_run: request.dry_run,
4491 audit_reason: request.audit_reason,
4492 quality_state: quality_state_from_proto(&request.quality_state)?,
4493 historical_state: request
4494 .historical_state
4495 .as_deref()
4496 .map(historical_state_from_proto)
4497 .transpose()?,
4498 })
4499 .await
4500 .map_err(map_store_error)?;
4501 record_trace(
4502 &self.runtime,
4503 TraceOperationKind::Recover,
4504 "grpc",
4505 Some(request.tenant_id),
4506 Some(namespace),
4507 principal,
4508 started_at_unix_ms,
4509 TraceStatus::Ok,
4510 None,
4511 OperationTraceSummary {
4512 record_id: Some(receipt.record_id.clone()),
4513 dry_run: Some(receipt.dry_run),
4514 ..OperationTraceSummary::default()
4515 },
4516 None,
4517 correlation_id,
4518 );
4519
4520 Ok(Response::new(RecoverReply {
4521 record_id: receipt.record_id,
4522 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
4523 previous_historical_state: historical_state_to_proto(
4524 receipt.previous_historical_state,
4525 ),
4526 quality_state: quality_state_to_proto(receipt.quality_state),
4527 historical_state: historical_state_to_proto(receipt.historical_state),
4528 changed: receipt.changed,
4529 dry_run: receipt.dry_run,
4530 }))
4531 }
4532 .await;
4533 record_grpc_result(&self.runtime.metrics().grpc_recover, result)
4534 }
4535
4536 async fn stats(
4537 &self,
4538 request: Request<ProtoStoreStatsRequest>,
4539 ) -> Result<Response<ProtoStoreStatsReply>, Status> {
4540 self.runtime.metrics().grpc_stats.record_started();
4541 validate_grpc_auth(
4542 &request,
4543 self.runtime.auth().as_ref(),
4544 AuthPermission::Admin,
4545 )?;
4546 let principal = grpc_principal(&request);
4547 let started_at_unix_ms = now_unix_ms();
4548 let correlation_id = self.runtime.traces().next_id("corr");
4549 let result = async {
4550 let request = request.into_inner();
4551 let tenant_id = request.tenant_id.clone();
4552 let namespace = request.namespace.clone();
4553 let _permit = self
4554 .runtime
4555 .admission()
4556 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4557 .await
4558 .map_err(|error| {
4559 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4560 })?;
4561 let report = self
4562 .store
4563 .stats(StoreStatsRequest {
4564 tenant_id: tenant_id.clone(),
4565 namespace: namespace.clone(),
4566 })
4567 .await
4568 .map_err(map_store_error)?;
4569 record_trace(
4570 &self.runtime,
4571 TraceOperationKind::Stats,
4572 "grpc",
4573 tenant_id,
4574 namespace,
4575 principal,
4576 started_at_unix_ms,
4577 TraceStatus::Ok,
4578 None,
4579 OperationTraceSummary::default(),
4580 None,
4581 correlation_id,
4582 );
4583
4584 Ok(Response::new(ProtoStoreStatsReply {
4585 generated_at_unix_ms: report.generated_at_unix_ms,
4586 total_records: report.total_records,
4587 storage_bytes: report.storage_bytes,
4588 namespaces: report
4589 .namespaces
4590 .into_iter()
4591 .map(namespace_stats_to_proto)
4592 .collect(),
4593 maintenance: Some(maintenance_stats_to_proto(report.maintenance)),
4594 engine: Some(engine_tuning_info_to_proto(report.engine)),
4595 }))
4596 }
4597 .await;
4598 record_grpc_result(&self.runtime.metrics().grpc_stats, result)
4599 }
4600
4601 async fn inspect_graph(
4602 &self,
4603 request: Request<ProtoGraphInspectionRequest>,
4604 ) -> Result<Response<GraphInspectionReply>, Status> {
4605 self.runtime.metrics().grpc_stats.record_started();
4606 validate_grpc_auth(
4607 &request,
4608 self.runtime.auth().as_ref(),
4609 AuthPermission::Admin,
4610 )?;
4611 let principal = grpc_principal(&request);
4612 let started_at_unix_ms = now_unix_ms();
4613 let correlation_id = self.runtime.traces().next_id("corr");
4614 let result = async {
4615 let request = request.into_inner();
4616 let tenant_id = request.tenant_id.clone();
4617 let namespace = request.namespace.clone();
4618 let _permit = self
4619 .runtime
4620 .admission()
4621 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4622 .await
4623 .map_err(|error| {
4624 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4625 })?;
4626 let report = self
4627 .store
4628 .inspect_graph(GraphInspectionRequest {
4629 tenant_id: tenant_id.clone(),
4630 namespace: namespace.clone(),
4631 actor_id: request.actor_id,
4632 conversation_id: request.conversation_id,
4633 session_id: request.session_id,
4634 include_archived: request.include_archived,
4635 include_suppressed: request.include_suppressed,
4636 include_deleted: request.include_deleted,
4637 max_nodes: request.max_nodes.map(|value| value as usize),
4638 })
4639 .await
4640 .map_err(map_store_error)?;
4641 record_trace(
4642 &self.runtime,
4643 TraceOperationKind::Stats,
4644 "grpc",
4645 tenant_id,
4646 namespace,
4647 principal,
4648 started_at_unix_ms,
4649 TraceStatus::Ok,
4650 None,
4651 OperationTraceSummary {
4652 request_count: Some(report.nodes.len() as u32),
4653 max_items: Some(report.edges.len() as u32),
4654 ..Default::default()
4655 },
4656 None,
4657 correlation_id,
4658 );
4659
4660 Ok(Response::new(graph_report_to_proto(report)))
4661 }
4662 .await;
4663 record_grpc_result(&self.runtime.metrics().grpc_stats, result)
4664 }
4665
4666 async fn integrity_check(
4667 &self,
4668 request: Request<ProtoIntegrityCheckRequest>,
4669 ) -> Result<Response<ProtoIntegrityCheckReply>, Status> {
4670 self.runtime.metrics().grpc_integrity.record_started();
4671 validate_grpc_auth(
4672 &request,
4673 self.runtime.auth().as_ref(),
4674 AuthPermission::Admin,
4675 )?;
4676 let principal = grpc_principal(&request);
4677 let started_at_unix_ms = now_unix_ms();
4678 let correlation_id = self.runtime.traces().next_id("corr");
4679 let result = async {
4680 let request = request.into_inner();
4681 let tenant_id = request.tenant_id.clone();
4682 let namespace = request.namespace.clone();
4683 let _permit = self
4684 .runtime
4685 .admission()
4686 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4687 .await
4688 .map_err(|error| {
4689 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4690 })?;
4691 let report = self
4692 .store
4693 .integrity_check(IntegrityCheckRequest {
4694 tenant_id: tenant_id.clone(),
4695 namespace: namespace.clone(),
4696 })
4697 .await
4698 .map_err(map_store_error)?;
4699 record_trace(
4700 &self.runtime,
4701 TraceOperationKind::IntegrityCheck,
4702 "grpc",
4703 tenant_id,
4704 namespace,
4705 principal,
4706 started_at_unix_ms,
4707 TraceStatus::Ok,
4708 None,
4709 OperationTraceSummary::default(),
4710 None,
4711 correlation_id,
4712 );
4713
4714 Ok(Response::new(ProtoIntegrityCheckReply {
4715 generated_at_unix_ms: report.generated_at_unix_ms,
4716 healthy: report.healthy,
4717 scanned_records: report.scanned_records,
4718 scanned_idempotency_keys: report.scanned_idempotency_keys,
4719 stale_idempotency_keys: report.stale_idempotency_keys,
4720 missing_idempotency_keys: report.missing_idempotency_keys,
4721 duplicate_active_records: report.duplicate_active_records,
4722 }))
4723 }
4724 .await;
4725 record_grpc_result(&self.runtime.metrics().grpc_integrity, result)
4726 }
4727
4728 async fn repair(
4729 &self,
4730 request: Request<ProtoRepairRequest>,
4731 ) -> Result<Response<ProtoRepairReply>, Status> {
4732 self.runtime.metrics().grpc_repair.record_started();
4733 validate_grpc_auth(
4734 &request,
4735 self.runtime.auth().as_ref(),
4736 AuthPermission::Admin,
4737 )?;
4738 let principal = grpc_principal(&request);
4739 let started_at_unix_ms = now_unix_ms();
4740 let correlation_id = self.runtime.traces().next_id("corr");
4741 let result = async {
4742 let request = request.into_inner();
4743 let tenant_id = request.tenant_id.clone();
4744 let namespace = request.namespace.clone();
4745 let _permit = self
4746 .runtime
4747 .admission()
4748 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4749 .await
4750 .map_err(|error| {
4751 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4752 })?;
4753 let report = self
4754 .store
4755 .repair(RepairRequest {
4756 tenant_id: tenant_id.clone(),
4757 namespace: namespace.clone(),
4758 dry_run: request.dry_run,
4759 reason: request.reason,
4760 remove_stale_idempotency_keys: request.remove_stale_idempotency_keys,
4761 rebuild_missing_idempotency_keys: request.rebuild_missing_idempotency_keys,
4762 })
4763 .await
4764 .map_err(map_store_error)?;
4765 record_trace(
4766 &self.runtime,
4767 TraceOperationKind::Repair,
4768 "grpc",
4769 tenant_id,
4770 namespace,
4771 principal,
4772 started_at_unix_ms,
4773 TraceStatus::Ok,
4774 None,
4775 OperationTraceSummary {
4776 dry_run: Some(request.dry_run),
4777 ..OperationTraceSummary::default()
4778 },
4779 None,
4780 correlation_id,
4781 );
4782
4783 Ok(Response::new(ProtoRepairReply {
4784 dry_run: report.dry_run,
4785 scanned_records: report.scanned_records,
4786 scanned_idempotency_keys: report.scanned_idempotency_keys,
4787 removed_stale_idempotency_keys: report.removed_stale_idempotency_keys,
4788 rebuilt_missing_idempotency_keys: report.rebuilt_missing_idempotency_keys,
4789 healthy_after: report.healthy_after,
4790 }))
4791 }
4792 .await;
4793 record_grpc_result(&self.runtime.metrics().grpc_repair, result)
4794 }
4795
4796 async fn run_maintenance(
4797 &self,
4798 request: Request<ProtoMaintenanceRunRequest>,
4799 ) -> Result<Response<MaintenanceRunReply>, Status> {
4800 self.runtime.metrics().grpc_maintenance.record_started();
4801 validate_grpc_auth(
4802 &request,
4803 self.runtime.auth().as_ref(),
4804 AuthPermission::Admin,
4805 )?;
4806 let principal = grpc_principal(&request);
4807 let started_at_unix_ms = now_unix_ms();
4808 let correlation_id = self.runtime.traces().next_id("corr");
4809 let result = async {
4810 let request = request.into_inner();
4811 let tenant_id = request.tenant_id.clone();
4812 let namespace = request.namespace.clone();
4813 let run_defaults = !request.run_integrity_check
4814 && !request.run_repair
4815 && !request.run_compaction
4816 && !request.run_synthesis;
4817 let maintenance_request = MaintenanceRunRequest {
4818 tenant_id: tenant_id.clone(),
4819 namespace: namespace.clone(),
4820 dry_run: request.dry_run,
4821 reason: request.reason,
4822 run_integrity_check: request.run_integrity_check || run_defaults,
4823 run_repair: request.run_repair || run_defaults,
4824 run_compaction: request.run_compaction || run_defaults,
4825 run_synthesis: request.run_synthesis,
4826 remove_stale_idempotency_keys: request.remove_stale_idempotency_keys,
4827 rebuild_missing_idempotency_keys: request.rebuild_missing_idempotency_keys,
4828 };
4829 let _permit = self
4830 .runtime
4831 .admission()
4832 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4833 .await
4834 .map_err(|error| {
4835 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4836 })?;
4837 let report = self
4838 .store
4839 .run_maintenance(maintenance_request)
4840 .await
4841 .map_err(map_store_error)?;
4842 record_trace(
4843 &self.runtime,
4844 TraceOperationKind::MaintenanceRun,
4845 "grpc",
4846 tenant_id,
4847 namespace,
4848 principal,
4849 started_at_unix_ms,
4850 TraceStatus::Ok,
4851 None,
4852 OperationTraceSummary {
4853 dry_run: Some(report.dry_run),
4854 ..OperationTraceSummary::default()
4855 },
4856 None,
4857 correlation_id,
4858 );
4859 Ok(Response::new(maintenance_report_to_proto(report)))
4860 }
4861 .await;
4862 record_grpc_result(&self.runtime.metrics().grpc_maintenance, result)
4863 }
4864
4865 async fn list_traces(
4866 &self,
4867 request: Request<ProtoListTracesRequest>,
4868 ) -> Result<Response<ProtoListTracesReply>, Status> {
4869 validate_grpc_auth(
4870 &request,
4871 self.runtime.auth().as_ref(),
4872 AuthPermission::Admin,
4873 )?;
4874 let request = request.into_inner();
4875 self.runtime
4876 .metrics()
4877 .trace_reads
4878 .fetch_add(1, Ordering::Relaxed);
4879 let traces = self.runtime.traces().list(&TraceListRequest {
4880 tenant_id: request.tenant_id,
4881 namespace: request.namespace,
4882 operation: ProtoTraceOperationKind::try_from(request.operation)
4883 .ok()
4884 .and_then(trace_operation_kind_from_proto),
4885 status: ProtoTraceStatus::try_from(request.status)
4886 .ok()
4887 .and_then(trace_status_from_proto),
4888 before_started_at_unix_ms: request.before_started_at_unix_ms,
4889 limit: request.limit.map(|value| value as usize),
4890 });
4891 Ok(Response::new(ProtoListTracesReply {
4892 traces: traces.into_iter().map(operation_trace_to_proto).collect(),
4893 }))
4894 }
4895
4896 async fn get_trace(
4897 &self,
4898 request: Request<ProtoGetTraceRequest>,
4899 ) -> Result<Response<ProtoOperationTrace>, Status> {
4900 validate_grpc_auth(
4901 &request,
4902 self.runtime.auth().as_ref(),
4903 AuthPermission::Admin,
4904 )?;
4905 let trace_id = request.into_inner().trace_id;
4906 self.runtime
4907 .metrics()
4908 .trace_reads
4909 .fetch_add(1, Ordering::Relaxed);
4910 let trace = self
4911 .runtime
4912 .traces()
4913 .get(&trace_id)
4914 .ok_or_else(|| Status::not_found(format!("trace {trace_id} not found")))?;
4915 Ok(Response::new(operation_trace_to_proto(trace)))
4916 }
4917
4918 async fn export(
4919 &self,
4920 request: Request<ProtoExportRequest>,
4921 ) -> Result<Response<ProtoExportReply>, Status> {
4922 validate_grpc_auth(
4923 &request,
4924 self.runtime.auth().as_ref(),
4925 AuthPermission::Admin,
4926 )?;
4927 let principal = grpc_principal(&request);
4928 let started_at_unix_ms = now_unix_ms();
4929 let correlation_id = self.runtime.traces().next_id("corr");
4930 let request = request.into_inner();
4931 let export_request = ExportRequest {
4932 tenant_id: request.tenant_id,
4933 namespace: request.namespace,
4934 include_archived: request.include_archived,
4935 };
4936 let _permit = self
4937 .runtime
4938 .admission()
4939 .acquire(AdmissionClass::Admin, export_request.tenant_id.as_deref())
4940 .await
4941 .map_err(|error| map_grpc_admission_error(self.runtime.metrics().as_ref(), error))?;
4942 let package = self
4943 .store
4944 .export(export_request.clone())
4945 .await
4946 .map_err(map_store_error)?;
4947 record_trace(
4948 &self.runtime,
4949 TraceOperationKind::Export,
4950 "grpc",
4951 export_request.tenant_id,
4952 export_request.namespace,
4953 principal,
4954 started_at_unix_ms,
4955 TraceStatus::Ok,
4956 None,
4957 OperationTraceSummary::default(),
4958 None,
4959 correlation_id,
4960 );
4961 Ok(Response::new(portable_package_to_proto(package)))
4962 }
4963
4964 async fn import(
4965 &self,
4966 request: Request<ProtoImportRequest>,
4967 ) -> Result<Response<ProtoImportReply>, Status> {
4968 validate_grpc_auth(
4969 &request,
4970 self.runtime.auth().as_ref(),
4971 AuthPermission::Admin,
4972 )?;
4973 let principal = grpc_principal(&request);
4974 let started_at_unix_ms = now_unix_ms();
4975 let correlation_id = self.runtime.traces().next_id("corr");
4976 let request = request.into_inner();
4977 let import_request = ImportRequest {
4978 package: portable_package_from_proto(
4979 request
4980 .package
4981 .ok_or_else(|| invalid_argument("portable package is required"))?,
4982 )?,
4983 mode: import_mode_from_proto(request.mode)?,
4984 dry_run: request.dry_run,
4985 };
4986 let tenant_id = import_request
4987 .package
4988 .records
4989 .first()
4990 .map(|entry| entry.record.scope.tenant_id.clone());
4991 let namespace = import_request
4992 .package
4993 .records
4994 .first()
4995 .map(|entry| entry.record.scope.namespace.clone());
4996 let _permit = self
4997 .runtime
4998 .admission()
4999 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
5000 .await
5001 .map_err(|error| map_grpc_admission_error(self.runtime.metrics().as_ref(), error))?;
5002 let report = self
5003 .store
5004 .import(import_request.clone())
5005 .await
5006 .map_err(map_store_error)?;
5007 record_trace(
5008 &self.runtime,
5009 TraceOperationKind::Import,
5010 "grpc",
5011 tenant_id,
5012 namespace,
5013 principal,
5014 started_at_unix_ms,
5015 if report.failed_records.is_empty() {
5016 TraceStatus::Ok
5017 } else {
5018 TraceStatus::Error
5019 },
5020 (!report.failed_records.is_empty())
5021 .then(|| format!("{} import validation failures", report.failed_records.len())),
5022 OperationTraceSummary {
5023 request_count: Some(import_request.package.records.len() as u32),
5024 dry_run: Some(import_request.dry_run),
5025 ..OperationTraceSummary::default()
5026 },
5027 None,
5028 correlation_id,
5029 );
5030 Ok(Response::new(ProtoImportReply {
5031 mode: request.mode,
5032 dry_run: report.dry_run,
5033 applied: report.applied,
5034 compatible_package: report.compatible_package,
5035 package_version: report.package_version,
5036 validated_records: report.validated_records,
5037 imported_records: report.imported_records,
5038 skipped_records: report.skipped_records,
5039 replaced_existing: report.replaced_existing,
5040 snapshot_id: report.snapshot_id,
5041 failed_records: report
5042 .failed_records
5043 .into_iter()
5044 .map(|failure| mnemara_protocol::v1::ImportFailure {
5045 record_id: failure.record_id,
5046 reason: failure.reason,
5047 })
5048 .collect(),
5049 }))
5050 }
5051
5052 async fn ship_snapshot(
5053 &self,
5054 request: Request<ProtoSnapshotShipRequest>,
5055 ) -> Result<Response<SnapshotShipReply>, Status> {
5056 self.runtime.metrics().grpc_snapshot_ship.record_started();
5057 validate_grpc_auth(
5058 &request,
5059 self.runtime.auth().as_ref(),
5060 AuthPermission::Admin,
5061 )?;
5062 let principal = grpc_principal(&request);
5063 let started_at_unix_ms = now_unix_ms();
5064 let correlation_id = self.runtime.traces().next_id("corr");
5065 let result = async {
5066 let request = request.into_inner();
5067 let mode = import_mode_from_proto(request.mode)?;
5068 let tenant_id = request.tenant_id.clone();
5069 let namespace = request.namespace.clone();
5070 let _permit = self
5071 .runtime
5072 .admission()
5073 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
5074 .await
5075 .map_err(|error| {
5076 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
5077 })?;
5078 let package = self
5079 .store
5080 .export(ExportRequest {
5081 tenant_id: tenant_id.clone(),
5082 namespace: namespace.clone(),
5083 include_archived: request.include_archived,
5084 })
5085 .await
5086 .map_err(map_store_error)?;
5087 let exported_records = package.records.len() as u64;
5088 let import_request = ImportRequest {
5089 package,
5090 mode,
5091 dry_run: request.dry_run,
5092 };
5093 let (remote_status, remote_report) = post_remote_import_raw(
5094 &request.target_url,
5095 request.bearer_token.as_deref(),
5096 &import_request,
5097 )
5098 .await
5099 .map_err(Status::unavailable)?;
5100 let report = SnapshotShipReport {
5101 target_url: request.target_url,
5102 exported_records,
5103 imported_records: remote_report.imported_records,
5104 skipped_records: remote_report.skipped_records,
5105 dry_run: remote_report.dry_run,
5106 compatible_package: remote_report.compatible_package,
5107 remote_status,
5108 remote_snapshot_id: Some(remote_report.snapshot_id),
5109 };
5110 record_trace(
5111 &self.runtime,
5112 TraceOperationKind::SnapshotShip,
5113 "grpc",
5114 tenant_id,
5115 namespace,
5116 principal,
5117 started_at_unix_ms,
5118 if report.compatible_package {
5119 TraceStatus::Ok
5120 } else {
5121 TraceStatus::Error
5122 },
5123 (!report.compatible_package)
5124 .then(|| "snapshot target rejected the package".to_string()),
5125 OperationTraceSummary {
5126 request_count: Some(report.exported_records as u32),
5127 dry_run: Some(report.dry_run),
5128 ..OperationTraceSummary::default()
5129 },
5130 None,
5131 correlation_id,
5132 );
5133 Ok(Response::new(snapshot_ship_report_to_proto(report)))
5134 }
5135 .await;
5136 record_grpc_result(&self.runtime.metrics().grpc_snapshot_ship, result)
5137 }
5138}