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