Skip to main content

meerkat_mobkit/
runtime.rs

1//! Runtime subsystem types — routing, delivery, gating, memory, scheduling, and session persistence.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs::{self, OpenOptions};
5use std::io::Write;
6use std::io::{BufRead, BufReader};
7use std::net::{TcpStream, ToSocketAddrs};
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::mpsc;
11use std::time::Duration;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use chrono::{Datelike, Offset, Timelike, Utc};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18use crate::auth::{
19    JwtClaimsValidationConfig, build_jwt_verification_key, inspect_jwt_header, parse_jwks_json,
20    parse_oidc_discovery_json, select_jwk_for_token, validate_jwt_with_verification_key,
21};
22use crate::baseline::{
23    BaselineVerificationError, BaselineVerificationReport, verify_meerkat_baseline_symbols,
24};
25use crate::decisions::{
26    AuthPolicy, AuthProvider, BigQueryNaming, ConsoleAccessRequest, ConsolePolicy,
27    DecisionPolicyError, ReleaseMetadata, RuntimeOpsPolicy, enforce_console_route_access,
28    load_trusted_mobkit_modules_from_toml, parse_release_metadata_json, validate_bigquery_naming,
29    validate_release_metadata, validate_runtime_ops_policy,
30};
31use crate::process::{ProcessBoundaryError, run_process_json_line};
32use crate::protocol::parse_unified_event_line;
33use crate::rpc::{RpcCapabilities, RpcCapabilitiesError, parse_rpc_capabilities};
34use crate::types::{
35    EventEnvelope, MobKitConfig, ModuleConfig, ModuleEvent, PreSpawnData, RestartPolicy,
36    UnifiedEvent,
37};
38
39mod bootstrap;
40mod console_ingress;
41pub mod cross_mob_control;
42pub mod cross_mob_remote;
43mod delivery;
44mod event_transport;
45mod gating;
46mod memory;
47pub mod metadata;
48mod module_boundary;
49mod routing;
50mod rpc;
51mod scheduling;
52mod session_store;
53mod supervisor;
54
55pub use bootstrap::{start_mobkit_runtime, start_mobkit_runtime_with_options};
56pub use console_ingress::{
57    ConsoleAgentLiveSnapshot, ConsoleLiveSnapshot, ConsoleMember, ConsoleModelCapabilities,
58    ConsoleRestJsonRequest, ConsoleRestJsonResponse, extract_bearer_token_from_header,
59    handle_console_rest_json_route, handle_console_rest_json_route_with_snapshot,
60    validate_console_token,
61};
62pub use event_transport::normalize_event_line;
63pub use metadata::{
64    InMemoryMetadataStore, LabelRpcResult, MetadataScope, MetadataStoreError,
65    PersistentMetadataStore, RuntimeMetadataTable, SqliteMetadataStore, dispatch_labels_delete,
66    dispatch_labels_get, dispatch_labels_set, labels_to_json_value, parse_labels_param,
67    parse_run_id_param,
68};
69pub use routing::WILDCARD_ROUTE;
70pub use routing::route_module_call;
71pub use rpc::{
72    route_module_call_rpc_json, route_module_call_rpc_subprocess,
73    run_rpc_capabilities_boundary_once,
74};
75pub use scheduling::evaluate_schedules_at_tick;
76pub use session_store::{
77    BigQueryGcConfig, BigQuerySessionStoreAdapter, BigQuerySessionStoreError, GcErrorCallback,
78    JsonFileSessionStore, JsonFileSessionStoreError, JsonStoreLockRecord, SessionPersistenceRow,
79    SessionStoreContract, SessionStoreKind, materialize_latest_session_rows,
80    materialize_live_session_rows, run_periodic_gc, run_periodic_gc_with_error_callback,
81    session_store_contracts,
82};
83pub use supervisor::{run_discovered_module_once, run_module_boundary_once};
84
85pub(crate) use scheduling::validate_schedules;
86
87use event_transport::{insert_event_sorted, merge_unified_events};
88use supervisor::supervise_module_start;
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum NormalizationError {
92    InvalidJson,
93    InvalidSchema,
94    MissingField(&'static str),
95    InvalidFieldType(&'static str),
96    SourceMismatch { expected: &'static str, got: String },
97}
98
99impl std::fmt::Display for NormalizationError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            Self::InvalidJson => write!(f, "invalid JSON"),
103            Self::InvalidSchema => write!(f, "invalid schema"),
104            Self::MissingField(field) => write!(f, "missing field: {field}"),
105            Self::InvalidFieldType(field) => write!(f, "invalid field type: {field}"),
106            Self::SourceMismatch { expected, got } => {
107                write!(f, "source mismatch: expected {expected}, got {got}")
108            }
109        }
110    }
111}
112
113impl std::error::Error for NormalizationError {}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum RuntimeBoundaryError {
117    Process(ProcessBoundaryError),
118    Normalize(NormalizationError),
119    Mcp(McpBoundaryError),
120}
121
122impl std::fmt::Display for RuntimeBoundaryError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            Self::Process(err) => write!(f, "process boundary: {err}"),
126            Self::Normalize(err) => write!(f, "normalization: {err}"),
127            Self::Mcp(err) => write!(f, "MCP boundary: {err}"),
128        }
129    }
130}
131
132impl std::error::Error for RuntimeBoundaryError {
133    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134        match self {
135            Self::Process(err) => Some(err),
136            Self::Normalize(err) => Some(err),
137            Self::Mcp(err) => Some(err),
138        }
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum McpBoundaryError {
144    RuntimeUnavailable(String),
145    McpRequired {
146        module_id: String,
147        flow: String,
148    },
149    Timeout {
150        module_id: String,
151        operation: String,
152        timeout_ms: u64,
153    },
154    ConnectionFailed {
155        module_id: String,
156        reason: String,
157    },
158    ToolListFailed {
159        module_id: String,
160        reason: String,
161    },
162    ToolNotFound {
163        module_id: String,
164        tool: String,
165        available_tools: Vec<String>,
166    },
167    ToolCallFailed {
168        module_id: String,
169        tool: String,
170        reason: String,
171    },
172    CloseFailed {
173        module_id: String,
174        reason: String,
175    },
176    OperationFailedWithCloseFailure {
177        primary: Box<McpBoundaryError>,
178        close: Box<McpBoundaryError>,
179    },
180    InvalidToolPayload {
181        module_id: String,
182        tool: String,
183        reason: String,
184    },
185    InvalidJsonResponse {
186        module_id: String,
187        tool: String,
188        response: String,
189    },
190}
191
192impl std::fmt::Display for McpBoundaryError {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        match self {
195            Self::RuntimeUnavailable(msg) => write!(f, "runtime unavailable: {msg}"),
196            Self::McpRequired { module_id, flow } => {
197                write!(f, "MCP required for module {module_id} flow {flow}")
198            }
199            Self::Timeout {
200                module_id,
201                operation,
202                timeout_ms,
203            } => {
204                write!(
205                    f,
206                    "timeout for module {module_id} operation {operation} after {timeout_ms}ms"
207                )
208            }
209            Self::ConnectionFailed { module_id, reason } => {
210                write!(f, "connection failed for module {module_id}: {reason}")
211            }
212            Self::ToolListFailed { module_id, reason } => {
213                write!(f, "tool list failed for module {module_id}: {reason}")
214            }
215            Self::ToolNotFound {
216                module_id,
217                tool,
218                available_tools,
219            } => {
220                write!(
221                    f,
222                    "tool {tool} not found for module {module_id} (available: {})",
223                    available_tools.join(", ")
224                )
225            }
226            Self::ToolCallFailed {
227                module_id,
228                tool,
229                reason,
230            } => {
231                write!(
232                    f,
233                    "tool call {tool} failed for module {module_id}: {reason}"
234                )
235            }
236            Self::CloseFailed { module_id, reason } => {
237                write!(f, "close failed for module {module_id}: {reason}")
238            }
239            Self::OperationFailedWithCloseFailure { primary, close } => {
240                write!(f, "operation failed: {primary}; close also failed: {close}")
241            }
242            Self::InvalidToolPayload {
243                module_id,
244                tool,
245                reason,
246            } => {
247                write!(
248                    f,
249                    "invalid tool payload for module {module_id} tool {tool}: {reason}"
250                )
251            }
252            Self::InvalidJsonResponse {
253                module_id,
254                tool,
255                response,
256            } => {
257                write!(
258                    f,
259                    "invalid JSON response for module {module_id} tool {tool}: {response}"
260                )
261            }
262        }
263    }
264}
265
266impl std::error::Error for McpBoundaryError {}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum ConfigResolutionError {
270    ModuleNotConfigured(String),
271    ModuleNotDiscovered(String),
272}
273
274impl std::fmt::Display for ConfigResolutionError {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self {
277            Self::ModuleNotConfigured(id) => write!(f, "module not configured: {id}"),
278            Self::ModuleNotDiscovered(id) => write!(f, "module not discovered: {id}"),
279        }
280    }
281}
282
283impl std::error::Error for ConfigResolutionError {}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum RuntimeFromConfigError {
287    Config(ConfigResolutionError),
288    Runtime(RuntimeBoundaryError),
289}
290
291impl std::fmt::Display for RuntimeFromConfigError {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        match self {
294            Self::Config(err) => write!(f, "config resolution: {err}"),
295            Self::Runtime(err) => write!(f, "runtime boundary: {err}"),
296        }
297    }
298}
299
300impl std::error::Error for RuntimeFromConfigError {
301    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
302        match self {
303            Self::Config(err) => Some(err),
304            Self::Runtime(err) => Some(err),
305        }
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum RpcRuntimeError {
311    Process(ProcessBoundaryError),
312    Capabilities(RpcCapabilitiesError),
313}
314
315impl std::fmt::Display for RpcRuntimeError {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        match self {
318            Self::Process(err) => write!(f, "process boundary: {err}"),
319            Self::Capabilities(err) => write!(f, "capabilities: {err}"),
320        }
321    }
322}
323
324impl std::error::Error for RpcRuntimeError {
325    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
326        match self {
327            Self::Process(err) => Some(err),
328            Self::Capabilities(err) => Some(err),
329        }
330    }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub enum BaselineRuntimeError {
335    Process(ProcessBoundaryError),
336    InvalidRepoPathJson,
337    MissingRepoRoot,
338    InvalidRepoRoot,
339    Baseline(BaselineVerificationError),
340}
341
342impl std::fmt::Display for BaselineRuntimeError {
343    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344        match self {
345            Self::Process(err) => write!(f, "process boundary: {err}"),
346            Self::InvalidRepoPathJson => write!(f, "invalid repo path JSON"),
347            Self::MissingRepoRoot => write!(f, "missing repo root"),
348            Self::InvalidRepoRoot => write!(f, "invalid repo root"),
349            Self::Baseline(err) => write!(f, "baseline verification: {err}"),
350        }
351    }
352}
353
354impl std::error::Error for BaselineRuntimeError {
355    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
356        match self {
357            Self::Process(err) => Some(err),
358            Self::Baseline(err) => Some(err),
359            _ => None,
360        }
361    }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
365pub enum MobkitRuntimeError {
366    Config(ConfigResolutionError),
367    MemoryBackend(ElephantMemoryStoreError),
368}
369
370impl std::fmt::Display for MobkitRuntimeError {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        match self {
373            Self::Config(err) => write!(f, "config resolution: {err}"),
374            Self::MemoryBackend(err) => write!(f, "memory backend: {err}"),
375        }
376    }
377}
378
379impl std::error::Error for MobkitRuntimeError {
380    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
381        match self {
382            Self::Config(err) => Some(err),
383            Self::MemoryBackend(err) => Some(err),
384        }
385    }
386}
387
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub enum DecisionRuntimeError {
390    Policy(DecisionPolicyError),
391}
392
393impl std::fmt::Display for DecisionRuntimeError {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        match self {
396            Self::Policy(err) => write!(f, "decision policy: {err}"),
397        }
398    }
399}
400
401impl std::error::Error for DecisionRuntimeError {
402    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
403        match self {
404            Self::Policy(err) => Some(err),
405        }
406    }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410pub struct RuntimeDecisionInputs {
411    pub bigquery: BigQueryNaming,
412    pub trusted_mobkit_toml: String,
413    pub auth: AuthPolicy,
414    pub trusted_oidc: TrustedOidcRuntimeConfig,
415    pub console: ConsolePolicy,
416    pub ops: RuntimeOpsPolicy,
417    pub release_metadata_json: String,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct RuntimeDecisionState {
422    pub bigquery: BigQueryNaming,
423    pub modules: Vec<ModuleConfig>,
424    pub auth: AuthPolicy,
425    pub trusted_oidc: TrustedOidcRuntimeConfig,
426    pub console: ConsolePolicy,
427    pub ops: RuntimeOpsPolicy,
428    pub release_metadata: ReleaseMetadata,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432pub struct TrustedOidcRuntimeConfig {
433    pub discovery_json: String,
434    pub jwks_json: String,
435    pub audience: String,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
439pub enum ElephantMemoryStoreError {
440    InvalidConfig(String),
441    Io(String),
442    Serialize(String),
443    InvalidStoreData(String),
444    ExternalCallFailed(String),
445}
446
447impl std::fmt::Display for ElephantMemoryStoreError {
448    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449        match self {
450            Self::InvalidConfig(msg) => write!(f, "invalid config: {msg}"),
451            Self::Io(msg) => write!(f, "I/O error: {msg}"),
452            Self::Serialize(msg) => write!(f, "serialization error: {msg}"),
453            Self::InvalidStoreData(msg) => write!(f, "invalid store data: {msg}"),
454            Self::ExternalCallFailed(msg) => write!(f, "external call failed: {msg}"),
455        }
456    }
457}
458
459impl std::error::Error for ElephantMemoryStoreError {}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct ElephantMemoryBackendConfig {
463    pub endpoint: String,
464    pub state_path: String,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(tag = "kind", rename_all = "snake_case")]
469pub enum MemoryBackendConfig {
470    Elephant(ElephantMemoryBackendConfig),
471}
472
473#[derive(Debug, Clone, PartialEq, Eq)]
474struct ElephantMemoryStoreAdapter {
475    endpoint: String,
476    state_path: PathBuf,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480pub struct RuntimeOptions {
481    pub on_failure_retry_budget: u32,
482    pub always_restart_budget: u32,
483    #[serde(default)]
484    pub supervisor_restart_backoff_ms: u64,
485    #[serde(default)]
486    pub supervisor_test_force_terminate_failure: bool,
487    #[serde(default)]
488    pub memory_backend: Option<MemoryBackendConfig>,
489    #[serde(default = "default_implicit_delegate_idle_retire_secs")]
490    pub implicit_delegate_idle_retire_secs: Option<u64>,
491    #[serde(default = "default_implicit_delegate_idle_sweep_interval_ms")]
492    pub implicit_delegate_idle_sweep_interval_ms: u64,
493}
494
495fn default_implicit_delegate_idle_retire_secs() -> Option<u64> {
496    Some(300)
497}
498
499fn default_implicit_delegate_idle_sweep_interval_ms() -> u64 {
500    10_000
501}
502
503impl Default for RuntimeOptions {
504    fn default() -> Self {
505        Self {
506            on_failure_retry_budget: 1,
507            always_restart_budget: 1,
508            supervisor_restart_backoff_ms: 0,
509            supervisor_test_force_terminate_failure: false,
510            memory_backend: None,
511            implicit_delegate_idle_retire_secs: default_implicit_delegate_idle_retire_secs(),
512            implicit_delegate_idle_sweep_interval_ms:
513                default_implicit_delegate_idle_sweep_interval_ms(),
514        }
515    }
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub enum LifecycleStage {
520    MobStarted,
521    ModulesStarted,
522    MergedStreamStarted,
523    ShutdownRequested,
524    ShutdownComplete,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct LifecycleEvent {
529    pub seq: u64,
530    pub stage: LifecycleStage,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub enum ModuleHealthState {
535    Starting,
536    Healthy,
537    Failed,
538    Restarting,
539    Stopped,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543pub struct ModuleHealthTransition {
544    pub module_id: String,
545    pub from: Option<ModuleHealthState>,
546    pub to: ModuleHealthState,
547    pub attempt: u32,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
551pub struct SupervisorReport {
552    pub transitions: Vec<ModuleHealthTransition>,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556pub struct RuntimeShutdownReport {
557    pub terminated_modules: Vec<String>,
558    pub orphan_processes: u32,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562pub struct ScheduleDefinition {
563    pub schedule_id: String,
564    pub interval: String,
565    pub timezone: String,
566    pub enabled: bool,
567    #[serde(default)]
568    pub jitter_ms: u64,
569    #[serde(default)]
570    pub catch_up: bool,
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct ScheduleTrigger {
575    pub schedule_id: String,
576    pub interval: String,
577    pub timezone: String,
578    pub due_tick_ms: u64,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct ScheduleEvaluation {
583    pub tick_ms: u64,
584    pub due_triggers: Vec<ScheduleTrigger>,
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
588pub struct SchedulingSupervisorSignal {
589    pub module_id: String,
590    pub latest_state: ModuleHealthState,
591    pub latest_attempt: u32,
592    pub restart_observed: bool,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596pub struct ScheduleDispatch {
597    pub claim_key: String,
598    pub schedule_id: String,
599    pub interval: String,
600    pub timezone: String,
601    pub due_tick_ms: u64,
602    pub tick_ms: u64,
603    pub event_id: String,
604    pub supervisor_signal: Option<SchedulingSupervisorSignal>,
605    #[serde(default)]
606    pub runtime_injection: Option<ScheduleRuntimeInjection>,
607    #[serde(default)]
608    pub runtime_injection_error: Option<String>,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct ScheduleRuntimeInjection {
613    pub member_id: String,
614    pub message: String,
615    pub injection_event_id: String,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct ScheduleDispatchReport {
620    pub tick_ms: u64,
621    pub due_count: usize,
622    pub dispatched: Vec<ScheduleDispatch>,
623    pub skipped_claims: Vec<String>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627pub struct RoutingResolveRequest {
628    pub recipient: String,
629    #[serde(default)]
630    pub channel: Option<String>,
631    #[serde(default)]
632    pub retry_max: Option<u32>,
633    #[serde(default)]
634    pub backoff_ms: Option<u64>,
635    #[serde(default)]
636    pub rate_limit_per_minute: Option<u32>,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640pub struct RuntimeRoute {
641    pub route_key: String,
642    pub recipient: String,
643    #[serde(default)]
644    pub channel: Option<String>,
645    pub sink: String,
646    pub target_module: String,
647    #[serde(default)]
648    pub retry_max: Option<u32>,
649    #[serde(default)]
650    pub backoff_ms: Option<u64>,
651    #[serde(default)]
652    pub rate_limit_per_minute: Option<u32>,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct RoutingResolution {
657    pub route_id: String,
658    pub recipient: String,
659    pub channel: String,
660    pub sink: String,
661    pub target_module: String,
662    pub retry_max: u32,
663    pub backoff_ms: u64,
664    pub rate_limit_per_minute: u32,
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
668pub struct DeliverySendRequest {
669    pub resolution: RoutingResolution,
670    pub payload: Value,
671    #[serde(default)]
672    pub idempotency_key: Option<String>,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676pub struct DeliveryAttempt {
677    pub attempt: u32,
678    pub status: String,
679    pub backoff_ms: u64,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683pub struct DeliveryRecord {
684    pub delivery_id: String,
685    pub route_id: String,
686    pub recipient: String,
687    pub sink: String,
688    pub target_module: String,
689    pub payload: Value,
690    pub status: String,
691    pub attempts: Vec<DeliveryAttempt>,
692    pub first_attempt_ms: u64,
693    pub final_attempt_ms: u64,
694    pub idempotency_key: Option<String>,
695    pub sink_adapter: Option<String>,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct DeliveryHistoryRequest {
700    #[serde(default)]
701    pub recipient: Option<String>,
702    #[serde(default)]
703    pub sink: Option<String>,
704    #[serde(default = "default_delivery_history_limit")]
705    pub limit: usize,
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
709pub struct DeliveryHistoryResponse {
710    pub deliveries: Vec<DeliveryRecord>,
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714pub struct MemoryStoreInfo {
715    pub store: String,
716    pub record_count: usize,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub struct MemoryIndexRequest {
721    pub entity: String,
722    pub topic: String,
723    #[serde(default)]
724    pub store: Option<String>,
725    #[serde(default)]
726    pub fact: Option<String>,
727    #[serde(default)]
728    pub metadata: Option<Value>,
729    #[serde(default)]
730    pub conflict: Option<bool>,
731    #[serde(default)]
732    pub conflict_reason: Option<String>,
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub struct MemoryAssertion {
737    pub assertion_id: String,
738    pub entity: String,
739    pub topic: String,
740    pub store: String,
741    pub fact: String,
742    #[serde(default)]
743    pub metadata: Option<Value>,
744    pub indexed_at_ms: u64,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748pub struct MemoryConflictSignal {
749    pub entity: String,
750    pub topic: String,
751    pub store: String,
752    #[serde(default)]
753    pub reason: Option<String>,
754    pub updated_at_ms: u64,
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
758pub struct MemoryIndexResult {
759    pub entity: String,
760    pub topic: String,
761    pub store: String,
762    #[serde(default)]
763    pub assertion_id: Option<String>,
764    pub conflict_active: bool,
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
768pub struct MemoryQueryRequest {
769    #[serde(default)]
770    pub entity: Option<String>,
771    #[serde(default)]
772    pub topic: Option<String>,
773    #[serde(default)]
774    pub store: Option<String>,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct MemoryQueryResult {
779    pub assertions: Vec<MemoryAssertion>,
780    pub conflicts: Vec<MemoryConflictSignal>,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
784struct PersistedMemoryState {
785    #[serde(default)]
786    assertions: Vec<MemoryAssertion>,
787    #[serde(default)]
788    conflicts: Vec<MemoryConflictSignal>,
789}
790
791#[derive(Debug, Clone, PartialEq, Eq)]
792pub enum MemoryIndexError {
793    EntityRequired,
794    TopicRequired,
795    UnsupportedStore(String),
796    FactRequiredWhenConflictUnset,
797    BackendPersistFailed(ElephantMemoryStoreError),
798}
799
800impl std::fmt::Display for MemoryIndexError {
801    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802        match self {
803            Self::EntityRequired => write!(f, "entity is required"),
804            Self::TopicRequired => write!(f, "topic is required"),
805            Self::UnsupportedStore(store) => write!(f, "unsupported store: {store}"),
806            Self::FactRequiredWhenConflictUnset => {
807                write!(f, "fact is required when conflict is unset")
808            }
809            Self::BackendPersistFailed(err) => write!(f, "backend persist failed: {err}"),
810        }
811    }
812}
813
814impl std::error::Error for MemoryIndexError {
815    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
816        match self {
817            Self::BackendPersistFailed(err) => Some(err),
818            _ => None,
819        }
820    }
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
824#[serde(rename_all = "snake_case")]
825pub enum GatingRiskTier {
826    R0,
827    R1,
828    R2,
829    R3,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833pub struct GatingEvaluateRequest {
834    pub action: String,
835    pub actor_id: String,
836    pub risk_tier: GatingRiskTier,
837    #[serde(default)]
838    pub rationale: Option<String>,
839    #[serde(default)]
840    pub requested_approver: Option<String>,
841    #[serde(default)]
842    pub approval_recipient: Option<String>,
843    #[serde(default)]
844    pub approval_channel: Option<String>,
845    #[serde(default)]
846    pub approval_timeout_ms: Option<u64>,
847    #[serde(default)]
848    pub entity: Option<String>,
849    #[serde(default)]
850    pub topic: Option<String>,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
854#[serde(rename_all = "snake_case")]
855pub enum GatingOutcome {
856    Allowed,
857    AllowedWithAudit,
858    PendingApproval,
859    SafeDraft,
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
863pub struct GatingEvaluateResult {
864    pub action_id: String,
865    pub action: String,
866    pub actor_id: String,
867    pub risk_tier: GatingRiskTier,
868    pub outcome: GatingOutcome,
869    #[serde(default)]
870    pub pending_id: Option<String>,
871    #[serde(default)]
872    pub fallback_reason: Option<String>,
873}
874
875#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
876pub struct GatingPendingEntry {
877    pub pending_id: String,
878    pub action_id: String,
879    pub action: String,
880    pub actor_id: String,
881    pub risk_tier: GatingRiskTier,
882    #[serde(default)]
883    pub requested_approver: Option<String>,
884    #[serde(default)]
885    pub approval_recipient: Option<String>,
886    #[serde(default)]
887    pub approval_channel: Option<String>,
888    #[serde(default)]
889    pub approval_route_id: Option<String>,
890    #[serde(default)]
891    pub approval_delivery_id: Option<String>,
892    pub created_at_ms: u64,
893    pub deadline_at_ms: u64,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(rename_all = "snake_case")]
898pub enum GatingDecision {
899    Approve,
900    Reject,
901    Escalate,
902}
903
904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905pub struct GatingDecideRequest {
906    pub pending_id: String,
907    pub approver_id: String,
908    pub decision: GatingDecision,
909    #[serde(default)]
910    pub reason: Option<String>,
911}
912
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914pub struct GatingDecisionResult {
915    pub pending_id: String,
916    pub action_id: String,
917    pub approver_id: String,
918    pub decision: GatingDecision,
919    pub outcome: GatingOutcome,
920    pub decided_at_ms: u64,
921    #[serde(default)]
922    pub reason: Option<String>,
923    #[serde(default)]
924    pub next_pending_id: Option<String>,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928pub struct GatingAuditEntry {
929    pub audit_id: String,
930    pub timestamp_ms: u64,
931    pub event_type: String,
932    pub action_id: String,
933    #[serde(default)]
934    pub pending_id: Option<String>,
935    pub actor_id: String,
936    pub risk_tier: GatingRiskTier,
937    pub outcome: GatingOutcome,
938    pub detail: Value,
939}
940
941#[derive(Debug, Clone, PartialEq, Eq)]
942pub enum GatingDecideError {
943    UnknownPendingId(String),
944    SelfApprovalForbidden,
945    ApproverMismatch { expected: String, provided: String },
946}
947
948impl std::fmt::Display for GatingDecideError {
949    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
950        match self {
951            Self::UnknownPendingId(id) => write!(f, "unknown pending id: {id}"),
952            Self::SelfApprovalForbidden => write!(f, "self-approval is forbidden"),
953            Self::ApproverMismatch { expected, provided } => {
954                write!(f, "approver mismatch: expected {expected}, got {provided}")
955            }
956        }
957    }
958}
959
960impl std::error::Error for GatingDecideError {}
961
962#[derive(Debug, Clone, PartialEq, Eq)]
963struct DeliveryIdempotencyEntry {
964    delivery_id: String,
965    payload: Value,
966    canonical_resolution: RoutingResolution,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq, Default)]
970struct RouterBoundaryOverrides {
971    channel: Option<String>,
972    sink: Option<String>,
973    target_module: Option<String>,
974    retry_max: Option<u32>,
975    backoff_ms: Option<u64>,
976    rate_limit_per_minute: Option<u32>,
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Default)]
980struct DeliveryBoundaryOutcome {
981    sink_adapter: Option<String>,
982    force_fail: bool,
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
986struct DeliveryRateWindowKey {
987    route_id: String,
988    recipient: String,
989    sink: String,
990    window_start_ms: u64,
991}
992
993#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
994struct MemoryConflictKey {
995    entity: String,
996    topic: String,
997    store: String,
998}
999
1000#[derive(Debug)]
1001pub struct MobkitRuntimeHandle {
1002    config: MobKitConfig,
1003    runtime_options: RuntimeOptions,
1004    loaded_modules: BTreeSet<String>,
1005    live_children: BTreeMap<String, Child>,
1006    pub lifecycle_events: Vec<LifecycleEvent>,
1007    pub supervisor_report: SupervisorReport,
1008    pub merged_events: Vec<EventEnvelope<UnifiedEvent>>,
1009    scheduling_claims: BTreeSet<String>,
1010    scheduling_claim_ticks: BTreeMap<u64, Vec<String>>,
1011    scheduling_last_due_ticks: BTreeMap<String, u64>,
1012    scheduling_dispatch_sequence: u64,
1013    routing_sequence: u64,
1014    routing_resolutions: BTreeMap<String, RoutingResolution>,
1015    routing_resolution_order: Vec<String>,
1016    runtime_routes: BTreeMap<String, RuntimeRoute>,
1017    delivery_sequence: u64,
1018    delivery_runtime_epoch_ms: u64,
1019    delivery_now_floor_ms: u64,
1020    delivery_clock_ms: u64,
1021    delivery_history: Vec<DeliveryRecord>,
1022    delivery_idempotency: BTreeMap<String, DeliveryIdempotencyEntry>,
1023    delivery_idempotency_by_delivery: BTreeMap<String, Vec<String>>,
1024    delivery_rate_window_counts: BTreeMap<DeliveryRateWindowKey, u32>,
1025    gating_sequence: u64,
1026    gating_pending: BTreeMap<String, GatingPendingEntry>,
1027    gating_pending_order: Vec<String>,
1028    gating_audit: Vec<GatingAuditEntry>,
1029    memory_sequence: u64,
1030    memory_assertions: Vec<MemoryAssertion>,
1031    memory_conflicts: BTreeMap<MemoryConflictKey, MemoryConflictSignal>,
1032    memory_backend: Option<ElephantMemoryStoreAdapter>,
1033    running: bool,
1034}
1035
1036impl MobkitRuntimeHandle {
1037    pub fn lifecycle_events(&self) -> &[LifecycleEvent] {
1038        &self.lifecycle_events
1039    }
1040
1041    pub fn supervisor_report(&self) -> &SupervisorReport {
1042        &self.supervisor_report
1043    }
1044
1045    #[doc(hidden)]
1046    pub fn inject_test_events(&mut self, events: Vec<EventEnvelope<UnifiedEvent>>) {
1047        for event in events {
1048            insert_event_sorted(&mut self.merged_events, event);
1049        }
1050    }
1051
1052    fn next_sequence(counter: &mut u64) -> u64 {
1053        let seq = *counter;
1054        *counter = counter.saturating_add(1);
1055        seq
1056    }
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Eq)]
1060pub enum ScheduleValidationError {
1061    EmptyScheduleId,
1062    DuplicateScheduleId(String),
1063    InvalidTickMs(u64),
1064    InvalidInterval {
1065        schedule_id: String,
1066        interval: String,
1067    },
1068    InvalidTimezone {
1069        schedule_id: String,
1070        timezone: String,
1071    },
1072}
1073
1074impl std::fmt::Display for ScheduleValidationError {
1075    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076        match self {
1077            Self::EmptyScheduleId => write!(f, "empty schedule id"),
1078            Self::DuplicateScheduleId(id) => write!(f, "duplicate schedule id: {id}"),
1079            Self::InvalidTickMs(ms) => write!(f, "invalid tick ms: {ms}"),
1080            Self::InvalidInterval {
1081                schedule_id,
1082                interval,
1083            } => {
1084                write!(f, "invalid interval for schedule {schedule_id}: {interval}")
1085            }
1086            Self::InvalidTimezone {
1087                schedule_id,
1088                timezone,
1089            } => {
1090                write!(f, "invalid timezone for schedule {schedule_id}: {timezone}")
1091            }
1092        }
1093    }
1094}
1095
1096impl std::error::Error for ScheduleValidationError {}
1097
1098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1099pub struct ModuleRouteRequest {
1100    pub module_id: String,
1101    pub method: String,
1102    pub params: Value,
1103}
1104
1105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1106pub struct ModuleRouteResponse {
1107    pub module_id: String,
1108    pub method: String,
1109    pub payload: Value,
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq)]
1113pub enum ModuleRouteError {
1114    UnloadedModule(String),
1115    ModuleRuntime(RuntimeBoundaryError),
1116    UnexpectedRouteResponse,
1117}
1118
1119impl std::fmt::Display for ModuleRouteError {
1120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1121        match self {
1122            Self::UnloadedModule(id) => write!(f, "unloaded module: {id}"),
1123            Self::ModuleRuntime(err) => write!(f, "module runtime: {err}"),
1124            Self::UnexpectedRouteResponse => write!(f, "unexpected route response"),
1125        }
1126    }
1127}
1128
1129impl std::error::Error for ModuleRouteError {
1130    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1131        match self {
1132            Self::ModuleRuntime(err) => Some(err),
1133            _ => None,
1134        }
1135    }
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Eq)]
1139pub enum RoutingResolveError {
1140    RouterModuleNotLoaded,
1141    DeliveryModuleNotLoaded,
1142    EmptyRecipient,
1143    InvalidChannel,
1144    InvalidRateLimitPerMinute,
1145    RetryMaxExceedsCap { provided: u32, cap: u32 },
1146    RouterBoundary(RuntimeBoundaryError),
1147}
1148
1149impl std::fmt::Display for RoutingResolveError {
1150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151        match self {
1152            Self::RouterModuleNotLoaded => write!(f, "router module not loaded"),
1153            Self::DeliveryModuleNotLoaded => write!(f, "delivery module not loaded"),
1154            Self::EmptyRecipient => write!(f, "empty recipient"),
1155            Self::InvalidChannel => write!(f, "invalid channel"),
1156            Self::InvalidRateLimitPerMinute => write!(f, "invalid rate limit per minute"),
1157            Self::RetryMaxExceedsCap { provided, cap } => {
1158                write!(f, "retry max {provided} exceeds cap {cap}")
1159            }
1160            Self::RouterBoundary(err) => write!(f, "router boundary: {err}"),
1161        }
1162    }
1163}
1164
1165impl std::error::Error for RoutingResolveError {
1166    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1167        match self {
1168            Self::RouterBoundary(err) => Some(err),
1169            _ => None,
1170        }
1171    }
1172}
1173
1174#[derive(Debug, Clone, PartialEq, Eq)]
1175pub enum DeliverySendError {
1176    DeliveryModuleNotLoaded,
1177    InvalidRouteTarget(String),
1178    InvalidRouteId,
1179    UnknownRouteId(String),
1180    ForgedResolution,
1181    InvalidRecipient,
1182    InvalidSink,
1183    InvalidIdempotencyKey,
1184    IdempotencyPayloadMismatch,
1185    RateLimited {
1186        sink: String,
1187        window_start_ms: u64,
1188        limit: u32,
1189    },
1190    DeliveryBoundary(RuntimeBoundaryError),
1191}
1192
1193impl std::fmt::Display for DeliverySendError {
1194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195        match self {
1196            Self::DeliveryModuleNotLoaded => write!(f, "delivery module not loaded"),
1197            Self::InvalidRouteTarget(target) => write!(f, "invalid route target: {target}"),
1198            Self::InvalidRouteId => write!(f, "invalid route id"),
1199            Self::UnknownRouteId(id) => write!(f, "unknown route id: {id}"),
1200            Self::ForgedResolution => write!(f, "forged resolution"),
1201            Self::InvalidRecipient => write!(f, "invalid recipient"),
1202            Self::InvalidSink => write!(f, "invalid sink"),
1203            Self::InvalidIdempotencyKey => write!(f, "invalid idempotency key"),
1204            Self::IdempotencyPayloadMismatch => write!(f, "idempotency payload mismatch"),
1205            Self::RateLimited {
1206                sink,
1207                window_start_ms,
1208                limit,
1209            } => {
1210                write!(
1211                    f,
1212                    "rate limited on sink {sink} (window {window_start_ms}ms, limit {limit})"
1213                )
1214            }
1215            Self::DeliveryBoundary(err) => write!(f, "delivery boundary: {err}"),
1216        }
1217    }
1218}
1219
1220impl std::error::Error for DeliverySendError {
1221    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1222        match self {
1223            Self::DeliveryBoundary(err) => Some(err),
1224            _ => None,
1225        }
1226    }
1227}
1228
1229#[derive(Debug, Clone, PartialEq, Eq)]
1230pub enum RuntimeRouteMutationError {
1231    EmptyRouteKey,
1232    EmptyRecipient,
1233    InvalidChannel,
1234    EmptySink,
1235    EmptyTargetModule,
1236    InvalidRateLimitPerMinute,
1237    RetryMaxExceedsCap { provided: u32, cap: u32 },
1238    RouteNotFound(String),
1239}
1240
1241impl std::fmt::Display for RuntimeRouteMutationError {
1242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1243        match self {
1244            Self::EmptyRouteKey => write!(f, "empty route key"),
1245            Self::EmptyRecipient => write!(f, "empty recipient"),
1246            Self::InvalidChannel => write!(f, "invalid channel"),
1247            Self::EmptySink => write!(f, "empty sink"),
1248            Self::EmptyTargetModule => write!(f, "empty target module"),
1249            Self::InvalidRateLimitPerMinute => write!(f, "invalid rate limit per minute"),
1250            Self::RetryMaxExceedsCap { provided, cap } => {
1251                write!(f, "retry max {provided} exceeds cap {cap}")
1252            }
1253            Self::RouteNotFound(key) => write!(f, "route not found: {key}"),
1254        }
1255    }
1256}
1257
1258impl std::error::Error for RuntimeRouteMutationError {}
1259
1260#[derive(Debug, Clone, PartialEq, Eq)]
1261pub enum RpcRouteError {
1262    InvalidRequest,
1263    BoundaryProcess(ProcessBoundaryError),
1264    Route(ModuleRouteError),
1265    InvalidResponse,
1266}
1267
1268impl std::fmt::Display for RpcRouteError {
1269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1270        match self {
1271            Self::InvalidRequest => write!(f, "invalid request"),
1272            Self::BoundaryProcess(err) => write!(f, "boundary process: {err}"),
1273            Self::Route(err) => write!(f, "route: {err}"),
1274            Self::InvalidResponse => write!(f, "invalid response"),
1275        }
1276    }
1277}
1278
1279impl std::error::Error for RpcRouteError {
1280    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1281        match self {
1282            Self::BoundaryProcess(err) => Some(err),
1283            Self::Route(err) => Some(err),
1284            _ => None,
1285        }
1286    }
1287}
1288
1289#[derive(Debug, Clone, PartialEq, Eq)]
1290pub enum RuntimeMutationError {
1291    Config(ConfigResolutionError),
1292    Runtime(RuntimeBoundaryError),
1293}
1294
1295impl std::fmt::Display for RuntimeMutationError {
1296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297        match self {
1298            Self::Config(err) => write!(f, "config resolution: {err}"),
1299            Self::Runtime(err) => write!(f, "runtime boundary: {err}"),
1300        }
1301    }
1302}
1303
1304impl std::error::Error for RuntimeMutationError {
1305    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1306        match self {
1307            Self::Config(err) => Some(err),
1308            Self::Runtime(err) => Some(err),
1309        }
1310    }
1311}
1312
1313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1314#[serde(rename_all = "snake_case")]
1315pub enum SubscribeScope {
1316    Mob,
1317    Agent,
1318    Interaction,
1319}
1320
1321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1322pub struct SubscribeRequest {
1323    pub scope: SubscribeScope,
1324    pub last_event_id: Option<String>,
1325    pub agent_id: Option<String>,
1326}
1327
1328impl Default for SubscribeRequest {
1329    fn default() -> Self {
1330        Self {
1331            scope: SubscribeScope::Mob,
1332            last_event_id: None,
1333            agent_id: None,
1334        }
1335    }
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Eq)]
1339pub enum SubscribeError {
1340    EmptyCheckpoint,
1341    UnknownCheckpoint(String),
1342    MissingAgentId,
1343    InvalidAgentId,
1344}
1345
1346impl std::fmt::Display for SubscribeError {
1347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1348        match self {
1349            Self::EmptyCheckpoint => write!(f, "empty checkpoint"),
1350            Self::UnknownCheckpoint(id) => write!(f, "unknown checkpoint: {id}"),
1351            Self::MissingAgentId => write!(f, "missing agent id"),
1352            Self::InvalidAgentId => write!(f, "invalid agent id"),
1353        }
1354    }
1355}
1356
1357impl std::error::Error for SubscribeError {}
1358
1359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1360pub struct SubscribeKeepAlive {
1361    pub interval_ms: u64,
1362    pub event: String,
1363}
1364
1365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1366pub struct SubscribeResponse {
1367    pub scope: SubscribeScope,
1368    pub replay_from_event_id: Option<String>,
1369    pub keep_alive: SubscribeKeepAlive,
1370    pub keep_alive_comment: String,
1371    pub event_frames: Vec<String>,
1372    pub events: Vec<EventEnvelope<UnifiedEvent>>,
1373}
1374
1375const SSE_KEEP_ALIVE_INTERVAL_MS: u64 = 15_000;
1376const SSE_KEEP_ALIVE_EVENT_NAME: &str = "keep-alive";
1377const SSE_KEEP_ALIVE_COMMENT_FRAME: &str = ": keep-alive\n\n";
1378const SUBSCRIBE_REPLAY_EVENT_CAP: usize = 3;
1379const SCHEDULING_CLAIM_RETENTION_WINDOW_MS: u64 = 86_400_000;
1380const SCHEDULING_CLAIMS_MAX_RETAINED: usize = 4_096;
1381const SCHEDULING_LAST_DUE_MAX_RETAINED: usize = 4_096;
1382const DELIVERY_HISTORY_LIMIT_DEFAULT: usize = 20;
1383const DELIVERY_HISTORY_LIMIT_MAX: usize = 200;
1384const ROUTING_RESOLUTION_LIMIT_MAX: usize = 512;
1385pub const ROUTING_RETRY_MAX_CAP: u32 = 10;
1386const DELIVERY_RATE_WINDOW_MS: u64 = 60_000;
1387const DELIVERY_RATE_WINDOWS_RETAINED: u64 = 2;
1388const DELIVERY_CLOCK_STEP_MS: u64 = 1_000;
1389const GATING_APPROVAL_TIMEOUT_DEFAULT_MS: u64 = 60_000;
1390const GATING_AUDIT_MAX_RETAINED: usize = 512;
1391const GATING_PENDING_MAX_RETAINED: usize = 512;
1392const MEMORY_ASSERTIONS_MAX_RETAINED: usize = 4_096;
1393const MEMORY_SUPPORTED_STORES: [&str; 5] = [
1394    "knowledge_graph",
1395    "vector",
1396    "timeline",
1397    "todo",
1398    "top_of_mind",
1399];
1400const ELEPHANT_HEALTHCHECK_TIMEOUT: Duration = Duration::from_secs(2);
1401// Multi-year bounded lookback so sparse valid cron schedules (for example leap-day)
1402// are not silently skipped when polling cadence is coarse.
1403const CRON_LOOKBACK_MINUTES: u64 = 5_270_400;
1404const CONSOLE_EXPERIENCE_ROUTE: &str = "/console/experience";
1405const CONSOLE_MODULES_ROUTE: &str = "/console/modules";
1406const EVENTS_SUBSCRIBE_METHOD: &str = "mobkit/events/subscribe";
1407
1408fn default_delivery_history_limit() -> usize {
1409    DELIVERY_HISTORY_LIMIT_DEFAULT
1410}
1411
1412pub fn run_meerkat_baseline_verification_once(
1413    command: &str,
1414    args: &[String],
1415    env: &[(String, String)],
1416    timeout: Duration,
1417) -> Result<BaselineVerificationReport, BaselineRuntimeError> {
1418    let line = run_process_json_line(command, args, env, timeout)
1419        .map_err(BaselineRuntimeError::Process)?;
1420    let value: Value =
1421        serde_json::from_str(&line).map_err(|_| BaselineRuntimeError::InvalidRepoPathJson)?;
1422    let repo = value
1423        .as_object()
1424        .and_then(|obj| obj.get("repo_root"))
1425        .ok_or(BaselineRuntimeError::MissingRepoRoot)?
1426        .as_str()
1427        .ok_or(BaselineRuntimeError::InvalidRepoRoot)?;
1428    if repo.trim().is_empty() {
1429        return Err(BaselineRuntimeError::InvalidRepoRoot);
1430    }
1431    verify_meerkat_baseline_symbols(Some(std::path::Path::new(repo)))
1432        .map_err(BaselineRuntimeError::Baseline)
1433}
1434
1435pub fn build_runtime_decision_state(
1436    input: RuntimeDecisionInputs,
1437) -> Result<RuntimeDecisionState, DecisionRuntimeError> {
1438    validate_bigquery_naming(&input.bigquery).map_err(DecisionRuntimeError::Policy)?;
1439    let modules = load_trusted_mobkit_modules_from_toml(&input.trusted_mobkit_toml)
1440        .map_err(DecisionRuntimeError::Policy)?;
1441    validate_runtime_ops_policy(&input.ops).map_err(DecisionRuntimeError::Policy)?;
1442    let release_metadata = parse_release_metadata_json(&input.release_metadata_json)
1443        .map_err(DecisionRuntimeError::Policy)?;
1444    validate_release_metadata(&release_metadata).map_err(DecisionRuntimeError::Policy)?;
1445    if input.trusted_oidc.audience.trim().is_empty() {
1446        return Err(DecisionRuntimeError::Policy(
1447            DecisionPolicyError::InvalidTrustedAuthConfig(
1448                "trusted OIDC audience must not be empty".to_string(),
1449            ),
1450        ));
1451    }
1452    parse_oidc_discovery_json(&input.trusted_oidc.discovery_json).map_err(|err| {
1453        DecisionRuntimeError::Policy(DecisionPolicyError::InvalidTrustedAuthConfig(format!(
1454            "invalid trusted OIDC discovery: {err:?}"
1455        )))
1456    })?;
1457    parse_jwks_json(&input.trusted_oidc.jwks_json).map_err(|err| {
1458        DecisionRuntimeError::Policy(DecisionPolicyError::InvalidTrustedAuthConfig(format!(
1459            "invalid trusted JWKS: {err:?}"
1460        )))
1461    })?;
1462
1463    Ok(RuntimeDecisionState {
1464        bigquery: input.bigquery,
1465        modules,
1466        auth: input.auth,
1467        trusted_oidc: input.trusted_oidc,
1468        console: input.console,
1469        ops: input.ops,
1470        release_metadata,
1471    })
1472}
1473
1474fn current_time_ms() -> u64 {
1475    SystemTime::now()
1476        .duration_since(UNIX_EPOCH)
1477        .unwrap_or_default()
1478        .as_millis() as u64
1479}