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