Skip to main content

rig_memory/
writer.rs

1//! A running session's logs: [`Writer`] queues each agent's changes as a
2//! host saves them and writes them on [`flush`](Writer::flush).
3
4use std::collections::{BTreeMap, HashMap};
5use std::io;
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7
8use rig_core::conversation::{Change, ChangeRecord, Conversation, Lineage, MessageId};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11
12use crate::blobs::{PutBlob, put_blob, read_blob, store_change};
13use crate::fork::turn_cut;
14use crate::format::{Header, LogLines, Meta, SESSION, write_change, write_meta};
15use crate::session::{
16    Fold, Folded, fold_all, fold_lines, log_lines, read_header, read_logs, repairs, retired,
17    session_of,
18};
19use crate::store::now;
20use crate::{LogWriteError, Repair, SavedAgent, Session, SessionError, Stamp, Store};
21use rig_core::now_ms;
22
23/// One log as the writer keeps it.
24#[derive(Default)]
25pub(crate) struct Log {
26    /// 0 for an agent the user started, 1 for its subagents, and so on.
27    depth: usize,
28    next_seq: u64,
29    /// Whether the log holds a change or a saved value, so it is written.
30    started: bool,
31    pending: Vec<u8>,
32    /// The newest value of each saved value, so an unchanged one is not
33    /// written again.
34    state: BTreeMap<String, Value>,
35    /// The log's lineage, from its header.
36    lineage: Lineage,
37    /// The changes it holds.
38    changes: u64,
39    /// Its bytes in the store, so a write is refused once another writer
40    /// changed it.
41    len: u64,
42}
43
44/// The records of every log, before and after they are written.
45#[derive(Default)]
46pub(crate) struct Book {
47    /// The next record's `n`.
48    next_n: u64,
49    /// Records are queued only once started.
50    live: bool,
51    /// Why the logging stopped: every record after a failed write is
52    /// dropped.
53    failure: Option<String>,
54    /// Whether a flush wrote a record.
55    wrote: bool,
56    logs: HashMap<String, Log>,
57    /// The logs that end in a torn line, each with the line's bytes: the
58    /// line is set aside before anything is written after it.
59    torn: HashMap<String, u64>,
60}
61
62impl Book {
63    /// A book that goes on after the logs `folded`, each as it ends.
64    pub fn after(folded: &[Folded]) -> Self {
65        let parents: HashMap<&str, Option<&str>> = folded
66            .iter()
67            .map(|log| (log.name.as_str(), log.header.parent.as_deref()))
68            .collect();
69        let mut book = Self::default();
70        for log in folded {
71            // Parents come from the logs' headers; the bound stops a corrupt store's loop.
72            let ancestors = std::iter::successors(log.header.parent.as_deref(), |parent| {
73                parents.get(parent).copied().flatten()
74            });
75            let known = ancestors.take_while(|parent| parents.contains_key(parent));
76            let kept = Log {
77                depth: known.take(folded.len()).count(),
78                next_seq: log.next_seq,
79                started: true,
80                state: log.state.clone(),
81                lineage: log.header.lineage,
82                changes: log.conversation.revision(),
83                len: log.len,
84                ..Log::default()
85            };
86            book.logs.insert(log.name.clone(), kept);
87            book.next_n = book.next_n.max(log.next_n);
88        }
89        book
90    }
91
92    /// Sets aside the torn line each log of `torn` ends in, given with its
93    /// bytes, as [`set_aside_tail`] does, before that log is next written.
94    pub fn set_aside(&mut self, torn: impl IntoIterator<Item = (String, u64)>) {
95        self.torn.extend(torn);
96    }
97
98    pub fn start(&mut self) {
99        self.live = true;
100    }
101
102    fn is_live(&self) -> bool {
103        self.live && self.failure.is_none()
104    }
105
106    /// The log `name`, started first with a header naming `parent` when it
107    /// has none; `None` while nothing is logged.
108    pub fn opened(&mut self, name: &str, parent: Option<&str>) -> Option<&mut Log> {
109        if !self.is_live() {
110            return None;
111        }
112        if !self.logs.contains_key(name) {
113            let depth = |parent| self.logs.get(parent).map_or(0, |log: &Log| log.depth + 1);
114            let header = Header::new(name, parent);
115            let mut log = Log {
116                depth: parent.map_or(0, depth),
117                lineage: header.lineage,
118                ..Log::default()
119            };
120            let header = Meta::Header(header);
121            let stamp = Stamp {
122                n: self.next_n,
123                seq: 0,
124                t: now_ms(),
125            };
126            if let Err(failure) = write_meta(&mut log.pending, stamp, &header) {
127                self.failure = Some(failure.to_string());
128                return None;
129            }
130            log.next_seq += 1;
131            self.next_n += 1;
132            self.logs.insert(name.to_owned(), log);
133        }
134        self.logs.get_mut(name)
135    }
136
137    /// Queues `meta` for the log `name`; whether it was queued.
138    fn meta(&mut self, name: &str, meta: &Meta) -> bool {
139        if self.opened(name, None).is_none() {
140            return false;
141        }
142        let n = self.next_n;
143        let Some(log) = self.logs.get_mut(name) else {
144            return false;
145        };
146        let stamp = Stamp {
147            n,
148            seq: log.next_seq,
149            t: now_ms(),
150        };
151        match write_meta(&mut log.pending, stamp, meta) {
152            Ok(()) => {
153                log.next_seq += 1;
154                self.next_n += 1;
155                true
156            }
157            Err(failure) => {
158                self.failure = Some(failure.to_string());
159                false
160            }
161        }
162    }
163
164    /// Queues the unsaved changes of `conversation` for the log `name`, or
165    /// one reset when it does not continue the log
166    /// ([`Conversation::continues`]), empties its outbox and marks it as
167    /// continuing the log; the changes queued, each stamped with when it
168    /// was made. While nothing is logged the outbox is emptied all the
169    /// same, and the next save writes a reset. Each image is stored as a
170    /// blob through `put`.
171    pub fn save(
172        &mut self,
173        put: &mut PutBlob<'_>,
174        name: &str,
175        conversation: &mut Conversation,
176    ) -> Vec<ChangeRecord> {
177        let Some(log) = self.opened(name, None) else {
178            return stamp_all(conversation.take_unsaved());
179        };
180        let (lineage, held) = (log.lineage, log.changes);
181        let continues = conversation.continues(lineage, held);
182        let unsaved = conversation.take_unsaved();
183        let changes = if continues {
184            unsaved
185        } else {
186            vec![ChangeRecord {
187                change: Change::Reset {
188                    state: Box::new(conversation.view()),
189                },
190                by: None,
191                at: None,
192            }]
193        };
194        if changes.is_empty() {
195            conversation.continue_log(lineage, held);
196            return changes;
197        }
198        let mut queued = Vec::with_capacity(changes.len());
199        for recorded in stamp_all(changes) {
200            if let Err(failure) = self.queue(put, name, &recorded) {
201                self.failure = Some(failure);
202                break;
203            }
204            queued.push(recorded);
205        }
206        let held = held.saturating_add(queued.len() as u64);
207        if let Some(log) = self.logs.get_mut(name) {
208            log.started = true;
209            log.changes = held;
210        }
211        conversation.continue_log(lineage, held);
212        queued
213    }
214
215    /// Queues `recorded`, its images stored as blobs, for the log `name`.
216    fn queue(
217        &mut self,
218        put: &mut PutBlob<'_>,
219        name: &str,
220        recorded: &ChangeRecord,
221    ) -> Result<(), String> {
222        let stored = store_change(&recorded.change, put)
223            .map_err(|failure| format!("storing an image failed: {failure}"))?;
224        let change = stored.as_ref().unwrap_or(&recorded.change);
225        let n = self.next_n;
226        let log = self.logs.get_mut(name).ok_or("no log")?;
227        let stamp = Stamp {
228            n,
229            seq: log.next_seq,
230            t: recorded.at.unwrap_or_default(),
231        };
232        write_change(&mut log.pending, stamp, recorded.by.as_ref(), change)
233            .map_err(|failure| failure.to_string())?;
234        log.next_seq += 1;
235        self.next_n += 1;
236        Ok(())
237    }
238
239    /// Queues the saved value `key` of the log `name` when it changed;
240    /// `None` or `null` when it was removed. With `starts`, the change
241    /// starts the log.
242    pub fn save_value(&mut self, name: &str, key: &str, value: Option<Value>, starts: bool) {
243        let value = value.filter(|value| !value.is_null());
244        let known = self.logs.get(name).and_then(|log| log.state.get(key));
245        if known == value.as_ref() {
246            return;
247        }
248        let meta = Meta::State {
249            key: key.to_owned(),
250            value: value.clone(),
251        };
252        if self.meta(name, &meta)
253            && let Some(log) = self.logs.get_mut(name)
254        {
255            log.started |= starts;
256            match value {
257                Some(value) => log.state.insert(key.to_owned(), value),
258                None => log.state.remove(key),
259            };
260        }
261    }
262
263    /// Queues the saved values of the log `name` that differ from `known`,
264    /// and the removal of each of `known` that `values` does not have.
265    pub fn states(
266        &mut self,
267        name: &str,
268        known: Option<&BTreeMap<String, Value>>,
269        values: &BTreeMap<String, Value>,
270    ) {
271        let removed = known.into_iter().flat_map(BTreeMap::keys);
272        let removed: Vec<String> = removed
273            .filter(|key| !values.contains_key(*key))
274            .cloned()
275            .collect();
276        for key in removed {
277            self.save_value(name, &key, None, true);
278        }
279        for (key, value) in values {
280            self.save_value(name, key, Some(value.clone()), true);
281        }
282    }
283
284    /// Starts the log `to` in `store` as a copy of the log `from` up to a
285    /// turn boundary at or before `through` ([`turn_cut`]), with a header
286    /// of its own and a fresh lineage, and goes on after it; the copy as
287    /// read. `from` must be written already. An agent with no log yet gets
288    /// an empty one.
289    pub fn fork(
290        &mut self,
291        store: &dyn Store,
292        (from, to): (&str, &str),
293        through: Option<MessageId>,
294    ) -> Result<Folded, SessionError> {
295        let refuse = |why: String| SessionError::from(io::Error::other(why));
296        if !self.is_live() {
297            return Err(refuse("the session is not logged".to_owned()));
298        }
299        if self.logs.contains_key(to) || log_lines(store, to)?.is_some() {
300            return Err(refuse(format!("there is a log `{to}` already")));
301        }
302        let lines = log_lines(store, from)?.unwrap_or_else(|| LogLines::parse(Vec::new()));
303        let mut bytes = Vec::new();
304        if let Some((_, start, first)) = lines.iter().next() {
305            let (stamp, _) = read_header(from, first)?;
306            let cut = turn_cut(from, &lines, through)?;
307            let stamp = Stamp {
308                t: now_ms(),
309                ..stamp
310            };
311            let header = Meta::Header(Header::new(to, None));
312            write_meta(&mut bytes, stamp, &header).map_err(io::Error::from)?;
313            let start = usize::try_from(start).unwrap_or(usize::MAX);
314            bytes.extend_from_slice(lines.prefix(cut).get(start..).unwrap_or_default());
315        } else if through.is_some() {
316            return Err(refuse(format!("the log `{from}` holds no message")));
317        }
318        if bytes.is_empty() {
319            let _ = self
320                .opened(to, None)
321                .ok_or_else(|| refuse("logging stopped".to_owned()))?;
322            let folded = Folded {
323                name: to.to_owned(),
324                ..Folded::default()
325            };
326            return Ok(folded);
327        }
328        let copy = LogLines::parse(bytes);
329        let read = |blob: &str| read_blob(store, blob);
330        let folded = fold_lines(to, &copy, Fold::WHOLE, &read)?
331            .ok_or_else(|| refuse(format!("the copy of the log `{from}` is empty")))?;
332        refused(to, store.append_log(to, 0, copy.prefix(u64::MAX)))?;
333        let log = Log {
334            next_seq: folded.next_seq,
335            started: true,
336            state: folded.state.clone(),
337            lineage: folded.header.lineage,
338            changes: folded.conversation.revision(),
339            len: copy.len(),
340            ..Log::default()
341        };
342        self.logs.insert(to.to_owned(), log);
343        Ok(folded)
344    }
345
346    pub fn retire(&mut self, name: &str) {
347        self.meta(name, &Meta::Retired);
348    }
349
350    /// Writes every queued record to `store`: each started log's records
351    /// with one write, a subagent's log before its parent's, and the
352    /// [`SESSION`] log once any is started. A failed write stops the
353    /// logging; one the store refused because another writer changed the
354    /// log is [`SessionError::Changed`].
355    pub fn flush(&mut self, store: &dyn Store) -> Result<(), SessionError> {
356        now(self.flush_async(store))
357    }
358
359    /// [`Self::flush`] to an [`AsyncStore`](crate::AsyncStore).
360    pub async fn flush_async<S: crate::AsyncStore + ?Sized>(
361        &mut self,
362        store: &S,
363    ) -> Result<(), SessionError> {
364        if let Some(failure) = &self.failure {
365            return Err(io::Error::other(failure.clone()).into());
366        }
367        let any = self.logs.values().any(|log| log.started);
368        let mut due: Vec<(usize, String)> = self
369            .logs
370            .iter()
371            .filter(|(name, log)| log.started || any && name.as_str() == SESSION)
372            .filter(|(_, log)| !log.pending.is_empty())
373            .map(|(name, log)| (log.depth, name.clone()))
374            .collect();
375        due.sort_by(|a, b| b.cmp(a));
376        for (_, name) in due {
377            let Some(log) = self.logs.get_mut(&name) else {
378                continue;
379            };
380            let mut written = Ok(());
381            if let Some(cut) = self.torn.remove(&name) {
382                written = set_aside_tail_async(store, &name, log.len, cut)
383                    .await
384                    .map(drop);
385            }
386            if written.is_ok() {
387                let appended = store.append_log_async(&name, log.len, &log.pending).await;
388                written = refused(&name, appended);
389            }
390            if let Err(failure) = written {
391                let why = format!("writing the log {name} failed: {failure}");
392                self.failure = Some(why.clone());
393                return Err(match failure {
394                    SessionError::Changed { .. } => failure,
395                    _ => io::Error::other(why).into(),
396                });
397            }
398            log.len += log.pending.len() as u64;
399            log.pending.clear();
400            self.wrote = true;
401        }
402        Ok(())
403    }
404}
405
406/// A running session's logs, the harness's appender. Every method takes
407/// `&self`, so clones share them. Nothing is logged before
408/// [`start`](Self::start), nor once a write failed; a default writer has no
409/// store and logs nothing.
410#[derive(Clone, Default)]
411pub struct Writer(Arc<Mutex<Inner>>);
412
413#[derive(Default)]
414struct Inner {
415    store: Option<Arc<dyn Store>>,
416    book: Book,
417}
418
419impl Writer {
420    fn inner(&self) -> MutexGuard<'_, Inner> {
421        self.0.lock().unwrap_or_else(PoisonError::into_inner)
422    }
423
424    /// Opens the session in `store` to go on with it, and returns the
425    /// session as read (retired agents and their subagents left out).
426    ///
427    /// Reads like [`Session::read`], so a broken session, or one of
428    /// another format, is refused before anything is written. Then each torn tail
429    /// is moved to `recovered/<log>-<sha256 of the tail>.part`
430    /// ([`Repair::TornTail`]).
431    /// Nothing is logged until [`start`](Self::start).
432    ///
433    /// # Errors
434    ///
435    /// As [`Session::read`], or when setting a torn tail aside fails.
436    pub fn open(store: Arc<dyn Store>) -> Result<(Self, Session), SessionError> {
437        let mut folded = fold_all(&*store, &read_logs(&*store)?)?;
438        let mut repairs = repairs(&*store, &folded)?;
439        for repair in &mut repairs {
440            if let Repair::TornTail {
441                log,
442                kept,
443                cut,
444                set_aside,
445            } = repair
446            {
447                *set_aside = Some(set_aside_tail(&*store, log, *kept, *cut)?);
448            }
449        }
450        let book = Book::after(&folded);
451        let skipped = retired(&folded);
452        folded.retain(|log| !skipped.contains(&log.name));
453        let writer = Self(Arc::new(Mutex::new(Inner {
454            store: Some(store),
455            book,
456        })));
457        Ok((writer, session_of(folded, repairs)))
458    }
459
460    /// Starts logging, once the host restored the session: what it did to
461    /// restore is not logged.
462    pub fn start(&self) {
463        let mut inner = self.inner();
464        if inner.store.is_some() {
465            inner.book.start();
466        }
467    }
468
469    /// Whether records are logged: the writer was started, and no write
470    /// failed.
471    pub fn is_live(&self) -> bool {
472        self.inner().book.is_live()
473    }
474
475    /// Why the logging stopped, if it did.
476    pub fn failure(&self) -> Option<String> {
477        self.inner().book.failure.clone()
478    }
479
480    /// Whether a [`flush`](Self::flush) wrote a record to the store, such
481    /// as for a host that marks its session saved once something is.
482    pub fn has_written(&self) -> bool {
483        self.inner().book.wrote
484    }
485
486    /// Starts the log `log`, of an agent spawned by the agent `parent`,
487    /// unless it has one.
488    pub fn open_log(&self, log: &str, parent: Option<&str>) {
489        let _ = self.inner().book.opened(log, parent);
490    }
491
492    /// Queues the unsaved changes of `conversation` for the log `log` and
493    /// empties its outbox: the changes, each stamped with when it was made
494    /// ([`ChangeRecord::at`]). When the conversation does not continue the log
495    /// ([`Conversation::continues`]), as when a value made elsewhere
496    /// replaced it, one [`Change::Reset`] to the whole value is queued
497    /// instead. Either way the conversation then continues the log.
498    pub fn save(&self, log: &str, conversation: &mut Conversation) -> Vec<ChangeRecord> {
499        let mut inner = self.inner();
500        let Inner { store, book } = &mut *inner;
501        match store {
502            Some(store) => {
503                let put = &mut |name: String, bytes: Vec<u8>| put_blob(&**store, &name, &bytes);
504                book.save(put, log, conversation)
505            }
506            None => stamp_all(conversation.take_unsaved()),
507        }
508    }
509
510    /// Queues the saved value `key` of the log `log`, an agent's id or
511    /// [`SESSION`], when it changed; `None` or a `null` value when it was
512    /// removed. With `starts`, the change starts the log.
513    pub fn save_value(&self, log: &str, key: &str, value: Option<Value>, starts: bool) {
514        self.inner().book.save_value(log, key, value, starts);
515    }
516
517    /// Starts the log `to` as a copy of the log `from` up to a turn
518    /// boundary at or before message `through` (its end with `None`): through
519    /// the last record after which no turn runs, no tool call waits for its
520    /// result, the model owes no answer and nothing after `through` is in the
521    /// conversation. What is queued is written first. The copy keeps the
522    /// records' stamps, so [`as_of`](crate::as_of) and
523    /// [`undo_summary`](crate::undo_summary) read it as they read `from`,
524    /// and has its own header and lineage. Returns the agent of the copy:
525    /// its conversation continues the new log, and its saved values are
526    /// those `from` had at the cut.
527    ///
528    /// # Errors
529    ///
530    /// When nothing is logged, the log `to` exists, `from` does not read or
531    /// never held `through`, or a write fails.
532    pub fn fork_log(
533        &self,
534        from: &str,
535        to: &str,
536        through: Option<MessageId>,
537    ) -> Result<SavedAgent, SessionError> {
538        let mut inner = self.inner();
539        let Inner { store, book } = &mut *inner;
540        let Some(store) = store else {
541            return Err(io::Error::other("the session is not stored").into());
542        };
543        book.flush(&**store)?;
544        let folded = book.fork(&**store, (from, to), through)?;
545        Ok(SavedAgent {
546            id: folded.name,
547            started_ms: folded.started_ms,
548            conversation: folded.conversation,
549            saved: folded.state,
550            ..SavedAgent::default()
551        })
552    }
553
554    /// Queues that the agent of the log `log` is gone for good.
555    pub fn retire(&self, log: &str) {
556        self.inner().book.retire(log);
557    }
558
559    /// Writes every queued record: each started log's records with one
560    /// write, a subagent's log before its parent's, so a parent never
561    /// refers to a subagent message that is not stored, and the [`SESSION`]
562    /// log once any is started. No fsync: written is not yet proof against
563    /// a power cut. A failed write stops the logging ([`Self::failure`]).
564    ///
565    /// # Errors
566    ///
567    /// When the logging stopped, now or before: a write failed, so what
568    /// was queued is not stored and nothing more will be. A writer without
569    /// a store keeps nothing and never fails.
570    pub fn flush(&self) -> Result<(), SessionError> {
571        let mut inner = self.inner();
572        let Inner { store, book } = &mut *inner;
573        match store {
574            Some(store) => book.flush(&**store),
575            None => Ok(()),
576        }
577    }
578}
579
580/// Copies the last `cut` bytes of the log `log`, `kept + cut` bytes long,
581/// to the file `recovered/<log>-<sha256 of them>.part`, then replaces the
582/// log with the bytes before them. Returns the name of the file the tail
583/// went to. A crash between the two leaves the tail in the log, set aside
584/// again by the next open.
585///
586/// # Errors
587///
588/// [`SessionError::Changed`], writing nothing, when the log is not
589/// `kept + cut` bytes long; or when the store fails.
590pub(crate) fn set_aside_tail(
591    store: &dyn Store,
592    log: &str,
593    kept: u64,
594    cut: u64,
595) -> Result<String, SessionError> {
596    now(set_aside_tail_async(store, log, kept, cut))
597}
598
599/// [`set_aside_tail`] in an [`AsyncStore`](crate::AsyncStore).
600async fn set_aside_tail_async<S: crate::AsyncStore + ?Sized>(
601    store: &S,
602    log: &str,
603    kept: u64,
604    cut: u64,
605) -> Result<String, SessionError> {
606    let bytes = store.read_log_async(log).await?;
607    let split = usize::try_from(kept)
608        .ok()
609        .and_then(|kept| bytes.split_at_checked(kept));
610    let Some((prefix, tail)) = split.filter(|(_, tail)| tail.len() as u64 == cut) else {
611        let log = log.to_owned();
612        return Err(SessionError::Changed { log });
613    };
614    let name = format!("recovered/{log}-{:x}.part", Sha256::digest(tail));
615    store.write_file_async(&name, tail).await?;
616    let replaced = store.replace_log_async(log, kept + cut, prefix).await;
617    refused(log, replaced)?;
618    Ok(name)
619}
620
621/// The outcome of a write to the log `log`: a refusal because another
622/// writer changed it is [`SessionError::Changed`].
623pub(crate) fn refused(log: &str, written: Result<(), LogWriteError>) -> Result<(), SessionError> {
624    written.map_err(|failure| match failure {
625        LogWriteError::Changed => SessionError::Changed {
626            log: log.to_owned(),
627        },
628        LogWriteError::Io(failure) => failure.into(),
629    })
630}
631
632/// `changes`, each stamped with now unless it says when it was made.
633fn stamp_all(changes: Vec<ChangeRecord>) -> Vec<ChangeRecord> {
634    let now = now_ms();
635    let stamp = |recorded: ChangeRecord| ChangeRecord {
636        at: Some(recorded.at.unwrap_or(now)),
637        ..recorded
638    };
639    changes.into_iter().map(stamp).collect()
640}