Skip to main content

meerkat_mobkit/runtime/
bootstrap.rs

1//! Runtime bootstrap — config resolution, module startup, and event loop initialization.
2
3use super::*;
4
5pub fn start_mobkit_runtime(
6    config: MobKitConfig,
7    agent_events: Vec<EventEnvelope<UnifiedEvent>>,
8    timeout: Duration,
9) -> Result<MobkitRuntimeHandle, MobkitRuntimeError> {
10    start_mobkit_runtime_with_options(config, agent_events, timeout, RuntimeOptions::default())
11}
12
13pub fn start_mobkit_runtime_with_options(
14    config: MobKitConfig,
15    agent_events: Vec<EventEnvelope<UnifiedEvent>>,
16    timeout: Duration,
17    options: RuntimeOptions,
18) -> Result<MobkitRuntimeHandle, MobkitRuntimeError> {
19    let delivery_runtime_epoch_ms = current_time_ms();
20    let mut lifecycle_events = Vec::new();
21    let mut seq = 0_u64;
22    lifecycle_events.push(LifecycleEvent {
23        seq,
24        stage: LifecycleStage::MobStarted,
25    });
26    seq += 1;
27
28    let mut supervisor_transitions = Vec::new();
29    let mut module_events = Vec::new();
30    let mut loaded_modules = BTreeSet::new();
31    let mut live_children = BTreeMap::new();
32
33    for module_id in &config.discovery.modules {
34        let module = config
35            .modules
36            .iter()
37            .find(|module| &module.id == module_id)
38            .ok_or_else(|| {
39                MobkitRuntimeError::Config(ConfigResolutionError::ModuleNotConfigured(
40                    module_id.clone(),
41                ))
42            })?;
43
44        let pre_spawn = config
45            .pre_spawn
46            .iter()
47            .find(|data| data.module_id == *module_id);
48
49        let mut start_result = supervise_module_start(module, pre_spawn, timeout, &options);
50        supervisor_transitions.append(&mut start_result.transitions);
51        if let Some(error) = start_result.terminal_error.as_ref() {
52            let timestamp_ms = current_time_ms();
53            module_events.push(EventEnvelope {
54                event_id: format!("evt-supervisor-warning-{}-{timestamp_ms}", module.id),
55                source: "module".to_string(),
56                timestamp_ms,
57                event: UnifiedEvent::Module(ModuleEvent {
58                    module: module.id.clone(),
59                    event_type: "supervisor.warning".to_string(),
60                    payload: serde_json::json!({
61                        "error": format!("{error:?}")
62                    }),
63                }),
64            });
65        }
66        if let Some(event) = start_result.event {
67            loaded_modules.insert(module_id.clone());
68            if let Some(child) = start_result.child {
69                live_children.insert(module_id.clone(), child);
70            }
71            module_events.push(event);
72        }
73    }
74
75    lifecycle_events.push(LifecycleEvent {
76        seq,
77        stage: LifecycleStage::ModulesStarted,
78    });
79    seq += 1;
80
81    let merged_events = merge_unified_events(module_events, agent_events);
82    lifecycle_events.push(LifecycleEvent {
83        seq,
84        stage: LifecycleStage::MergedStreamStarted,
85    });
86
87    let memory_backend = match options.memory_backend.as_ref() {
88        Some(MemoryBackendConfig::LocalJson(config)) => Some(
89            LocalJsonMemoryStoreAdapter::from_config(config)
90                .map_err(MobkitRuntimeError::MemoryBackend)?,
91        ),
92        Some(MemoryBackendConfig::Elephant(legacy)) => {
93            tracing::warn!(
94                "memory backend kind 'elephant' is deprecated: it only health-checks the \
95                 endpoint and persists the ledger as local JSON; use kind 'local_json' with \
96                 an optional health_check_endpoint"
97            );
98            Some(
99                LocalJsonMemoryStoreAdapter::from_config(&LocalJsonMemoryBackendConfig::from(
100                    legacy.clone(),
101                ))
102                .map_err(MobkitRuntimeError::MemoryBackend)?,
103            )
104        }
105        None => None,
106    };
107    let persisted_memory = match memory_backend.as_ref() {
108        Some(backend) => backend
109            .read_state()
110            .map_err(MobkitRuntimeError::MemoryBackend)?,
111        None => PersistedMemoryState::default(),
112    };
113    let mut memory_assertions = persisted_memory
114        .assertions
115        .into_iter()
116        .filter_map(|assertion| {
117            let entity = MobkitRuntimeHandle::canonical_memory_token(&assertion.entity)?;
118            let topic = MobkitRuntimeHandle::canonical_memory_token(&assertion.topic)?;
119            let store = MobkitRuntimeHandle::canonical_memory_store(&assertion.store)?;
120            let fact = assertion.fact.trim();
121            if fact.is_empty() {
122                return None;
123            }
124            Some(MemoryAssertion {
125                assertion_id: assertion.assertion_id,
126                entity,
127                topic,
128                store,
129                fact: fact.to_string(),
130                metadata: assertion.metadata,
131                indexed_at_ms: assertion.indexed_at_ms,
132            })
133        })
134        .collect::<Vec<_>>();
135    while memory_assertions.len() > MEMORY_ASSERTIONS_MAX_RETAINED {
136        memory_assertions.remove(0);
137    }
138    let mut memory_conflicts = BTreeMap::new();
139    for signal in persisted_memory.conflicts {
140        let Some(entity) = MobkitRuntimeHandle::canonical_memory_token(&signal.entity) else {
141            continue;
142        };
143        let Some(topic) = MobkitRuntimeHandle::canonical_memory_token(&signal.topic) else {
144            continue;
145        };
146        let Some(store) = MobkitRuntimeHandle::canonical_memory_store(&signal.store) else {
147            continue;
148        };
149        let normalized_signal = MemoryConflictSignal {
150            entity: entity.clone(),
151            topic: topic.clone(),
152            store: store.clone(),
153            reason: signal
154                .reason
155                .as_deref()
156                .map(str::trim)
157                .filter(|value| !value.is_empty())
158                .map(ToString::to_string),
159            updated_at_ms: signal.updated_at_ms,
160        };
161        memory_conflicts.insert(
162            MemoryConflictKey {
163                entity,
164                topic,
165                store,
166            },
167            normalized_signal,
168        );
169    }
170    let memory_sequence = memory_assertions
171        .iter()
172        .filter_map(|assertion| parse_memory_assertion_sequence(&assertion.assertion_id))
173        .max()
174        .map(|last_sequence| last_sequence.saturating_add(1))
175        .unwrap_or(memory_assertions.len() as u64);
176
177    Ok(MobkitRuntimeHandle {
178        config,
179        runtime_options: options,
180        loaded_modules,
181        live_children,
182        lifecycle_events,
183        supervisor_report: SupervisorReport {
184            transitions: supervisor_transitions,
185        },
186        merged_events,
187        scheduling_claims: BTreeSet::new(),
188        scheduling_claim_ticks: BTreeMap::new(),
189        scheduling_last_due_ticks: BTreeMap::new(),
190        scheduling_dispatch_sequence: 0,
191        routing_sequence: 0,
192        routing_resolutions: BTreeMap::new(),
193        routing_resolution_order: Vec::new(),
194        runtime_routes: BTreeMap::new(),
195        delivery_sequence: 0,
196        delivery_runtime_epoch_ms,
197        delivery_now_floor_ms: 0,
198        delivery_clock_ms: 0,
199        delivery_history: Vec::new(),
200        delivery_idempotency: BTreeMap::new(),
201        delivery_idempotency_by_delivery: BTreeMap::new(),
202        delivery_rate_window_counts: BTreeMap::new(),
203        gating_sequence: 0,
204        gating_pending: BTreeMap::new(),
205        gating_pending_order: Vec::new(),
206        gating_audit: Vec::new(),
207        gating_resolution_observers: GatingResolutionObservers::default(),
208        memory_sequence,
209        memory_assertions,
210        memory_conflicts,
211        memory_backend,
212        running: true,
213    })
214}
215
216fn parse_memory_assertion_sequence(assertion_id: &str) -> Option<u64> {
217    assertion_id
218        .strip_prefix("memory-assert-")
219        .and_then(|suffix| suffix.parse::<u64>().ok())
220}