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::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::completion_feed::CompletionSeq;
16use crate::ops_lifecycle::OpsLifecycleRegistry;
17use crate::types::SessionId;
18
19/// Unique identifier for a runtime epoch (UUID v7 for time-ordering).
20///
21/// A runtime epoch identifies a continuous async-ordering domain. The same
22/// session may span multiple epochs (e.g., after reset or process restart
23/// without durable recovery). The same identity may span multiple sessions
24/// and epochs.
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct RuntimeEpochId(pub Uuid);
27
28impl RuntimeEpochId {
29    /// Create a new epoch ID using UUID v7.
30    pub fn new() -> Self {
31        Self(Uuid::now_v7())
32    }
33
34    /// Create from an existing UUID.
35    pub fn from_uuid(uuid: Uuid) -> Self {
36        Self(uuid)
37    }
38}
39
40impl Default for RuntimeEpochId {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl std::fmt::Display for RuntimeEpochId {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52/// Shared consumer cursor state for the epoch.
53///
54/// Written by the agent boundary and runtime loop; read by the persistence
55/// channel for snapshotting. Atomics provide lock-free monotonic updates.
56///
57/// Cursor values may be stale relative to the agent's true position when
58/// read for persistence — this is safe (stale cursors produce duplicate
59/// notices on recovery, never lost notices).
60pub struct EpochCursorState {
61    /// Agent's `applied_cursor` — advanced at the CallingLlm boundary.
62    pub agent_applied_cursor: AtomicU64,
63    /// Runtime loop's `observed_seq` — advanced after feed reads.
64    pub runtime_observed_seq: AtomicU64,
65    /// Runtime loop's `last_injected_seq` — advanced after continuation injection.
66    pub runtime_last_injected_seq: AtomicU64,
67}
68
69impl EpochCursorState {
70    /// Create fresh cursor state (all zeros).
71    pub fn new() -> Self {
72        Self {
73            agent_applied_cursor: AtomicU64::new(0),
74            runtime_observed_seq: AtomicU64::new(0),
75            runtime_last_injected_seq: AtomicU64::new(0),
76        }
77    }
78
79    /// Create from recovered persisted values.
80    pub fn from_recovered(
81        agent_applied_cursor: CompletionSeq,
82        runtime_observed_seq: CompletionSeq,
83        runtime_last_injected_seq: CompletionSeq,
84    ) -> Self {
85        Self {
86            agent_applied_cursor: AtomicU64::new(agent_applied_cursor),
87            runtime_observed_seq: AtomicU64::new(runtime_observed_seq),
88            runtime_last_injected_seq: AtomicU64::new(runtime_last_injected_seq),
89        }
90    }
91
92    /// Snapshot current cursor values for persistence.
93    pub fn snapshot(&self) -> EpochCursorSnapshot {
94        EpochCursorSnapshot {
95            agent_applied_cursor: self.agent_applied_cursor.load(Ordering::Acquire),
96            runtime_observed_seq: self.runtime_observed_seq.load(Ordering::Acquire),
97            runtime_last_injected_seq: self.runtime_last_injected_seq.load(Ordering::Acquire),
98        }
99    }
100}
101
102impl Default for EpochCursorState {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl std::fmt::Debug for EpochCursorState {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("EpochCursorState")
111            .field(
112                "agent_applied_cursor",
113                &self.agent_applied_cursor.load(Ordering::Relaxed),
114            )
115            .field(
116                "runtime_observed_seq",
117                &self.runtime_observed_seq.load(Ordering::Relaxed),
118            )
119            .field(
120                "runtime_last_injected_seq",
121                &self.runtime_last_injected_seq.load(Ordering::Relaxed),
122            )
123            .finish()
124    }
125}
126
127/// Serializable snapshot of cursor values, captured for persistence.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct EpochCursorSnapshot {
130    pub agent_applied_cursor: CompletionSeq,
131    pub runtime_observed_seq: CompletionSeq,
132    pub runtime_last_injected_seq: CompletionSeq,
133}
134
135/// Bundle of epoch-local runtime facts.
136///
137/// Created by the runtime epoch owner ([`RuntimeSessionAdapter::prepare_bindings`]),
138/// consumed by the factory. The factory never creates competing registries
139/// when it receives this bundle.
140///
141/// The `session_id` field acts as an identity witness: the factory validates
142/// that `bindings.session_id == session.id()` to catch cross-wired bindings.
143pub struct SessionRuntimeBindings {
144    /// Session this binding was prepared for. Factory validates this matches
145    /// the session being built.
146    pub session_id: SessionId,
147    /// Epoch identity — stable across rebuilds within the same epoch,
148    /// rotated on reset/restart-without-recovery.
149    pub epoch_id: RuntimeEpochId,
150    /// Canonical ops lifecycle registry for this epoch.
151    pub ops_lifecycle: Arc<dyn OpsLifecycleRegistry>,
152    /// Shared consumer cursor state for this epoch.
153    pub cursor_state: Arc<EpochCursorState>,
154}
155
156impl Clone for SessionRuntimeBindings {
157    fn clone(&self) -> Self {
158        Self {
159            session_id: self.session_id.clone(),
160            epoch_id: self.epoch_id.clone(),
161            ops_lifecycle: Arc::clone(&self.ops_lifecycle),
162            cursor_state: Arc::clone(&self.cursor_state),
163        }
164    }
165}
166
167impl std::fmt::Debug for SessionRuntimeBindings {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("SessionRuntimeBindings")
170            .field("session_id", &self.session_id)
171            .field("epoch_id", &self.epoch_id)
172            .field("ops_lifecycle", &"<dyn OpsLifecycleRegistry>")
173            .field("cursor_state", &self.cursor_state)
174            .finish()
175    }
176}
177
178/// Discriminant for how the factory should resolve async-operation lifecycle resources.
179///
180/// - `StandaloneEphemeral`: factory creates local-only ephemeral bindings.
181///   Suitable for WASM, tests, embedded, and doc examples.
182/// - `SessionOwned`: factory consumes pre-created bindings from the runtime
183///   epoch owner. Never creates a competing registry.
184pub enum RuntimeBuildMode {
185    /// Standalone: factory creates local-only ephemeral bindings.
186    StandaloneEphemeral,
187    /// Runtime-backed: factory consumes pre-created bindings. The epoch_id
188    /// and session_id serve as identity witnesses.
189    SessionOwned(SessionRuntimeBindings),
190}
191
192impl Clone for RuntimeBuildMode {
193    fn clone(&self) -> Self {
194        match self {
195            Self::StandaloneEphemeral => Self::StandaloneEphemeral,
196            Self::SessionOwned(b) => Self::SessionOwned(b.clone()),
197        }
198    }
199}
200
201impl std::fmt::Debug for RuntimeBuildMode {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            Self::StandaloneEphemeral => write!(f, "StandaloneEphemeral"),
205            Self::SessionOwned(b) => f
206                .debug_tuple("SessionOwned")
207                .field(&b.session_id)
208                .field(&b.epoch_id)
209                .finish(),
210        }
211    }
212}