Skip to main content

meerkat_core/
runtime_epoch.rs

1//! Runtime epoch identity and session runtime bindings.
2//!
3//! A runtime epoch identifies a continuous async-ordering domain for a session.
4//! `SessionRuntimeBindings` bundles the epoch-local runtime facts that the
5//! factory consumes but never creates for runtime-backed surfaces.
6//!
7//! Design rule: one build consumes bindings, it does not create them.
8
9use std::any::Any;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::completion_feed::CompletionSeq;
17use crate::handles::{
18    AuthLeaseHandle, CommsDrainHandle, ExternalToolSurfaceHandle, InteractionStreamHandle,
19    McpServerLifecycleHandle, ModelRoutingHandle, PeerCommsHandle, PeerInteractionHandle,
20    SessionAdmissionHandle, SessionClaimHandle, SessionContextHandle, TurnStateHandle,
21};
22use crate::ops_lifecycle::OpsLifecycleRegistry;
23use crate::tool_scope::ToolVisibilityOwner;
24use crate::types::SessionId;
25
26/// Unique identifier for a runtime epoch (UUID v7 for time-ordering).
27///
28/// A runtime epoch identifies a continuous async-ordering domain. The same
29/// session may span multiple epochs (e.g., after reset or process restart
30/// without durable recovery). The same identity may span multiple sessions
31/// and epochs.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct RuntimeEpochId(pub Uuid);
34
35impl RuntimeEpochId {
36    /// Create a new epoch ID using UUID v7.
37    pub fn new() -> Self {
38        Self(crate::time_compat::new_uuid_v7())
39    }
40
41    /// Create from an existing UUID.
42    pub fn from_uuid(uuid: Uuid) -> Self {
43        Self(uuid)
44    }
45}
46
47impl Default for RuntimeEpochId {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl std::fmt::Display for RuntimeEpochId {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}", self.0)
56    }
57}
58
59/// Shared consumer cursor state for the epoch.
60///
61/// Written by the agent boundary and runtime loop; read by the persistence
62/// channel for snapshotting. Atomics provide lock-free monotonic updates.
63///
64/// Cursor values may be stale relative to the agent's true position when
65/// read for persistence — this is safe (stale cursors produce duplicate
66/// notices on recovery, never lost notices).
67pub struct EpochCursorState {
68    /// Agent's `applied_cursor` — advanced at the CallingLlm boundary.
69    pub agent_applied_cursor: AtomicU64,
70    /// Runtime loop's `observed_seq` — advanced after feed reads.
71    pub runtime_observed_seq: AtomicU64,
72    /// Runtime loop's `last_injected_seq` — advanced after continuation injection.
73    pub runtime_last_injected_seq: AtomicU64,
74}
75
76impl EpochCursorState {
77    /// Create fresh cursor state (all zeros).
78    pub fn new() -> Self {
79        Self {
80            agent_applied_cursor: AtomicU64::new(0),
81            runtime_observed_seq: AtomicU64::new(0),
82            runtime_last_injected_seq: AtomicU64::new(0),
83        }
84    }
85
86    /// Create from recovered persisted values.
87    pub fn from_recovered(
88        agent_applied_cursor: CompletionSeq,
89        runtime_observed_seq: CompletionSeq,
90        runtime_last_injected_seq: CompletionSeq,
91    ) -> Self {
92        Self {
93            agent_applied_cursor: AtomicU64::new(agent_applied_cursor),
94            runtime_observed_seq: AtomicU64::new(runtime_observed_seq),
95            runtime_last_injected_seq: AtomicU64::new(runtime_last_injected_seq),
96        }
97    }
98
99    /// Snapshot current cursor values for persistence.
100    pub fn snapshot(&self) -> EpochCursorSnapshot {
101        EpochCursorSnapshot {
102            agent_applied_cursor: self.agent_applied_cursor.load(Ordering::Acquire),
103            runtime_observed_seq: self.runtime_observed_seq.load(Ordering::Acquire),
104            runtime_last_injected_seq: self.runtime_last_injected_seq.load(Ordering::Acquire),
105        }
106    }
107}
108
109impl Default for EpochCursorState {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl std::fmt::Debug for EpochCursorState {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("EpochCursorState")
118            .field(
119                "agent_applied_cursor",
120                &self.agent_applied_cursor.load(Ordering::Relaxed),
121            )
122            .field(
123                "runtime_observed_seq",
124                &self.runtime_observed_seq.load(Ordering::Relaxed),
125            )
126            .field(
127                "runtime_last_injected_seq",
128                &self.runtime_last_injected_seq.load(Ordering::Relaxed),
129            )
130            .finish()
131    }
132}
133
134/// Serializable snapshot of cursor values, captured for persistence.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct EpochCursorSnapshot {
137    pub agent_applied_cursor: CompletionSeq,
138    pub runtime_observed_seq: CompletionSeq,
139    pub runtime_last_injected_seq: CompletionSeq,
140}
141
142/// Bundle of epoch-local runtime facts.
143///
144/// Created by the runtime epoch owner, consumed by the factory. The factory
145/// never creates competing registries when it receives this bundle.
146///
147/// `SessionRuntimeBindings` is opaque so downstream callers cannot assemble a
148/// fake machine-owned binding bundle. The factory validates both the session id
149/// and the runtime-private authority witness before consuming the handles.
150pub struct SessionRuntimeBindings {
151    /// Session this binding was prepared for. Factory validates this matches
152    /// the session being built.
153    session_id: SessionId,
154    /// Epoch identity — stable across rebuilds within the same epoch,
155    /// rotated on reset/restart-without-recovery.
156    epoch_id: RuntimeEpochId,
157    /// Canonical ops lifecycle registry for this epoch.
158    ops_lifecycle: Arc<dyn OpsLifecycleRegistry>,
159    /// Shared consumer cursor state for this epoch.
160    cursor_state: Arc<EpochCursorState>,
161    /// Canonical durable tool-visibility owner for this session/runtime binding.
162    tool_visibility_owner: Arc<dyn ToolVisibilityOwner>,
163    /// Turn-execution DSL handle (Phase 5F/0 addition).
164    turn_state: Arc<dyn TurnStateHandle>,
165    /// Comms drain lifecycle DSL handle (Phase 5F/0 addition).
166    comms_drain: Arc<dyn CommsDrainHandle>,
167    /// External tool surface DSL handle (Phase 5F/0 addition).
168    external_tool_surface: Arc<dyn ExternalToolSurfaceHandle>,
169    /// Peer comms classification DSL handle (Phase 5F/0 addition).
170    peer_comms: Arc<dyn PeerCommsHandle>,
171    /// Session turn-admission DSL handle (Phase 5F/0 addition).
172    session_admission: Arc<dyn SessionAdmissionHandle>,
173    /// Session model-routing baseline DSL handle.
174    ///
175    /// The factory sets this after resolving the concrete LLM identity so
176    /// runtime-backed tool resolution observes a machine-owned baseline.
177    model_routing: Arc<dyn ModelRoutingHandle>,
178    /// Auth lease lifecycle DSL handle (Phase 1.5-rev addition).
179    auth_lease: Arc<dyn AuthLeaseHandle>,
180    /// MCP server lifecycle DSL handle (Phase 5G / T5g addition).
181    ///
182    /// Routes per-server MCP handshake events into the session's MeerkatMachine
183    /// DSL (`mcp_server_states` substate) and exposes the `PendingConnect` set
184    /// to the agent loop for the `[MCP_PENDING]` system-notice toggle.
185    mcp_server_lifecycle: Arc<dyn McpServerLifecycleHandle>,
186    /// Peer interaction lifecycle DSL handle (W1-A / issue #264).
187    ///
188    /// Any session-owned runtime binding that can emit semantic peer
189    /// request/response receipts carries this handle. Standalone/embedded
190    /// builds without bindings are transport-only unless they explicitly
191    /// prepare an ephemeral machine authority and use `SessionOwned`.
192    peer_interaction: Arc<dyn PeerInteractionHandle>,
193    /// Session-context advancement DSL handle (W2-E / issue #264).
194    ///
195    /// Fires `AdvanceSessionContext` at every canonical session-truth
196    /// mutation site so the realtime projection consumer can subscribe to
197    /// a typed `SessionContextAdvanced` effect instead of polling a watch
198    /// channel. Shares the same `HandleDslAuthority` as the other handles.
199    session_context: Arc<dyn SessionContextHandle>,
200    /// Session-identity claim handle owned by the runtime (dogma #2).
201    ///
202    /// Comms runtimes built for this session acquire their typed
203    /// [`SessionClaim`] through this handle. The canonical owner lives on
204    /// `MeerkatMachine` so per-process uniqueness is enforced by the
205    /// runtime, not by process-global shell statics.
206    ///
207    /// [`SessionClaim`]: crate::handles::SessionClaim
208    session_claim_handle: Arc<dyn SessionClaimHandle>,
209    /// Interaction stream lifecycle DSL handle (U6 / dogma #5).
210    ///
211    /// Required with session-owned peer request/response semantics so stream
212    /// reservations remain a projection of machine state.
213    interaction_stream: Arc<dyn InteractionStreamHandle>,
214    runtime_authority: Arc<dyn Any + Send + Sync>,
215}
216
217impl SessionRuntimeBindings {
218    /// Construct bindings from the runtime crate after `MeerkatMachine` has
219    /// prepared the session epoch and minted its private authority witness.
220    ///
221    /// Public callers can name this function, but cannot satisfy the factory's
222    /// runtime authority check without a witness minted by `meerkat-runtime`.
223    #[doc(hidden)]
224    #[allow(clippy::too_many_arguments)]
225    pub fn __from_runtime_authority(
226        session_id: SessionId,
227        epoch_id: RuntimeEpochId,
228        ops_lifecycle: Arc<dyn OpsLifecycleRegistry>,
229        cursor_state: Arc<EpochCursorState>,
230        tool_visibility_owner: Arc<dyn ToolVisibilityOwner>,
231        turn_state: Arc<dyn TurnStateHandle>,
232        comms_drain: Arc<dyn CommsDrainHandle>,
233        external_tool_surface: Arc<dyn ExternalToolSurfaceHandle>,
234        peer_comms: Arc<dyn PeerCommsHandle>,
235        session_admission: Arc<dyn SessionAdmissionHandle>,
236        model_routing: Arc<dyn ModelRoutingHandle>,
237        auth_lease: Arc<dyn AuthLeaseHandle>,
238        mcp_server_lifecycle: Arc<dyn McpServerLifecycleHandle>,
239        peer_interaction: Arc<dyn PeerInteractionHandle>,
240        session_context: Arc<dyn SessionContextHandle>,
241        session_claim_handle: Arc<dyn SessionClaimHandle>,
242        interaction_stream: Arc<dyn InteractionStreamHandle>,
243        runtime_authority: Arc<dyn Any + Send + Sync>,
244    ) -> Self {
245        Self {
246            session_id,
247            epoch_id,
248            ops_lifecycle,
249            cursor_state,
250            tool_visibility_owner,
251            turn_state,
252            comms_drain,
253            external_tool_surface,
254            peer_comms,
255            session_admission,
256            model_routing,
257            auth_lease,
258            mcp_server_lifecycle,
259            peer_interaction,
260            session_context,
261            session_claim_handle,
262            interaction_stream,
263            runtime_authority,
264        }
265    }
266
267    pub fn session_id(&self) -> &SessionId {
268        &self.session_id
269    }
270
271    pub fn epoch_id(&self) -> &RuntimeEpochId {
272        &self.epoch_id
273    }
274
275    pub fn ops_lifecycle(&self) -> &Arc<dyn OpsLifecycleRegistry> {
276        &self.ops_lifecycle
277    }
278
279    pub fn cursor_state(&self) -> &Arc<EpochCursorState> {
280        &self.cursor_state
281    }
282
283    pub fn tool_visibility_owner(&self) -> &Arc<dyn ToolVisibilityOwner> {
284        &self.tool_visibility_owner
285    }
286
287    pub fn turn_state(&self) -> &Arc<dyn TurnStateHandle> {
288        &self.turn_state
289    }
290
291    pub fn comms_drain(&self) -> &Arc<dyn CommsDrainHandle> {
292        &self.comms_drain
293    }
294
295    pub fn external_tool_surface(&self) -> &Arc<dyn ExternalToolSurfaceHandle> {
296        &self.external_tool_surface
297    }
298
299    pub fn peer_comms(&self) -> &Arc<dyn PeerCommsHandle> {
300        &self.peer_comms
301    }
302
303    pub fn session_admission(&self) -> &Arc<dyn SessionAdmissionHandle> {
304        &self.session_admission
305    }
306
307    pub fn model_routing(&self) -> &Arc<dyn ModelRoutingHandle> {
308        &self.model_routing
309    }
310
311    pub fn auth_lease(&self) -> &Arc<dyn AuthLeaseHandle> {
312        &self.auth_lease
313    }
314
315    pub fn mcp_server_lifecycle(&self) -> &Arc<dyn McpServerLifecycleHandle> {
316        &self.mcp_server_lifecycle
317    }
318
319    pub fn peer_interaction(&self) -> &Arc<dyn PeerInteractionHandle> {
320        &self.peer_interaction
321    }
322
323    pub fn session_context(&self) -> &Arc<dyn SessionContextHandle> {
324        &self.session_context
325    }
326
327    pub fn session_claim_handle(&self) -> &Arc<dyn SessionClaimHandle> {
328        &self.session_claim_handle
329    }
330
331    pub fn interaction_stream(&self) -> &Arc<dyn InteractionStreamHandle> {
332        &self.interaction_stream
333    }
334
335    #[doc(hidden)]
336    pub fn __runtime_authority(&self) -> &(dyn Any + Send + Sync) {
337        self.runtime_authority.as_ref()
338    }
339}
340
341impl Clone for SessionRuntimeBindings {
342    fn clone(&self) -> Self {
343        Self {
344            session_id: self.session_id.clone(),
345            epoch_id: self.epoch_id.clone(),
346            ops_lifecycle: Arc::clone(&self.ops_lifecycle),
347            cursor_state: Arc::clone(&self.cursor_state),
348            tool_visibility_owner: Arc::clone(&self.tool_visibility_owner),
349            turn_state: Arc::clone(&self.turn_state),
350            comms_drain: Arc::clone(&self.comms_drain),
351            external_tool_surface: Arc::clone(&self.external_tool_surface),
352            peer_comms: Arc::clone(&self.peer_comms),
353            session_admission: Arc::clone(&self.session_admission),
354            model_routing: Arc::clone(&self.model_routing),
355            auth_lease: Arc::clone(&self.auth_lease),
356            mcp_server_lifecycle: Arc::clone(&self.mcp_server_lifecycle),
357            peer_interaction: Arc::clone(&self.peer_interaction),
358            session_context: Arc::clone(&self.session_context),
359            session_claim_handle: Arc::clone(&self.session_claim_handle),
360            interaction_stream: Arc::clone(&self.interaction_stream),
361            runtime_authority: Arc::clone(&self.runtime_authority),
362        }
363    }
364}
365
366impl std::fmt::Debug for SessionRuntimeBindings {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        f.debug_struct("SessionRuntimeBindings")
369            .field("session_id", &self.session_id)
370            .field("epoch_id", &self.epoch_id)
371            .field("ops_lifecycle", &"<dyn OpsLifecycleRegistry>")
372            .field("cursor_state", &self.cursor_state)
373            .field("tool_visibility_owner", &"<dyn ToolVisibilityOwner>")
374            .field("turn_state", &"<dyn TurnStateHandle>")
375            .field("comms_drain", &"<dyn CommsDrainHandle>")
376            .field("external_tool_surface", &"<dyn ExternalToolSurfaceHandle>")
377            .field("peer_comms", &"<dyn PeerCommsHandle>")
378            .field("session_admission", &"<dyn SessionAdmissionHandle>")
379            .field("model_routing", &"<dyn ModelRoutingHandle>")
380            .field("auth_lease", &"<dyn AuthLeaseHandle>")
381            .field("mcp_server_lifecycle", &"<dyn McpServerLifecycleHandle>")
382            .field("peer_interaction", &"<dyn PeerInteractionHandle>")
383            .field("session_context", &"<dyn SessionContextHandle>")
384            .field("session_claim_handle", &"<dyn SessionClaimHandle>")
385            .field("interaction_stream", &"<dyn InteractionStreamHandle>")
386            .finish()
387    }
388}
389
390/// Discriminant for how the factory should resolve async-operation lifecycle resources.
391///
392/// - `StandaloneEphemeral`: factory creates local-only ephemeral bindings for
393///   non-comms semantics. Comms peer request/response remains transport-only
394///   unless the host prepares explicit `SessionOwned` authority.
395/// - `SessionOwned`: factory consumes pre-created bindings from the runtime
396///   epoch owner. Never creates a competing registry.
397///
398/// The `SessionOwned` variant is intentionally large — it carries the full
399/// bundle of Arc-wrapped DSL handles and registries. Every value passes
400/// through the factory exactly once, never lands in a collection, so paying
401/// for an extra heap indirection on every construction would regress the hot
402/// path. The `StandaloneEphemeral` path only appears in WASM, tests, and
403/// standalone embedded runs that do not need semantic peer request/response
404/// authority.
405#[allow(clippy::large_enum_variant)]
406pub enum RuntimeBuildMode {
407    /// Standalone: factory creates local-only ephemeral bindings.
408    StandaloneEphemeral,
409    /// Runtime-backed: factory consumes pre-created bindings. The epoch_id
410    /// and session_id serve as identity witnesses.
411    SessionOwned(SessionRuntimeBindings),
412}
413
414impl Clone for RuntimeBuildMode {
415    fn clone(&self) -> Self {
416        match self {
417            Self::StandaloneEphemeral => Self::StandaloneEphemeral,
418            Self::SessionOwned(b) => Self::SessionOwned(b.clone()),
419        }
420    }
421}
422
423impl std::fmt::Debug for RuntimeBuildMode {
424    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425        match self {
426            Self::StandaloneEphemeral => write!(f, "StandaloneEphemeral"),
427            Self::SessionOwned(b) => f
428                .debug_tuple("SessionOwned")
429                .field(&b.session_id)
430                .field(&b.epoch_id)
431                .finish(),
432        }
433    }
434}