Skip to main content

mnemara_server/
lib.rs

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