Skip to main content

liminal_server/agents/
waiting_state.rs

1//! Atomic agent target journal and the prepared-operation ledger.
2
3use super::WaitingRecord;
4use crate::agents::registry::REGISTRY_STREAM;
5use crate::auth_lys::live::drafts::{ApprovalTransition, DraftError, execution_digest};
6use haematite::tree::Hash;
7use liminal::durability::{
8    DurabilityError, DurableStore, RoutedBatchItem, ScanCursor, bridge::block_on,
9};
10use lys_pass::drafts::{ApprovedDraft, DraftRequest, Execution};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::BTreeMap;
14use std::sync::{Arc, Mutex, MutexGuard};
15
16const ROUTE: &str = "liminal/approvals/v2";
17const KEY: &[u8] = b"snapshot";
18
19pub use crate::auth_lys::live::drafts::JournalLimits;
20
21#[derive(Clone, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct Prepared {
24    pub holder: String,
25    pub agent: Option<String>,
26    pub request: DraftRequest,
27    pub at_ms: u64,
28    pub mode: crate::auth_lys::GrantMode,
29    pub draft: Option<String>,
30}
31#[derive(Clone, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Outcome {
34    pub draft: Option<ApprovedDraft>,
35    pub approval: ApprovalTransition,
36    pub receipt: Execution,
37    pub acknowledged: bool,
38    pub delivered: bool,
39}
40#[derive(Clone, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct TargetRow {
43    pub(crate) timestamp: u64,
44    pub(crate) target: Value,
45    approval: Option<ApprovalTransition>,
46}
47#[derive(Clone, Default, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct Snapshot {
50    pub version: u8,
51    pub rows: Vec<TargetRow>,
52    pub prepared: BTreeMap<String, Prepared>,
53    pub outcomes: BTreeMap<String, Outcome>,
54    pub waiting: BTreeMap<String, WaitingRecord>,
55    pub deliveries: BTreeMap<String, super::Delivery>,
56}
57use super::super::pulls::Kept;
58
59/// Installed before the agent registry opens; native projections read the same
60/// target rows whose atomic replacement retains approval, receipt and outbox.
61pub struct DraftJournal {
62    pub(crate) store: Arc<dyn DurableStore>,
63    limits: JournalLimits,
64    kept: Mutex<Kept>,
65}
66impl std::fmt::Debug for DraftJournal {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("DraftJournal").finish_non_exhaustive()
69    }
70}
71impl DraftJournal {
72    /// Migrates the complete old target log in one durable replacement. Once a
73    /// snapshot exists, the old stream is never consulted or written again.
74    ///
75    /// # Errors
76    /// Oversized/corrupt history, failed migration or uncertain storage.
77    pub fn open(
78        store: Arc<dyn DurableStore>,
79        limits: JournalLimits,
80    ) -> Result<Arc<Self>, DraftError> {
81        let bytes = block_on(store.get_routed(ROUTE, KEY))
82            .map_err(|_| DraftError::Store)?
83            .map_err(|_| DraftError::Store)?;
84        let mut snapshot = Snapshot {
85            version: 2,
86            ..Snapshot::default()
87        };
88        if let Some(bytes) = &bytes {
89            if bytes.len() > limits.snapshot_bytes.get() {
90                return Err(DraftError::Bound);
91            }
92            snapshot = serde_json::from_slice(bytes).map_err(|_| DraftError::Store)?;
93            if snapshot.version != 2 {
94                return Err(DraftError::Contract);
95            }
96        } else {
97            Self::migrate(store.as_ref(), limits, &mut snapshot)?;
98        }
99        let journal = Arc::new(Self {
100            store,
101            limits,
102            kept: Mutex::new(Kept {
103                snapshot,
104                hash: bytes.as_deref().map(Hash::of),
105                uncertain: false,
106                active: None,
107                pending: None,
108            }),
109        });
110        journal.validate(&journal.lock()?.snapshot)?;
111        if bytes.is_none() {
112            journal.update(|_| Ok(()))?;
113        }
114        Ok(journal)
115    }
116    fn migrate(
117        store: &dyn DurableStore,
118        limits: JournalLimits,
119        snapshot: &mut Snapshot,
120    ) -> Result<(), DraftError> {
121        let head = block_on(store.read_stream_next_seq(REGISTRY_STREAM))
122            .map_err(|_| DraftError::Store)?
123            .map_err(|_| DraftError::Store)?
124            .unwrap_or(0);
125        if head > u64::try_from(limits.rows.get()).map_err(|_| DraftError::Bound)? {
126            return Err(DraftError::Bound);
127        }
128        let mut cursor = Some(ScanCursor {
129            stream_key: REGISTRY_STREAM.into(),
130            next_sequence: 0,
131        });
132        let mut next = 0;
133        let mut imported_bytes: usize = 0;
134        while next < head {
135            let page =
136                block_on(store.scan_page(REGISTRY_STREAM, cursor, limits.replay_page_rows.get()))
137                    .map_err(|_| DraftError::Store)?
138                    .map_err(|_| DraftError::Store)?;
139            if page.entries.is_empty() {
140                return Err(DraftError::Contract);
141            }
142            cursor = page.next;
143            for row in page.entries {
144                imported_bytes = imported_bytes
145                    .checked_add(row.payload.len())
146                    .ok_or(DraftError::Bound)?;
147                if imported_bytes > limits.snapshot_bytes.get() {
148                    return Err(DraftError::Bound);
149                }
150                if row.sequence != next || next >= head {
151                    return Err(DraftError::Contract);
152                }
153                let target: Value =
154                    serde_json::from_slice(&row.payload).map_err(|_| DraftError::Store)?;
155                let approval = Self::legacy_approval(&target)?;
156                if let Some(approval) = &approval {
157                    let receipt: Execution =
158                        serde_json::from_value(target["receipt"]["execution"].clone())
159                            .map_err(|_| DraftError::Store)?;
160                    if snapshot
161                        .outcomes
162                        .insert(
163                            approval.draft.clone(),
164                            Outcome {
165                                draft: None,
166                                approval: approval.clone(),
167                                receipt,
168                                acknowledged: false,
169                                delivered: false,
170                            },
171                        )
172                        .is_some()
173                    {
174                        return Err(DraftError::Contract);
175                    }
176                }
177                snapshot.rows.push(TargetRow {
178                    timestamp: row.timestamp,
179                    target,
180                    approval,
181                });
182                next = next.checked_add(1).ok_or(DraftError::Bound)?;
183            }
184        }
185        if block_on(store.read_stream_next_seq(REGISTRY_STREAM))
186            .map_err(|_| DraftError::Store)?
187            .map_err(|_| DraftError::Store)?
188            .unwrap_or(0)
189            != head
190        {
191            return Err(DraftError::Contract);
192        }
193        Ok(())
194    }
195    fn legacy_approval(target: &Value) -> Result<Option<ApprovalTransition>, DraftError> {
196        if target["receipt"].is_null() {
197            return Ok(None);
198        }
199        let receipt: Execution = serde_json::from_value(target["receipt"]["execution"].clone())
200            .map_err(|_| DraftError::Store)?;
201        Ok(Some(ApprovalTransition {
202            draft: target["receipt"]["draft"]
203                .as_str()
204                .ok_or(DraftError::Contract)?
205                .into(),
206            digest: execution_digest(&receipt).into(),
207            approved_by: vec!["lys".into()],
208            executed_at_ms: target["transition"]["at_ms"]
209                .as_u64()
210                .ok_or(DraftError::Contract)?,
211        }))
212    }
213    pub(crate) fn lock(&self) -> Result<MutexGuard<'_, Kept>, DraftError> {
214        let kept = self.kept.lock().map_err(|_| DraftError::Store)?;
215        if kept.uncertain {
216            return Err(DraftError::Store);
217        }
218        Ok(kept)
219    }
220    fn validate(&self, snapshot: &Snapshot) -> Result<Vec<u8>, DraftError> {
221        if snapshot.rows.len() > self.limits.rows.get()
222            || snapshot.prepared.len() > self.limits.operations.get()
223            || snapshot.outcomes.len() > self.limits.operations.get()
224            || snapshot.deliveries.len() > self.limits.operations.get()
225            || snapshot.waiting.len() > self.limits.operations.get()
226        {
227            return Err(DraftError::Bound);
228        }
229        let bytes = serde_json::to_vec(snapshot).map_err(|_| DraftError::Store)?;
230        if bytes.len() > self.limits.snapshot_bytes.get()
231            || self
232                .store
233                .max_request_bytes()
234                .is_some_and(|bound| bytes.len() > bound)
235        {
236            return Err(DraftError::Bound);
237        }
238        Ok(bytes)
239    }
240    /// Keeps a candidate only after one conditional replacement and one flush.
241    /// A failed write/flush poisons this owner until it is reopened from disk.
242    ///
243    /// # Errors
244    /// Conflict, capacity or storage failure; no failure is converted to success.
245    pub fn update<T>(
246        &self,
247        change: impl FnOnce(&mut Snapshot) -> Result<T, DraftError>,
248    ) -> Result<T, DraftError> {
249        self.replace(change, true)
250    }
251    fn replace<T>(
252        &self,
253        change: impl FnOnce(&mut Snapshot) -> Result<T, DraftError>,
254        flush: bool,
255    ) -> Result<T, DraftError> {
256        let mut kept = self.lock()?;
257        if kept.pending.is_some() || (flush && kept.active.is_some()) {
258            return Err(DraftError::Contract);
259        }
260        let mut candidate = kept.snapshot.clone();
261        let result = change(&mut candidate)?;
262        let bytes = self.validate(&candidate)?;
263        let hash = Hash::of(&bytes);
264        let write = block_on(self.store.apply_routed_batch(
265            ROUTE,
266            vec![RoutedBatchItem {
267                key: KEY.into(),
268                expected: kept.hash,
269                value: bytes,
270                ttl: None,
271            }],
272        ))
273        .map_err(|_| DraftError::Store)
274        .and_then(|result| result.map_err(|_| DraftError::Store));
275        if write.is_err() {
276            kept.uncertain = true;
277            return Err(DraftError::Store);
278        }
279        if flush {
280            if !matches!(block_on(self.store.flush()), Ok(Ok(()))) {
281                kept.uncertain = true;
282                return Err(DraftError::Store);
283            }
284            kept.snapshot = candidate;
285            kept.hash = Some(hash);
286        } else {
287            kept.pending = Some((candidate, hash));
288        }
289        drop(kept);
290        Ok(result)
291    }
292    /// Reads a bounded durable projection without exposing it in diagnostics.
293    ///
294    /// # Errors
295    /// An uncertain or unavailable journal.
296    pub fn read<T>(&self, read: impl FnOnce(&Snapshot) -> T) -> Result<T, DraftError> {
297        Ok(read(&self.lock()?.snapshot))
298    }
299    /// Captures only the matching native agent transition. Other presence
300    /// observations remain ordinary target rows, without approval metadata.
301    ///
302    /// # Errors
303    /// Another approval is executing or the journal is unavailable.
304    pub fn begin(
305        &self,
306        draft: &ApprovedDraft,
307        approval: &ApprovalTransition,
308    ) -> Result<(), DraftError> {
309        let mut kept = self.lock()?;
310        if approval.draft != draft.id
311            || approval.digest != draft.request_digest
312            || approval.approved_by.is_empty()
313        {
314            return Err(DraftError::ReceiptMismatch);
315        }
316        if kept.active.is_some() {
317            return Err(DraftError::Store);
318        }
319        let act: crate::agents::records::AgentAct =
320            serde_json::from_str(&draft.words).map_err(|_| DraftError::Contract)?;
321        let mut candidate = kept.snapshot.clone();
322        let receipt = Execution::Executed {
323            request_digest: draft.request_digest.clone(),
324            receipt_digest: lys_pass::drafts::request_digest(&format!(
325                "{}:{}:{}",
326                draft.id,
327                draft.request_digest,
328                candidate.rows.len()
329            )),
330        };
331        let target = serde_json::json!({"version":1,"name":act.name,"owner":act.owner,"transition":{"holder":act.holder,"at_ms":approval.executed_at_ms,"change":act.change},"receipt":{"draft":draft.id,"execution":receipt}});
332        candidate.rows.push(TargetRow {
333            timestamp: approval.executed_at_ms,
334            target,
335            approval: Some(approval.clone()),
336        });
337        candidate.outcomes.insert(
338            draft.id.clone(),
339            Outcome {
340                draft: Some(draft.clone()),
341                approval: approval.clone(),
342                receipt,
343                acknowledged: false,
344                delivered: false,
345            },
346        );
347        self.validate(&candidate)?;
348        kept.active = Some((draft.clone(), approval.clone()));
349        drop(kept);
350        Ok(())
351    }
352    /// Removes the synchronous execution context after its target owner returns.
353    ///
354    /// # Errors
355    /// The journal is unavailable.
356    pub fn finish(&self) -> Result<(), DraftError> {
357        self.kept.lock().map_err(|_| DraftError::Store)?.active = None;
358        Ok(())
359    }
360    pub(crate) fn append_native(
361        &self,
362        payloads: Vec<Vec<u8>>,
363        expected: u64,
364    ) -> Result<u64, DraftError> {
365        let active = self.lock()?.active.clone();
366        self.replace(
367            |snapshot| {
368                if u64::try_from(snapshot.rows.len()).map_err(|_| DraftError::Bound)? != expected
369                    || payloads.is_empty()
370                {
371                    return Err(DraftError::Contract);
372                }
373                for payload in payloads {
374                    let target: Value =
375                        serde_json::from_slice(&payload).map_err(|_| DraftError::Contract)?;
376                    let approval = if target["receipt"].is_null() {
377                        None
378                    } else {
379                        let (draft, transition) = active.as_ref().ok_or(DraftError::Contract)?;
380                        let receipt: Execution =
381                            serde_json::from_value(target["receipt"]["execution"].clone())
382                                .map_err(|_| DraftError::Contract)?;
383                        if target["receipt"]["draft"] != draft.id
384                            || execution_digest(&receipt) != draft.request_digest
385                            || target["name"] != draft.target.id
386                            || target["transition"]["at_ms"] != transition.executed_at_ms
387                            || snapshot.outcomes.contains_key(&draft.id)
388                        {
389                            return Err(DraftError::ReceiptMismatch);
390                        }
391                        snapshot.outcomes.insert(
392                            draft.id.clone(),
393                            Outcome {
394                                draft: Some(draft.clone()),
395                                approval: transition.clone(),
396                                receipt,
397                                acknowledged: false,
398                                delivered: false,
399                            },
400                        );
401                        Some(transition.clone())
402                    };
403                    snapshot.rows.push(TargetRow {
404                        timestamp: target["transition"]["at_ms"]
405                            .as_u64()
406                            .ok_or(DraftError::Contract)?,
407                        target,
408                        approval,
409                    });
410                }
411                u64::try_from(snapshot.rows.len()).map_err(|_| DraftError::Bound)
412            },
413            false,
414        )
415    }
416    pub(crate) fn flush_pending(&self) -> Result<(), DurabilityError> {
417        let mut kept = self.lock().map_err(|error| Self::storage(&error))?;
418        let result = block_on(self.store.flush())
419            .map_err(|_| Self::storage(&DraftError::Store))
420            .and_then(|result| result);
421        if let Err(error) = result {
422            kept.uncertain = true;
423            return Err(error);
424        }
425        if let Some((snapshot, hash)) = kept.pending.take() {
426            kept.snapshot = snapshot;
427            kept.hash = Some(hash);
428        }
429        drop(kept);
430        Ok(())
431    }
432    pub(crate) fn storage(error: &DraftError) -> DurabilityError {
433        DurabilityError::AppendUnresolved {
434            stream_key: REGISTRY_STREAM.into(),
435            sequence: 0,
436            cause: error.to_string(),
437        }
438    }
439}
440
441#[path = "waiting_agents.rs"]
442mod agents;
443pub use agents::{AgentBindings, ApprovalAgents};