Skip to main content

cookie_agent_engine/
events.rs

1//! Buffered/durable session event logs and ephemeral tool-output hubs.
2
3use std::{
4    collections::{HashMap, HashSet, VecDeque},
5    fs::{self, OpenOptions},
6    io::{self, BufWriter, Write},
7    path::{Path, PathBuf},
8    sync::{
9        Arc, Condvar, Mutex,
10        atomic::{AtomicBool, AtomicU64, Ordering},
11    },
12    thread::{self, JoinHandle},
13    time::{Duration, Instant},
14};
15
16#[cfg(unix)]
17use std::fs::File;
18
19use base64::{Engine as _, engine::general_purpose::STANDARD};
20use cookie_agent_protocol::{
21    AssistantToolCallRef, AttemptId, EventOrigin, EventPayload, ModelCallId, OutputDelta,
22    OutputGap, OutputSnapshot, OutputStream, ProviderItemId, RunId, SessionId, StoredEvent,
23    ToolCallId, ToolCallStart, deserialize_event_payload_best_effort,
24};
25use jiff::Timestamp;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use thiserror::Error;
29use tokio::sync::mpsc;
30
31use crate::ownership::{WriteAuthority, WriteCapability};
32
33#[derive(Debug, Error)]
34pub enum EventLogError {
35    #[error("event log IO failure at {path}: {source}")]
36    Io {
37        path: PathBuf,
38        #[source]
39        source: io::Error,
40    },
41    #[error("invalid JSONL record in {path}: {source}")]
42    Json {
43        path: PathBuf,
44        #[source]
45        source: serde_json::Error,
46    },
47    #[error("event log {0} has no SessionCreated record")]
48    MissingCreation(PathBuf),
49    #[error("corrupt event log at {path}: {message}")]
50    Corrupt { path: PathBuf, message: String },
51    #[error("event log {0} is read-only")]
52    ReadOnly(PathBuf),
53}
54
55#[derive(Debug)]
56pub struct EventLog {
57    path: PathBuf,
58    session_id: SessionId,
59    append: Mutex<()>,
60    events: Mutex<EventStorage>,
61    diagnostics: Vec<EventLoadDiagnostic>,
62    initial_validation_taint: ValidationTaint,
63    validation: Mutex<ValidationState>,
64    next_seq: AtomicU64,
65    persisted: AtomicBool,
66    read_only: bool,
67    write_capability: Option<WriteCapability>,
68    #[cfg(test)]
69    _test_authority: Option<WriteAuthority>,
70    #[cfg(test)]
71    append_authorization_hook: Mutex<Option<AppendAuthorizationHook>>,
72    writer: Mutex<Option<Arc<EventLogWriter>>>,
73}
74
75#[derive(Clone, Copy)]
76enum TornTail {
77    Truncate,
78    Ignore,
79}
80
81const EVENT_SYNC_WINDOW: Duration = Duration::from_millis(8);
82const EVENT_SYNC_BYTES: usize = 32 * 1024;
83
84#[derive(Debug)]
85struct EventLogWriter {
86    shared: Arc<EventLogWriterShared>,
87    worker: Mutex<Option<JoinHandle<()>>>,
88}
89
90#[derive(Debug)]
91struct EventLogWriterShared {
92    path: PathBuf,
93    state: Mutex<EventLogWriterState>,
94    wake: Condvar,
95    #[cfg(test)]
96    before_sync: Mutex<Option<SyncHook>>,
97}
98
99#[derive(Debug)]
100struct EventLogWriterState {
101    output: BufWriter<fs::File>,
102    unsynced_bytes: usize,
103    sync_deadline: Option<Instant>,
104    directory_sync_pending: bool,
105    background_error: Option<WriterFailure>,
106    #[cfg(test)]
107    background_sync_paused: bool,
108    shutdown: bool,
109}
110
111#[derive(Clone, Debug)]
112struct WriterFailure {
113    kind: io::ErrorKind,
114    message: String,
115}
116
117#[cfg(test)]
118#[derive(Debug)]
119struct SyncHook {
120    reached: std::sync::mpsc::Sender<()>,
121    release: std::sync::mpsc::Receiver<()>,
122}
123
124#[cfg(test)]
125#[derive(Debug)]
126struct AppendAuthorizationHook {
127    reached: std::sync::mpsc::Sender<()>,
128    release: std::sync::mpsc::Receiver<()>,
129}
130
131#[derive(Debug, Default)]
132struct EventIndex {
133    last_run_started: Option<(u64, RunId, cookie_agent_protocol::ModelSelection)>,
134    last_checkpoint_seq: u64,
135    last_checkpoint_input_through_seq: u64,
136    last_recorded_usage: Option<(u64, u64)>,
137    last_turn_usage: Option<(u64, u64)>,
138}
139
140impl EventIndex {
141    fn observe(&mut self, event: &StoredEvent) {
142        match &event.payload {
143            EventPayload::RunStarted { selection, .. } => {
144                if let Some(run) = event.run_id {
145                    self.last_run_started = Some((event.seq, run, selection.model.clone()));
146                }
147            }
148            EventPayload::ContextCheckpointCommitted { commit } => {
149                self.last_checkpoint_seq = event.seq;
150                self.last_checkpoint_input_through_seq = commit.boundaries.input_through_seq;
151            }
152            EventPayload::ModelUsageRecorded { usage, .. } => {
153                if let Some(usage) = usage_total(event.seq, usage) {
154                    self.last_recorded_usage = Some(usage);
155                }
156            }
157            EventPayload::ModelTurnCommitted { turn, .. } => {
158                if let Some(usage) = usage_total(event.seq, &turn.usage) {
159                    self.last_turn_usage = Some(usage);
160                }
161            }
162            _ => {}
163        }
164    }
165
166    fn latest_real_usage(&self) -> Option<(u64, u64)> {
167        self.last_recorded_usage.or(self.last_turn_usage)
168    }
169}
170
171#[derive(Debug)]
172struct EventStorage {
173    all: Vec<StoredEvent>,
174    visible: Vec<usize>,
175    visible_ceiling: u64,
176    snapshot: Option<Arc<[StoredEvent]>>,
177    index: EventIndex,
178}
179
180impl EventStorage {
181    fn new(all: Vec<StoredEvent>) -> Self {
182        let mut storage = Self {
183            all,
184            visible: Vec::new(),
185            visible_ceiling: u64::MAX,
186            snapshot: None,
187            index: EventIndex::default(),
188        };
189        storage.rebuild_visible();
190        storage
191    }
192
193    fn push(&mut self, event: StoredEvent) {
194        let revert = match &event.payload {
195            EventPayload::SessionReverted { through_seq } => Some(*through_seq),
196            _ => None,
197        };
198        let index = self.all.len();
199        self.all.push(event);
200        if let Some(through_seq) = revert {
201            self.visible_ceiling = self.visible_ceiling.min(through_seq);
202            let all = &self.all;
203            self.visible
204                .retain(|candidate| all[*candidate].seq <= self.visible_ceiling);
205            self.visible.push(index);
206            self.rebuild_index();
207        } else {
208            self.index.observe(&self.all[index]);
209            self.visible.push(index);
210        }
211        self.snapshot = None;
212    }
213
214    fn rebuild_visible(&mut self) {
215        self.visible.clear();
216        self.visible_ceiling = u64::MAX;
217        for (index, event) in self.all.iter().enumerate() {
218            if let EventPayload::SessionReverted { through_seq } = &event.payload {
219                self.visible_ceiling = self.visible_ceiling.min(*through_seq);
220                let all = &self.all;
221                self.visible
222                    .retain(|candidate| all[*candidate].seq <= self.visible_ceiling);
223            }
224            self.visible.push(index);
225        }
226        self.rebuild_index();
227        self.snapshot = None;
228    }
229
230    fn rebuild_index(&mut self) {
231        self.index = EventIndex::default();
232        for index in &self.visible {
233            self.index.observe(&self.all[*index]);
234        }
235    }
236
237    fn snapshot(&mut self) -> Arc<[StoredEvent]> {
238        self.snapshot
239            .get_or_insert_with(|| {
240                Arc::from(
241                    self.visible
242                        .iter()
243                        .map(|index| self.all[*index].clone())
244                        .collect::<Vec<_>>(),
245                )
246            })
247            .clone()
248    }
249}
250
251fn usage_total(seq: u64, usage: &cookie_agent_protocol::Usage) -> Option<(u64, u64)> {
252    let input = usage.input_tokens;
253    let output = usage.output_tokens;
254    (input.is_some() || output.is_some()).then(|| {
255        (
256            seq,
257            input
258                .unwrap_or_default()
259                .saturating_add(output.unwrap_or_default()),
260        )
261    })
262}
263
264impl EventLogWriter {
265    fn open(path: &Path) -> Result<Arc<Self>, EventLogError> {
266        let created = !path.exists();
267        let mut options = OpenOptions::new();
268        options.create(true).append(true);
269        #[cfg(unix)]
270        {
271            use std::os::unix::fs::OpenOptionsExt as _;
272            options.mode(0o600);
273        }
274        #[cfg(windows)]
275        {
276            use std::os::windows::fs::OpenOptionsExt as _;
277
278            const FILE_SHARE_READ: u32 = 0x1;
279            const FILE_SHARE_WRITE: u32 = 0x2;
280            const FILE_SHARE_DELETE: u32 = 0x4;
281            options.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
282        }
283        let file = options.open(path).map_err(|source| EventLogError::Io {
284            path: path.to_owned(),
285            source,
286        })?;
287        let shared = Arc::new(EventLogWriterShared {
288            path: path.to_owned(),
289            state: Mutex::new(EventLogWriterState {
290                output: BufWriter::new(file),
291                unsynced_bytes: 0,
292                sync_deadline: None,
293                directory_sync_pending: created,
294                background_error: None,
295                #[cfg(test)]
296                background_sync_paused: false,
297                shutdown: false,
298            }),
299            wake: Condvar::new(),
300            #[cfg(test)]
301            before_sync: Mutex::new(None),
302        });
303        let worker_shared = shared.clone();
304        let worker = thread::Builder::new()
305            .name("event-log-sync".into())
306            .spawn(move || event_log_sync_worker(&worker_shared))
307            .map_err(|source| EventLogError::Io {
308                path: path.to_owned(),
309                source,
310            })?;
311        Ok(Arc::new(Self {
312            shared,
313            worker: Mutex::new(Some(worker)),
314        }))
315    }
316
317    fn append(&self, bytes: &[u8], barrier: bool) -> Result<(), EventLogError> {
318        let mut state = self
319            .shared
320            .state
321            .lock()
322            .unwrap_or_else(|poisoned| poisoned.into_inner());
323        if let Some(failure) = state.background_error.take() {
324            return Err(self.shared.failure(failure));
325        }
326        state
327            .output
328            .write_all(bytes)
329            .and_then(|()| state.output.write_all(b"\n"))
330            .map_err(|source| self.shared.io_error(source))?;
331        state.unsynced_bytes = state.unsynced_bytes.saturating_add(bytes.len() + 1);
332        if barrier || state.unsynced_bytes >= EVENT_SYNC_BYTES {
333            self.shared.sync(&mut state)?;
334        } else if state.sync_deadline.is_none() {
335            state.sync_deadline = Some(Instant::now() + EVENT_SYNC_WINDOW);
336            self.shared.wake.notify_one();
337        }
338        Ok(())
339    }
340
341    fn flush(&self) -> Result<(), EventLogError> {
342        let mut state = self
343            .shared
344            .state
345            .lock()
346            .unwrap_or_else(|poisoned| poisoned.into_inner());
347        if let Some(failure) = state.background_error.take() {
348            return Err(self.shared.failure(failure));
349        }
350        if state.unsynced_bytes > 0 || state.directory_sync_pending {
351            self.shared.sync(&mut state)?;
352        }
353        Ok(())
354    }
355
356    fn shutdown(&self) {
357        {
358            let mut state = self
359                .shared
360                .state
361                .lock()
362                .unwrap_or_else(|poisoned| poisoned.into_inner());
363            state.shutdown = true;
364            self.shared.wake.notify_one();
365        }
366        if let Some(worker) = self
367            .worker
368            .lock()
369            .unwrap_or_else(|poisoned| poisoned.into_inner())
370            .take()
371        {
372            let _ = worker.join();
373        }
374    }
375
376    #[cfg(test)]
377    fn install_sync_hook(&self) -> (std::sync::mpsc::Receiver<()>, std::sync::mpsc::Sender<()>) {
378        let (reached, reached_receiver) = std::sync::mpsc::channel();
379        let (release, release_receiver) = std::sync::mpsc::channel();
380        *self
381            .shared
382            .before_sync
383            .lock()
384            .expect("event log sync hook lock poisoned") = Some(SyncHook {
385            reached,
386            release: release_receiver,
387        });
388        (reached_receiver, release)
389    }
390
391    #[cfg(test)]
392    fn pause_background_sync(&self) {
393        self.shared
394            .state
395            .lock()
396            .expect("event log writer state lock poisoned")
397            .background_sync_paused = true;
398        self.shared.wake.notify_one();
399    }
400}
401
402impl EventLogWriterShared {
403    fn sync(&self, state: &mut EventLogWriterState) -> Result<(), EventLogError> {
404        #[cfg(test)]
405        if let Some(hook) = self
406            .before_sync
407            .lock()
408            .expect("event log sync hook lock poisoned")
409            .take()
410        {
411            let _ = hook.reached.send(());
412            let _ = hook.release.recv();
413        }
414        state
415            .output
416            .flush()
417            .and_then(|()| state.output.get_ref().sync_data())
418            .map_err(|source| self.io_error(source))?;
419        if state.directory_sync_pending
420            && let Some(parent) = self.path.parent()
421        {
422            fsync_directory(parent)?;
423            state.directory_sync_pending = false;
424        }
425        state.unsynced_bytes = 0;
426        state.sync_deadline = None;
427        Ok(())
428    }
429
430    fn io_error(&self, source: io::Error) -> EventLogError {
431        EventLogError::Io {
432            path: self.path.clone(),
433            source,
434        }
435    }
436
437    fn failure(&self, failure: WriterFailure) -> EventLogError {
438        self.io_error(io::Error::new(failure.kind, failure.message))
439    }
440}
441
442fn event_log_sync_worker(shared: &EventLogWriterShared) {
443    let mut state = shared
444        .state
445        .lock()
446        .unwrap_or_else(|poisoned| poisoned.into_inner());
447    loop {
448        if state.shutdown {
449            if state.unsynced_bytes > 0 || state.directory_sync_pending {
450                let _ = shared.sync(&mut state);
451            }
452            return;
453        }
454        #[cfg(test)]
455        if state.background_sync_paused {
456            state = shared
457                .wake
458                .wait(state)
459                .unwrap_or_else(|poisoned| poisoned.into_inner());
460            continue;
461        }
462        let Some(deadline) = state.sync_deadline else {
463            state = shared
464                .wake
465                .wait(state)
466                .unwrap_or_else(|poisoned| poisoned.into_inner());
467            continue;
468        };
469        let now = Instant::now();
470        if now < deadline {
471            let (next_state, _) = shared
472                .wake
473                .wait_timeout(state, deadline - now)
474                .unwrap_or_else(|poisoned| poisoned.into_inner());
475            state = next_state;
476            continue;
477        }
478        if let Err(EventLogError::Io { source, .. }) = shared.sync(&mut state) {
479            state.background_error = Some(WriterFailure {
480                kind: source.kind(),
481                message: source.to_string(),
482            });
483            state.sync_deadline = None;
484        }
485    }
486}
487
488#[derive(Clone, Debug, Eq, PartialEq)]
489pub struct EventLoadDiagnostic {
490    pub seq: u64,
491    pub reason: String,
492    pub engine_version: Option<String>,
493    pub skipped: bool,
494}
495
496impl EventLog {
497    #[cfg(test)]
498    pub fn create(
499        path: PathBuf,
500        session_id: SessionId,
501        origin: EventOrigin,
502        creation: EventPayload,
503    ) -> Result<Arc<Self>, EventLogError> {
504        let authority = WriteAuthority::new();
505        let capability = authority.capability();
506        Self::create_owned(
507            path,
508            session_id,
509            origin,
510            creation,
511            capability,
512            true,
513            Some(authority),
514        )
515    }
516
517    fn create_owned(
518        path: PathBuf,
519        session_id: SessionId,
520        origin: EventOrigin,
521        creation: EventPayload,
522        capability: WriteCapability,
523        persisted: bool,
524        _test_authority: Option<WriteAuthority>,
525    ) -> Result<Arc<Self>, EventLogError> {
526        let log = Arc::new(Self {
527            path,
528            session_id,
529            append: Mutex::new(()),
530            events: Mutex::new(EventStorage::new(Vec::new())),
531            diagnostics: Vec::new(),
532            initial_validation_taint: ValidationTaint::default(),
533            validation: Mutex::new(ValidationState::default()),
534            next_seq: AtomicU64::new(1),
535            persisted: AtomicBool::new(persisted),
536            read_only: false,
537            write_capability: Some(capability),
538            #[cfg(test)]
539            _test_authority,
540            #[cfg(test)]
541            append_authorization_hook: Mutex::new(None),
542            writer: Mutex::new(None),
543        });
544        if !matches!(creation, EventPayload::SessionCreated { .. }) {
545            return Err(EventLogError::MissingCreation(log.path.clone()));
546        }
547        log.append_inner(None, None, origin, creation)?;
548        Ok(log)
549    }
550
551    #[cfg(test)]
552    pub fn create_buffered(
553        path: PathBuf,
554        session_id: SessionId,
555        origin: EventOrigin,
556        creation: EventPayload,
557    ) -> Result<Arc<Self>, EventLogError> {
558        let authority = WriteAuthority::new();
559        let capability = authority.capability();
560        Self::create_owned(
561            path,
562            session_id,
563            origin,
564            creation,
565            capability,
566            false,
567            Some(authority),
568        )
569    }
570
571    pub(crate) fn create_buffered_owned(
572        path: PathBuf,
573        session_id: SessionId,
574        origin: EventOrigin,
575        creation: EventPayload,
576        capability: WriteCapability,
577    ) -> Result<Arc<Self>, EventLogError> {
578        Self::create_owned(path, session_id, origin, creation, capability, false, None)
579    }
580
581    #[cfg(test)]
582    pub fn open(path: PathBuf, session_id: SessionId) -> Result<Arc<Self>, EventLogError> {
583        let authority = WriteAuthority::new();
584        let capability = authority.capability();
585        Self::open_with_mode(path, session_id, false, Some(capability), Some(authority))
586    }
587
588    pub(crate) fn open_owned(
589        path: PathBuf,
590        session_id: SessionId,
591        capability: WriteCapability,
592    ) -> Result<Arc<Self>, EventLogError> {
593        Self::open_with_mode(path, session_id, false, Some(capability), None)
594    }
595
596    pub fn open_read_only(
597        path: PathBuf,
598        session_id: SessionId,
599    ) -> Result<Arc<Self>, EventLogError> {
600        Self::open_with_mode(path, session_id, true, None, None)
601    }
602
603    fn open_with_mode(
604        path: PathBuf,
605        session_id: SessionId,
606        read_only: bool,
607        write_capability: Option<WriteCapability>,
608        _test_authority: Option<WriteAuthority>,
609    ) -> Result<Arc<Self>, EventLogError> {
610        let loaded = load_event_jsonl(
611            &path,
612            if read_only {
613                TornTail::Ignore
614            } else {
615                TornTail::Truncate
616            },
617        )?;
618        let records = loaded.records;
619        if !matches!(
620            records.first().map(|record| &record.payload),
621            Some(EventPayload::SessionCreated { .. })
622        ) {
623            return Err(EventLogError::MissingCreation(path));
624        }
625        let validation =
626            validate_records(&path, session_id, &records, &loaded.validation_taint, None)?;
627        Ok(Arc::new(Self {
628            path,
629            session_id,
630            append: Mutex::new(()),
631            events: Mutex::new(EventStorage::new(records)),
632            diagnostics: loaded.diagnostics,
633            initial_validation_taint: loaded.validation_taint,
634            validation: Mutex::new(validation),
635            next_seq: AtomicU64::new(loaded.next_seq),
636            persisted: AtomicBool::new(true),
637            read_only,
638            write_capability,
639            #[cfg(test)]
640            _test_authority,
641            #[cfg(test)]
642            append_authorization_hook: Mutex::new(None),
643            writer: Mutex::new(None),
644        }))
645    }
646
647    pub(crate) fn append_owned(
648        &self,
649        capability: &WriteCapability,
650        run_id: Option<RunId>,
651        origin: EventOrigin,
652        payload: EventPayload,
653    ) -> Result<StoredEvent, EventLogError> {
654        if !self
655            .write_capability
656            .as_ref()
657            .is_some_and(|expected| capability.authorizes(expected))
658        {
659            return Err(EventLogError::ReadOnly(self.path.clone()));
660        }
661        #[cfg(test)]
662        if let Some(hook) = self
663            .append_authorization_hook
664            .lock()
665            .expect("append authorization hook lock poisoned")
666            .take()
667        {
668            let _ = hook.reached.send(());
669            let _ = hook.release.recv();
670        }
671        self.append_inner(Some(capability), run_id, origin, payload)
672    }
673
674    #[cfg(test)]
675    pub fn append(
676        &self,
677        run_id: Option<RunId>,
678        origin: EventOrigin,
679        payload: EventPayload,
680    ) -> Result<StoredEvent, EventLogError> {
681        let capability = self
682            .write_capability
683            .as_ref()
684            .ok_or_else(|| EventLogError::ReadOnly(self.path.clone()))?;
685        self.append_owned(capability, run_id, origin, payload)
686    }
687
688    fn append_inner(
689        &self,
690        capability: Option<&WriteCapability>,
691        run_id: Option<RunId>,
692        origin: EventOrigin,
693        payload: EventPayload,
694    ) -> Result<StoredEvent, EventLogError> {
695        if self.read_only {
696            return Err(EventLogError::ReadOnly(self.path.clone()));
697        }
698        let _append = self
699            .append
700            .lock()
701            .unwrap_or_else(|poisoned| poisoned.into_inner());
702        if capability.is_some_and(|capability| {
703            !self
704                .write_capability
705                .as_ref()
706                .is_some_and(|expected| capability.authorizes(expected))
707        }) {
708            return Err(EventLogError::ReadOnly(self.path.clone()));
709        }
710        let events = self
711            .events
712            .lock()
713            .unwrap_or_else(|poisoned| poisoned.into_inner());
714        let event = StoredEvent {
715            engine_version: Some(env!("CARGO_PKG_VERSION").to_owned()),
716            origin: Some(origin),
717            session_id: self.session_id,
718            run_id,
719            seq: self.next_seq.load(Ordering::Acquire),
720            timestamp: Timestamp::now(),
721            payload,
722        };
723        event.validate().map_err(|error| EventLogError::Corrupt {
724            path: self.path.clone(),
725            message: error.to_string(),
726        })?;
727        let bytes = serde_json::to_vec(&event).map_err(|source| EventLogError::Json {
728            path: self.path.clone(),
729            source,
730        })?;
731        let mut validation = self
732            .validation
733            .lock()
734            .unwrap_or_else(|poisoned| poisoned.into_inner());
735        if let Err(error) =
736            validate_record_incremental(&self.path, self.session_id, &event, &mut validation, true)
737        {
738            *validation = validate_records(
739                &self.path,
740                self.session_id,
741                &events.all,
742                &self.initial_validation_taint,
743                None,
744            )?;
745            return Err(error);
746        }
747        drop(validation);
748        drop(events);
749        let write_result = if self.persisted.load(Ordering::Acquire) {
750            self.persistent_writer().and_then(|writer| {
751                writer.append(&bytes, event_requires_durable_barrier(&event.payload))
752            })
753        } else {
754            Ok(())
755        };
756        if let Err(error) = write_result {
757            let events = self
758                .events
759                .lock()
760                .unwrap_or_else(|poisoned| poisoned.into_inner());
761            let mut validation = self
762                .validation
763                .lock()
764                .unwrap_or_else(|poisoned| poisoned.into_inner());
765            *validation = validate_records(
766                &self.path,
767                self.session_id,
768                &events.all,
769                &self.initial_validation_taint,
770                None,
771            )?;
772            return Err(error);
773        }
774        let mut events = self
775            .events
776            .lock()
777            .unwrap_or_else(|poisoned| poisoned.into_inner());
778        events.push(event.clone());
779        self.next_seq.store(event.seq + 1, Ordering::Release);
780        Ok(event)
781    }
782
783    #[must_use]
784    pub fn events(&self) -> Vec<StoredEvent> {
785        self.event_snapshot().to_vec()
786    }
787
788    #[must_use]
789    pub fn event_snapshot(&self) -> Arc<[StoredEvent]> {
790        self.events
791            .lock()
792            .unwrap_or_else(|poisoned| poisoned.into_inner())
793            .snapshot()
794    }
795
796    #[must_use]
797    pub fn all_events(&self) -> Vec<StoredEvent> {
798        self.events
799            .lock()
800            .unwrap_or_else(|poisoned| poisoned.into_inner())
801            .all
802            .to_vec()
803    }
804
805    #[must_use]
806    pub fn last_event(&self) -> Option<StoredEvent> {
807        self.events
808            .lock()
809            .unwrap_or_else(|poisoned| poisoned.into_inner())
810            .all
811            .last()
812            .cloned()
813    }
814
815    pub(crate) fn last_run_started(
816        &self,
817    ) -> Option<(u64, RunId, cookie_agent_protocol::ModelSelection)> {
818        self.events
819            .lock()
820            .unwrap_or_else(|poisoned| poisoned.into_inner())
821            .index
822            .last_run_started
823            .clone()
824    }
825
826    pub(crate) fn latest_checkpoint_seq(&self) -> u64 {
827        self.events
828            .lock()
829            .unwrap_or_else(|poisoned| poisoned.into_inner())
830            .index
831            .last_checkpoint_seq
832    }
833
834    pub(crate) fn latest_real_usage(&self) -> Option<(u64, u64)> {
835        self.events
836            .lock()
837            .unwrap_or_else(|poisoned| poisoned.into_inner())
838            .index
839            .latest_real_usage()
840    }
841
842    pub(crate) fn checkpoint_covers_input(&self, input_through_seq: u64) -> bool {
843        self.events
844            .lock()
845            .unwrap_or_else(|poisoned| poisoned.into_inner())
846            .index
847            .last_checkpoint_input_through_seq
848            >= input_through_seq
849    }
850
851    #[must_use]
852    pub fn diagnostics(&self) -> &[EventLoadDiagnostic] {
853        &self.diagnostics
854    }
855
856    #[must_use]
857    pub(crate) fn delegation_event_tainted(&self, event: &StoredEvent) -> bool {
858        delegation_invocation_from_event(&event.payload).is_some_and(|invocation_id| {
859            self.validation
860                .lock()
861                .unwrap_or_else(|poisoned| poisoned.into_inner())
862                .taint
863                .delegation_before(invocation_id, event.seq)
864        })
865    }
866
867    #[must_use]
868    pub fn physical_tip_seq(&self) -> u64 {
869        self.next_seq.load(Ordering::Acquire).saturating_sub(1)
870    }
871
872    #[must_use]
873    pub fn path(&self) -> &Path {
874        &self.path
875    }
876
877    #[must_use]
878    pub fn is_persisted(&self) -> bool {
879        self.persisted.load(Ordering::Acquire)
880    }
881
882    pub fn mark_persisted(&self) {
883        self.persisted.store(true, Ordering::Release);
884    }
885
886    pub(crate) fn flush(&self) -> Result<(), EventLogError> {
887        if self.read_only {
888            return Ok(());
889        }
890        let _append = self
891            .append
892            .lock()
893            .unwrap_or_else(|poisoned| poisoned.into_inner());
894        let writer = self
895            .writer
896            .lock()
897            .unwrap_or_else(|poisoned| poisoned.into_inner())
898            .clone();
899        if let Some(writer) = writer {
900            writer.flush()?;
901        }
902        Ok(())
903    }
904
905    pub(crate) fn suspend_writer(&self) -> Result<(), EventLogError> {
906        if self.read_only {
907            return Ok(());
908        }
909        let _append = self
910            .append
911            .lock()
912            .unwrap_or_else(|poisoned| poisoned.into_inner());
913        let writer = self
914            .writer
915            .lock()
916            .unwrap_or_else(|poisoned| poisoned.into_inner())
917            .take();
918        if let Some(writer) = writer {
919            let result = writer.flush();
920            writer.shutdown();
921            result?;
922        }
923        Ok(())
924    }
925
926    fn persistent_writer(&self) -> Result<Arc<EventLogWriter>, EventLogError> {
927        if self.read_only {
928            return Err(EventLogError::ReadOnly(self.path.clone()));
929        }
930        let mut writer = self
931            .writer
932            .lock()
933            .unwrap_or_else(|poisoned| poisoned.into_inner());
934        if writer.is_none() {
935            *writer = Some(EventLogWriter::open(&self.path)?);
936        }
937        Ok(writer
938            .as_ref()
939            .expect("event log writer initialized")
940            .clone())
941    }
942
943    #[cfg(test)]
944    pub(crate) fn install_sync_hook_for_test(
945        &self,
946    ) -> (std::sync::mpsc::Receiver<()>, std::sync::mpsc::Sender<()>) {
947        self.persistent_writer()
948            .expect("open event log writer")
949            .install_sync_hook()
950    }
951
952    #[cfg(test)]
953    pub(crate) fn install_append_authorization_hook_for_test(
954        &self,
955    ) -> (std::sync::mpsc::Receiver<()>, std::sync::mpsc::Sender<()>) {
956        let (reached, reached_receiver) = std::sync::mpsc::channel();
957        let (release, release_receiver) = std::sync::mpsc::channel();
958        *self
959            .append_authorization_hook
960            .lock()
961            .expect("append authorization hook lock poisoned") = Some(AppendAuthorizationHook {
962            reached,
963            release: release_receiver,
964        });
965        (reached_receiver, release)
966    }
967
968    #[cfg(test)]
969    pub(crate) fn pause_background_sync_for_test(&self) {
970        self.persistent_writer()
971            .expect("open event log writer")
972            .pause_background_sync();
973    }
974
975    #[cfg(test)]
976    pub(crate) fn writer_is_open_for_test(&self) -> bool {
977        self.writer
978            .lock()
979            .expect("event log writer lock poisoned")
980            .is_some()
981    }
982
983    #[cfg(test)]
984    fn snapshot_lock_available_for_test(&self) -> bool {
985        self.events.try_lock().is_ok()
986    }
987}
988
989impl Drop for EventLog {
990    fn drop(&mut self) {
991        if let Some(writer) = self
992            .writer
993            .lock()
994            .unwrap_or_else(|poisoned| poisoned.into_inner())
995            .take()
996        {
997            writer.shutdown();
998        }
999    }
1000}
1001
1002fn event_requires_durable_barrier(payload: &EventPayload) -> bool {
1003    match payload {
1004        EventPayload::TextDelta { .. }
1005        | EventPayload::ReasoningDelta { .. }
1006        | EventPayload::ToolCallProgress { .. } => false,
1007        EventPayload::SessionCreated { .. }
1008        | EventPayload::SessionReverted { .. }
1009        | EventPayload::SessionPermissionOverlaySet { .. }
1010        | EventPayload::SkillLoaded { .. }
1011        | EventPayload::SkillInvocationNoted { .. }
1012        | EventPayload::AgentMdLoaded { .. }
1013        | EventPayload::AgentMdSkipped { .. }
1014        | EventPayload::PluginEventAdded { .. }
1015        | EventPayload::PluginDiagnostic { .. }
1016        | EventPayload::GoalActivated { .. }
1017        | EventPayload::GoalChecklistRevised { .. }
1018        | EventPayload::GoalLifecycleChanged { .. }
1019        | EventPayload::ProducerMessageAccepted { .. }
1020        | EventPayload::ProducerMessageAdmitted { .. }
1021        | EventPayload::ProducerMessagesClaimed { .. }
1022        | EventPayload::ProducerMessagesReleased { .. }
1023        | EventPayload::ProducerMessageConsumed { .. }
1024        | EventPayload::ProducerMessageDiscarded { .. }
1025        | EventPayload::RunStarted { .. }
1026        | EventPayload::MessageInjected { .. }
1027        | EventPayload::UserInputAdmitted { .. }
1028        | EventPayload::UserInputSubmitted { .. }
1029        | EventPayload::UserInputTransformed { .. }
1030        | EventPayload::UserInputRecalled { .. }
1031        | EventPayload::UserInputRecalledV2 { .. }
1032        | EventPayload::UserInputApplied { .. }
1033        | EventPayload::DelegatedContextSeeded { .. }
1034        | EventPayload::RunCompleted { .. }
1035        | EventPayload::RunFailed { .. }
1036        | EventPayload::RunCancelled { .. }
1037        | EventPayload::RunInterrupted { .. }
1038        | EventPayload::ModelAttemptStarted { .. }
1039        | EventPayload::ModelRequestPrepared { .. }
1040        | EventPayload::AttemptAbandoned { .. }
1041        | EventPayload::ModelReplayEvaluated { .. }
1042        | EventPayload::ModelTurnCommitted { .. }
1043        | EventPayload::ModelUsageRecorded { .. }
1044        | EventPayload::ModelFallback { .. }
1045        | EventPayload::ToolCallStarted { .. }
1046        | EventPayload::ToolCallTerminated { .. }
1047        | EventPayload::ToolOutputElided { .. }
1048        | EventPayload::ToolStdinSubmitted { .. }
1049        | EventPayload::ToolCallLinked { .. }
1050        | EventPayload::DelegationReserved { .. }
1051        | EventPayload::DelegationStarted { .. }
1052        | EventPayload::DelegationRunStarted { .. }
1053        | EventPayload::DelegationRunAttached { .. }
1054        | EventPayload::DelegationFinished { .. }
1055        | EventPayload::DelegateQueued { .. }
1056        | EventPayload::DelegateFinished { .. }
1057        | EventPayload::DelegateFinishedV2 { .. }
1058        | EventPayload::DelegateChildTerminated { .. }
1059        | EventPayload::ApprovalRequested { .. }
1060        | EventPayload::ApprovalEvaluated { .. }
1061        | EventPayload::ApprovalEscalated { .. }
1062        | EventPayload::ApprovalUserDecisionRecorded { .. }
1063        | EventPayload::ApprovalFinalized { .. }
1064        | EventPayload::ApprovalCancelled { .. }
1065        | EventPayload::ApprovalDoomLoopDetected { .. }
1066        | EventPayload::TreeApprovalGrantCommitted { .. }
1067        | EventPayload::InternalAgentStarted { .. }
1068        | EventPayload::InternalAgentCompleted { .. }
1069        | EventPayload::InternalAgentUsageRecorded { .. }
1070        | EventPayload::InternalAgentFailed { .. }
1071        | EventPayload::InternalAgentCancelled { .. }
1072        | EventPayload::InternalAgentInterrupted { .. }
1073        | EventPayload::InternalAgentFallback { .. }
1074        | EventPayload::ContextCheckpointCommitted { .. }
1075        | EventPayload::ContextRehydrated { .. }
1076        | EventPayload::SessionTitleCommitted { .. } => true,
1077    }
1078}
1079
1080#[derive(Clone, Debug, PartialEq)]
1081struct RunAttribution {
1082    start_seq: u64,
1083    agent_id: cookie_agent_protocol::AgentId,
1084    prompt_fingerprint: cookie_agent_protocol::Sha256Digest,
1085    selected_suffix: Vec<cookie_agent_protocol::ResolvedModelRef>,
1086    active_fallback_index: usize,
1087    next_attempt_ordinal: u32,
1088    attempts_on_active: u32,
1089    active_attempt: Option<AttemptId>,
1090    ordering_tainted: bool,
1091}
1092
1093#[derive(Clone, Debug, PartialEq)]
1094struct AttemptAttribution {
1095    run_id: RunId,
1096    resolved_model: cookie_agent_protocol::ResolvedModelRef,
1097    finished: bool,
1098}
1099
1100#[derive(Clone, Debug, PartialEq)]
1101struct InternalRunAttribution {
1102    start_seq: u64,
1103    invocation_id: cookie_agent_protocol::InternalAgentInvocationId,
1104    kind: cookie_agent_protocol::InternalAgentKind,
1105    run_id: RunId,
1106    active_model: Option<cookie_agent_protocol::ResolvedModelRef>,
1107    usage_recorded_in_phase: bool,
1108    model_phase_taint_seen: u64,
1109    usage_phase_taint_seen: u64,
1110}
1111
1112#[derive(Clone, Debug, PartialEq)]
1113struct DelegationAttribution {
1114    parent_run_id: RunId,
1115    child_session_id: SessionId,
1116    resume: bool,
1117    started: bool,
1118    child_run_id: Option<RunId>,
1119    finished: bool,
1120}
1121
1122#[derive(Clone, Debug, PartialEq)]
1123struct ValidationState {
1124    taint: ValidationTaint,
1125    runs: HashMap<RunId, RunAttribution>,
1126    approval_owners: HashMap<cookie_agent_protocol::ApprovalId, RunId>,
1127    attempts: HashMap<AttemptId, AttemptAttribution>,
1128    turns: HashMap<u64, (RunId, cookie_agent_protocol::PersistedModelTurn)>,
1129    turn_models: HashMap<u64, cookie_agent_protocol::ResolvedModelRef>,
1130    usage_turns: HashSet<u64>,
1131    internal_runs: HashMap<cookie_agent_protocol::InternalAgentRunId, InternalRunAttribution>,
1132    delegations: HashMap<cookie_agent_protocol::InvocationId, DelegationAttribution>,
1133    model_call_owners: HashMap<(RunId, ModelCallId), AssistantToolCallRef>,
1134    provider_item_owners: HashMap<(RunId, ProviderItemId), AssistantToolCallRef>,
1135    tool_starts: HashMap<ToolCallId, (RunId, ToolCallStart)>,
1136    terminated_tools: HashSet<ToolCallId>,
1137    elided_tools: HashSet<ToolCallId>,
1138    admissions: HashMap<u64, (RunId, String)>,
1139    next_model_turn_seq: u64,
1140    previous_seq: Option<u64>,
1141    previous_timestamp: Option<Timestamp>,
1142    active_run: Option<RunId>,
1143    record_count: usize,
1144}
1145
1146impl ValidationState {
1147    fn new(taint: ValidationTaint) -> Self {
1148        Self {
1149            taint,
1150            runs: HashMap::new(),
1151            approval_owners: HashMap::new(),
1152            attempts: HashMap::new(),
1153            turns: HashMap::new(),
1154            turn_models: HashMap::new(),
1155            usage_turns: HashSet::new(),
1156            internal_runs: HashMap::new(),
1157            delegations: HashMap::new(),
1158            model_call_owners: HashMap::new(),
1159            provider_item_owners: HashMap::new(),
1160            tool_starts: HashMap::new(),
1161            terminated_tools: HashSet::new(),
1162            elided_tools: HashSet::new(),
1163            admissions: HashMap::new(),
1164            next_model_turn_seq: 1,
1165            previous_seq: None,
1166            previous_timestamp: None,
1167            active_run: None,
1168            record_count: 0,
1169        }
1170    }
1171
1172    fn finish_record(&mut self, record: &StoredEvent) {
1173        self.previous_seq = Some(record.seq);
1174        self.previous_timestamp = Some(record.timestamp);
1175        self.record_count += 1;
1176    }
1177}
1178
1179impl Default for ValidationState {
1180    fn default() -> Self {
1181        Self::new(ValidationTaint::default())
1182    }
1183}
1184
1185#[derive(Clone, Debug, Default, PartialEq)]
1186struct ValidationTaint {
1187    broad: Vec<(u64, u64)>,
1188    runs: HashMap<RunId, u64>,
1189    attempts: HashMap<AttemptId, u64>,
1190    attempt_runs: HashMap<RunId, u64>,
1191    turns: HashMap<u64, u64>,
1192    tools: HashMap<ToolCallId, u64>,
1193    approvals: HashMap<cookie_agent_protocol::ApprovalId, u64>,
1194    internal_runs: HashMap<cookie_agent_protocol::InternalAgentRunId, u64>,
1195    internal_model_phases: HashMap<cookie_agent_protocol::InternalAgentRunId, Vec<u64>>,
1196    internal_usage_phases: HashMap<cookie_agent_protocol::InternalAgentRunId, Vec<u64>>,
1197    tool_terminals: HashMap<ToolCallId, u64>,
1198    admissions: HashMap<u64, u64>,
1199    delegations: HashMap<cookie_agent_protocol::InvocationId, u64>,
1200    delegation_repairs: HashMap<cookie_agent_protocol::InvocationId, u64>,
1201    run_ordering: HashMap<RunId, u64>,
1202    turn_ordering: Option<u64>,
1203    active_run_ordering: Option<u64>,
1204}
1205
1206const MAX_INTERNAL_PHASE_TAINTS_PER_RUN: usize = 64;
1207const INTERNAL_PHASE_TAINT_LIMIT_MESSAGE: &str =
1208    "internal-agent phase taint history exceeds the 64-transition per-run limit";
1209
1210impl ValidationTaint {
1211    fn broad_before(&self, seq: u64) -> bool {
1212        self.broad.iter().any(|(start, _)| *start < seq)
1213    }
1214
1215    fn keyed_before<K: Eq + std::hash::Hash>(
1216        &self,
1217        taints: &HashMap<K, u64>,
1218        key: &K,
1219        seq: u64,
1220    ) -> bool {
1221        self.broad_before(seq) || taints.get(key).is_some_and(|tainted| *tainted < seq)
1222    }
1223
1224    fn run_before(&self, run_id: RunId, seq: u64) -> bool {
1225        self.keyed_before(&self.runs, &run_id, seq)
1226    }
1227
1228    fn attempt_before(&self, attempt_id: AttemptId, seq: u64) -> bool {
1229        self.keyed_before(&self.attempts, &attempt_id, seq)
1230    }
1231
1232    fn turn_before(&self, model_turn_seq: u64, seq: u64) -> bool {
1233        self.keyed_before(&self.turns, &model_turn_seq, seq)
1234    }
1235
1236    fn tool_before(&self, tool_call_id: ToolCallId, seq: u64) -> bool {
1237        self.keyed_before(&self.tools, &tool_call_id, seq)
1238    }
1239
1240    fn approval_before(&self, approval_id: cookie_agent_protocol::ApprovalId, seq: u64) -> bool {
1241        self.keyed_before(&self.approvals, &approval_id, seq)
1242    }
1243
1244    fn internal_run_before(
1245        &self,
1246        internal_run_id: cookie_agent_protocol::InternalAgentRunId,
1247        seq: u64,
1248    ) -> bool {
1249        self.keyed_before(&self.internal_runs, &internal_run_id, seq)
1250    }
1251
1252    fn latest_broad_before(&self, seq: u64) -> Option<u64> {
1253        self.broad
1254            .iter()
1255            .filter(|(start, _)| *start < seq)
1256            .map(|(_, end)| *end)
1257            .max()
1258    }
1259
1260    fn internal_model_phase_taint_before(
1261        &self,
1262        internal_run_id: cookie_agent_protocol::InternalAgentRunId,
1263        seq: u64,
1264    ) -> Option<u64> {
1265        self.latest_broad_before(seq).max(
1266            self.internal_model_phases
1267                .get(&internal_run_id)
1268                .into_iter()
1269                .flatten()
1270                .copied()
1271                .filter(|tainted| *tainted < seq)
1272                .max(),
1273        )
1274    }
1275
1276    fn internal_usage_phase_taint_before(
1277        &self,
1278        internal_run_id: cookie_agent_protocol::InternalAgentRunId,
1279        seq: u64,
1280    ) -> Option<u64> {
1281        self.latest_broad_before(seq).max(
1282            self.internal_usage_phases
1283                .get(&internal_run_id)
1284                .into_iter()
1285                .flatten()
1286                .copied()
1287                .filter(|tainted| *tainted < seq)
1288                .max(),
1289        )
1290    }
1291
1292    fn tool_terminal_before(&self, tool_call_id: ToolCallId, seq: u64) -> bool {
1293        self.keyed_before(&self.tool_terminals, &tool_call_id, seq)
1294    }
1295
1296    fn admission_before(&self, admission_seq: u64, seq: u64) -> bool {
1297        self.keyed_before(&self.admissions, &admission_seq, seq)
1298    }
1299
1300    fn delegation_before(
1301        &self,
1302        invocation_id: cookie_agent_protocol::InvocationId,
1303        seq: u64,
1304    ) -> bool {
1305        self.broad_before(seq)
1306            || self.delegations.get(&invocation_id).is_some_and(|tainted| {
1307                *tainted < seq
1308                    && !self
1309                        .delegation_repairs
1310                        .get(&invocation_id)
1311                        .is_some_and(|repair| *repair >= *tainted && *repair <= seq)
1312            })
1313    }
1314
1315    fn delegation_unrepaired_before(
1316        &self,
1317        invocation_id: cookie_agent_protocol::InvocationId,
1318        seq: u64,
1319    ) -> bool {
1320        self.broad_before(seq)
1321            || self
1322                .delegations
1323                .get(&invocation_id)
1324                .is_some_and(|tainted| *tainted < seq)
1325    }
1326
1327    fn run_ordering_between(&self, run_id: RunId, start_seq: u64, seq: u64) -> bool {
1328        self.broad
1329            .iter()
1330            .any(|(start, end)| *start < seq && *end > start_seq)
1331            || self
1332                .run_ordering
1333                .get(&run_id)
1334                .is_some_and(|tainted| *tainted > start_seq && *tainted < seq)
1335            || self
1336                .attempt_runs
1337                .get(&run_id)
1338                .is_some_and(|tainted| *tainted > start_seq && *tainted < seq)
1339    }
1340
1341    fn turn_ordering_before(&self, seq: u64) -> bool {
1342        self.broad_before(seq) || self.turn_ordering.is_some_and(|tainted| tainted < seq)
1343    }
1344
1345    fn active_run_ordering_before(&self, seq: u64) -> bool {
1346        self.broad_before(seq)
1347            || self
1348                .active_run_ordering
1349                .is_some_and(|tainted| tainted < seq)
1350    }
1351
1352    fn mark_broad(&mut self, start: u64, end: u64) {
1353        self.broad.push((start, end));
1354    }
1355
1356    fn mark_record(
1357        &mut self,
1358        seq: u64,
1359        run_id: Option<RunId>,
1360        payload: &Value,
1361    ) -> Result<(), &'static str> {
1362        match payload.get("type").and_then(Value::as_str) {
1363            Some("run_started") => {
1364                if let Some(run_id) = run_id {
1365                    self.runs.entry(run_id).or_insert(seq);
1366                    self.run_ordering.entry(run_id).or_insert(seq);
1367                }
1368                self.active_run_ordering.get_or_insert(seq);
1369            }
1370            Some("model_attempt_started") => {
1371                if let Some(attempt_id) = json_field(payload, "attempt_id") {
1372                    self.attempts.entry(attempt_id).or_insert(seq);
1373                }
1374                if let Some(run_id) = run_id {
1375                    self.attempt_runs.entry(run_id).or_insert(seq);
1376                    self.run_ordering.entry(run_id).or_insert(seq);
1377                }
1378            }
1379            Some("attempt_abandoned") | Some("model_fallback") => {
1380                if let Some(run_id) = run_id {
1381                    self.run_ordering.entry(run_id).or_insert(seq);
1382                }
1383            }
1384            Some("model_turn_committed") => {
1385                if let Some(model_turn_seq) = payload.get("model_turn_seq").and_then(Value::as_u64)
1386                {
1387                    self.turns.entry(model_turn_seq).or_insert(seq);
1388                }
1389                self.turn_ordering.get_or_insert(seq);
1390                if let Some(run_id) = run_id {
1391                    self.run_ordering.entry(run_id).or_insert(seq);
1392                }
1393            }
1394            Some("tool_call_started") => {
1395                if let Some(tool_call_id) = json_field(payload, "tool_call_id") {
1396                    self.tools.entry(tool_call_id).or_insert(seq);
1397                }
1398            }
1399            Some("tool_call_terminated") => {
1400                if let Some(tool_call_id) = json_field(payload, "tool_call_id") {
1401                    self.tool_terminals.entry(tool_call_id).or_insert(seq);
1402                }
1403            }
1404            Some("approval_requested") => {
1405                if let Some(approval_id) = payload
1406                    .get("request")
1407                    .and_then(|request| json_field(request, "approval_id"))
1408                {
1409                    self.approvals.entry(approval_id).or_insert(seq);
1410                }
1411            }
1412            Some("internal_agent_started") => {
1413                if let Some(internal_run_id) = json_field(payload, "internal_run_id") {
1414                    self.internal_runs.entry(internal_run_id).or_insert(seq);
1415                }
1416            }
1417            Some("internal_agent_fallback") => {
1418                if let Some(internal_run_id) = json_field(payload, "internal_run_id") {
1419                    self.mark_internal_fallback(internal_run_id, seq)?;
1420                }
1421            }
1422            Some("internal_agent_usage_recorded") => {
1423                if let Some(internal_run_id) = json_field(payload, "internal_run_id") {
1424                    self.mark_internal_usage(internal_run_id, seq)?;
1425                }
1426            }
1427            Some("user_input_admitted") => {
1428                self.admissions.entry(seq).or_insert(seq);
1429            }
1430            Some(
1431                "delegation_reserved"
1432                | "delegation_started"
1433                | "delegation_run_started"
1434                | "delegation_run_attached"
1435                | "delegation_finished",
1436            ) => {
1437                if let Some(invocation_id) = delegation_invocation_from_value(payload) {
1438                    self.delegations.entry(invocation_id).or_insert(seq);
1439                }
1440            }
1441            Some("run_completed" | "run_failed" | "run_cancelled" | "run_interrupted") => {
1442                self.active_run_ordering.get_or_insert(seq);
1443            }
1444            _ => {}
1445        }
1446        Ok(())
1447    }
1448
1449    fn mark_event(&mut self, event: &StoredEvent) -> Result<(), &'static str> {
1450        let seq = event.seq;
1451        match &event.payload {
1452            EventPayload::RunStarted { .. } => {
1453                if let Some(run_id) = event.run_id {
1454                    self.runs.entry(run_id).or_insert(seq);
1455                    self.run_ordering.entry(run_id).or_insert(seq);
1456                }
1457                self.active_run_ordering.get_or_insert(seq);
1458            }
1459            EventPayload::ModelAttemptStarted { attempt_id, .. } => {
1460                self.attempts.entry(*attempt_id).or_insert(seq);
1461                if let Some(run_id) = event.run_id {
1462                    self.attempt_runs.entry(run_id).or_insert(seq);
1463                    self.run_ordering.entry(run_id).or_insert(seq);
1464                }
1465            }
1466            EventPayload::AttemptAbandoned { .. } | EventPayload::ModelFallback { .. } => {
1467                if let Some(run_id) = event.run_id {
1468                    self.run_ordering.entry(run_id).or_insert(seq);
1469                }
1470            }
1471            EventPayload::ModelTurnCommitted { model_turn_seq, .. } => {
1472                self.turns.entry(*model_turn_seq).or_insert(seq);
1473                self.turn_ordering.get_or_insert(seq);
1474                if let Some(run_id) = event.run_id {
1475                    self.run_ordering.entry(run_id).or_insert(seq);
1476                }
1477            }
1478            EventPayload::ToolCallStarted { start } => {
1479                self.tools.entry(start.tool_call_id).or_insert(seq);
1480            }
1481            EventPayload::ToolCallTerminated { termination } => {
1482                self.tool_terminals
1483                    .entry(termination.tool_call_id)
1484                    .or_insert(seq);
1485            }
1486            EventPayload::ApprovalRequested { request } => {
1487                self.approvals.entry(request.approval_id()).or_insert(seq);
1488            }
1489            EventPayload::InternalAgentStarted {
1490                internal_run_id, ..
1491            } => {
1492                self.internal_runs.entry(*internal_run_id).or_insert(seq);
1493            }
1494            EventPayload::InternalAgentFallback {
1495                internal_run_id, ..
1496            } => {
1497                self.mark_internal_fallback(*internal_run_id, seq)?;
1498            }
1499            EventPayload::InternalAgentUsageRecorded {
1500                internal_run_id, ..
1501            } => {
1502                self.mark_internal_usage(*internal_run_id, seq)?;
1503            }
1504            EventPayload::UserInputAdmitted { .. } => {
1505                self.admissions.entry(seq).or_insert(seq);
1506            }
1507            EventPayload::DelegationReserved { reservation, .. } => {
1508                self.delegations
1509                    .entry(reservation.invocation_id)
1510                    .or_insert(seq);
1511            }
1512            EventPayload::DelegationStarted { invocation_id, .. }
1513            | EventPayload::DelegationRunStarted { invocation_id, .. }
1514            | EventPayload::DelegationRunAttached { invocation_id, .. }
1515            | EventPayload::DelegationFinished { invocation_id, .. } => {
1516                self.delegations.entry(*invocation_id).or_insert(seq);
1517            }
1518            EventPayload::RunCompleted { .. }
1519            | EventPayload::RunFailed { .. }
1520            | EventPayload::RunCancelled { .. }
1521            | EventPayload::RunInterrupted { .. } => {
1522                self.active_run_ordering.get_or_insert(seq);
1523            }
1524            _ => {}
1525        }
1526        Ok(())
1527    }
1528
1529    fn mark_internal_fallback(
1530        &mut self,
1531        internal_run_id: cookie_agent_protocol::InternalAgentRunId,
1532        seq: u64,
1533    ) -> Result<(), &'static str> {
1534        let model_len = self
1535            .internal_model_phases
1536            .get(&internal_run_id)
1537            .map_or(0, Vec::len);
1538        let usage_len = self
1539            .internal_usage_phases
1540            .get(&internal_run_id)
1541            .map_or(0, Vec::len);
1542        if model_len >= MAX_INTERNAL_PHASE_TAINTS_PER_RUN
1543            || usage_len >= MAX_INTERNAL_PHASE_TAINTS_PER_RUN
1544        {
1545            return Err(INTERNAL_PHASE_TAINT_LIMIT_MESSAGE);
1546        }
1547        self.internal_model_phases
1548            .entry(internal_run_id)
1549            .or_default()
1550            .push(seq);
1551        self.internal_usage_phases
1552            .entry(internal_run_id)
1553            .or_default()
1554            .push(seq);
1555        Ok(())
1556    }
1557
1558    fn mark_internal_usage(
1559        &mut self,
1560        internal_run_id: cookie_agent_protocol::InternalAgentRunId,
1561        seq: u64,
1562    ) -> Result<(), &'static str> {
1563        let usage = self
1564            .internal_usage_phases
1565            .entry(internal_run_id)
1566            .or_default();
1567        if usage.len() >= MAX_INTERNAL_PHASE_TAINTS_PER_RUN {
1568            return Err(INTERNAL_PHASE_TAINT_LIMIT_MESSAGE);
1569        }
1570        usage.push(seq);
1571        Ok(())
1572    }
1573}
1574
1575fn json_field<T: for<'de> Deserialize<'de>>(value: &Value, field: &str) -> Option<T> {
1576    serde_json::from_value(value.get(field)?.clone()).ok()
1577}
1578
1579fn delegation_invocation_from_value(
1580    payload: &Value,
1581) -> Option<cookie_agent_protocol::InvocationId> {
1582    match payload.get("type").and_then(Value::as_str) {
1583        Some("delegation_reserved") => payload
1584            .get("reservation")
1585            .and_then(|reservation| json_field(reservation, "invocation_id")),
1586        Some(
1587            "delegation_started"
1588            | "delegation_run_started"
1589            | "delegation_run_attached"
1590            | "delegation_finished",
1591        ) => json_field(payload, "invocation_id"),
1592        _ => None,
1593    }
1594}
1595
1596fn delegation_invocation_from_event(
1597    payload: &EventPayload,
1598) -> Option<cookie_agent_protocol::InvocationId> {
1599    match payload {
1600        EventPayload::DelegationReserved { reservation, .. } => Some(reservation.invocation_id),
1601        EventPayload::DelegationStarted { invocation_id, .. }
1602        | EventPayload::DelegationRunStarted { invocation_id, .. }
1603        | EventPayload::DelegationRunAttached { invocation_id, .. }
1604        | EventPayload::DelegationFinished { invocation_id, .. } => Some(*invocation_id),
1605        _ => None,
1606    }
1607}
1608
1609fn validate_observed_duplicates(path: &Path, records: &[StoredEvent]) -> Result<(), EventLogError> {
1610    let mut runs = HashSet::new();
1611    let mut attempts = HashSet::new();
1612    let mut turns = HashSet::new();
1613    let mut usage_turns = HashSet::new();
1614    let mut internal_runs = HashSet::new();
1615    let mut tool_starts = HashSet::new();
1616    let mut tool_terminations = HashSet::new();
1617    let mut approvals = HashSet::new();
1618    let mut model_calls = HashSet::new();
1619    let mut provider_items = HashSet::new();
1620    for record in records {
1621        match &record.payload {
1622            EventPayload::RunStarted { .. } => {
1623                let Some(run_id) = record.run_id else {
1624                    return corrupt(path, "RunStarted is missing run_id");
1625                };
1626                if !runs.insert(run_id) {
1627                    return corrupt(path, "run_id has more than one RunStarted event");
1628                }
1629            }
1630            EventPayload::ModelAttemptStarted { attempt_id, .. } => {
1631                if !attempts.insert(*attempt_id) {
1632                    return corrupt(path, "attempt_id has more than one ModelAttemptStarted");
1633                }
1634            }
1635            EventPayload::ModelTurnCommitted {
1636                model_turn_seq,
1637                turn,
1638                ..
1639            } => {
1640                if !turns.insert(*model_turn_seq) {
1641                    return corrupt(path, "model turn sequence is duplicated");
1642                }
1643                if let Some(run_id) = record.run_id {
1644                    for part in &turn.content {
1645                        if let cookie_agent_protocol::PersistedAssistantPart::ToolCall {
1646                            id,
1647                            provider_item_id,
1648                            ..
1649                        } = part
1650                        {
1651                            if !model_calls.insert((run_id, id.clone())) {
1652                                return corrupt(path, "model call id is reused within a run");
1653                            }
1654                            if let Some(provider_item_id) = provider_item_id
1655                                && !provider_items.insert((run_id, provider_item_id.clone()))
1656                            {
1657                                return corrupt(path, "provider item id is reused within a run");
1658                            }
1659                        }
1660                    }
1661                }
1662            }
1663            EventPayload::ModelUsageRecorded { model_turn_seq, .. } => {
1664                if !usage_turns.insert(*model_turn_seq) {
1665                    return corrupt(path, "usage ownership does not match its model turn");
1666                }
1667            }
1668            EventPayload::InternalAgentStarted {
1669                internal_run_id, ..
1670            } => {
1671                if !internal_runs.insert(*internal_run_id) {
1672                    return corrupt(path, "internal_run_id has more than one start");
1673                }
1674            }
1675            EventPayload::ToolCallStarted { start } => {
1676                if !tool_starts.insert(start.tool_call_id) {
1677                    return corrupt(path, "tool_call_id has more than one start");
1678                }
1679            }
1680            EventPayload::ToolCallTerminated { termination } => {
1681                if !tool_terminations.insert(termination.tool_call_id) {
1682                    return corrupt(path, "tool call has more than one terminal event");
1683                }
1684            }
1685            EventPayload::ApprovalRequested { request }
1686                if !approvals.insert(request.approval_id()) =>
1687            {
1688                return corrupt(
1689                    path,
1690                    "approval_id has more than one ApprovalRequested event",
1691                );
1692            }
1693            _ => {}
1694        }
1695    }
1696    Ok(())
1697}
1698
1699fn validate_record_local(path: &Path, record: &StoredEvent) -> Result<(), EventLogError> {
1700    match &record.payload {
1701        EventPayload::SessionReverted { through_seq } => {
1702            if record.run_id.is_some() || *through_seq == 0 || *through_seq >= record.seq {
1703                return corrupt(
1704                    path,
1705                    "SessionReverted target is not an existing prior event",
1706                );
1707            }
1708        }
1709        EventPayload::SessionPermissionOverlaySet { .. } if record.run_id.is_some() => {
1710            return corrupt(path, "SessionPermissionOverlaySet must not have run_id");
1711        }
1712        EventPayload::DelegateChildTerminated { .. } if record.run_id.is_some() => {
1713            return corrupt(path, "DelegateChildTerminated must not have run_id");
1714        }
1715        EventPayload::DelegatedContextSeeded { .. } if record.run_id.is_some() => {
1716            return corrupt(path, "DelegatedContextSeeded must be runless");
1717        }
1718        EventPayload::PluginEventAdded { .. } | EventPayload::PluginDiagnostic { .. }
1719            if record.run_id.is_some() =>
1720        {
1721            return corrupt(path, "plugin events must be runless");
1722        }
1723        EventPayload::SessionTitleCommitted { change, .. } => {
1724            let runless = matches!(
1725                change,
1726                cookie_agent_protocol::SessionTitleChange::UserSet { .. }
1727                    | cookie_agent_protocol::SessionTitleChange::UserClear { .. }
1728                    | cookie_agent_protocol::SessionTitleChange::UserReset { .. }
1729                    | cookie_agent_protocol::SessionTitleChange::DelegatedSet { .. }
1730            );
1731            if runless != record.run_id.is_none() {
1732                return corrupt(path, "SessionTitleCommitted has inconsistent run ownership");
1733            }
1734        }
1735        _ => {}
1736    }
1737    Ok(())
1738}
1739
1740fn validate_records(
1741    path: &Path,
1742    session_id: SessionId,
1743    records: &[StoredEvent],
1744    initial_taint: &ValidationTaint,
1745    strict_from_seq: Option<u64>,
1746) -> Result<ValidationState, EventLogError> {
1747    validate_observed_duplicates(path, records)?;
1748    let mut state = ValidationState::new(initial_taint.clone());
1749    for record in records {
1750        let strict = strict_from_seq.is_some_and(|from| record.seq >= from);
1751        validate_record_incremental(path, session_id, record, &mut state, strict)?;
1752    }
1753    Ok(state)
1754}
1755
1756fn validate_record_incremental(
1757    path: &Path,
1758    session_id: SessionId,
1759    record: &StoredEvent,
1760    state: &mut ValidationState,
1761    strict: bool,
1762) -> Result<(), EventLogError> {
1763    if state
1764        .previous_seq
1765        .is_some_and(|previous| record.seq <= previous)
1766    {
1767        return corrupt(
1768            path,
1769            format!(
1770                "event sequence {} is not strictly greater than {}",
1771                record.seq,
1772                state.previous_seq.expect("checked previous sequence")
1773            ),
1774        );
1775    }
1776    if record.session_id != session_id {
1777        return corrupt(
1778            path,
1779            "event envelope session ID does not match its directory",
1780        );
1781    }
1782    if state
1783        .previous_timestamp
1784        .is_some_and(|timestamp| record.timestamp < timestamp)
1785    {
1786        return corrupt(path, "event timestamps are not monotonic");
1787    }
1788    if state.record_count == 0 {
1789        let EventPayload::SessionCreated { .. } = &record.payload else {
1790            return Err(EventLogError::MissingCreation(path.to_owned()));
1791        };
1792        if record.run_id.is_some() {
1793            return corrupt(path, "invalid initial SessionCreated record");
1794        }
1795        state.finish_record(record);
1796        return Ok(());
1797    }
1798    if matches!(record.payload, EventPayload::SessionCreated { .. }) {
1799        return corrupt(path, "SessionCreated appeared after sequence 1");
1800    }
1801    validate_record_local(path, record)?;
1802    let ValidationState {
1803        taint,
1804        runs,
1805        approval_owners,
1806        attempts,
1807        turns,
1808        turn_models,
1809        usage_turns,
1810        internal_runs,
1811        delegations,
1812        model_call_owners,
1813        provider_item_owners,
1814        tool_starts,
1815        terminated_tools,
1816        elided_tools,
1817        admissions,
1818        next_model_turn_seq,
1819        active_run,
1820        ..
1821    } = state;
1822    if let EventPayload::UserInputAdmitted { input } = &record.payload
1823        && let Some(run_id) = record.run_id
1824    {
1825        admissions.insert(record.seq, (run_id, input.clone()));
1826    }
1827    let missing_admission = |user_input_seq: u64, run_id: RunId, input: &str| {
1828        !admissions
1829            .get(&user_input_seq)
1830            .is_some_and(|(owner, admitted)| *owner == run_id && admitted == input)
1831    };
1832    let tainted_prerequisite = if strict {
1833        false
1834    } else {
1835        match &record.payload {
1836            EventPayload::SkillLoaded { .. } | EventPayload::SkillInvocationNoted { .. }
1837                if record.run_id.is_some() =>
1838            {
1839                record.run_id.is_some_and(|run_id| {
1840                    !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1841                })
1842            }
1843            EventPayload::SessionTitleCommitted { .. } if record.run_id.is_some() => {
1844                record.run_id.is_some_and(|run_id| {
1845                    !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1846                })
1847            }
1848            EventPayload::UserInputRecalledV2 {
1849                user_input_seq,
1850                input,
1851            } => record.run_id.is_some_and(|run_id| {
1852                missing_admission(*user_input_seq, run_id, input)
1853                    && taint.admission_before(*user_input_seq, record.seq)
1854            }),
1855            EventPayload::ModelAttemptStarted { .. }
1856            | EventPayload::InternalAgentStarted { .. }
1857            | EventPayload::ModelFallback { .. }
1858            | EventPayload::ApprovalRequested { .. } => record.run_id.is_some_and(|run_id| {
1859                !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1860            }),
1861            EventPayload::TextDelta { attempt_id, .. }
1862            | EventPayload::ReasoningDelta { attempt_id, .. }
1863            | EventPayload::ModelRequestPrepared { attempt_id, .. }
1864            | EventPayload::AttemptAbandoned { attempt_id, .. }
1865            | EventPayload::ModelReplayEvaluated { attempt_id, .. }
1866            | EventPayload::ModelTurnCommitted { attempt_id, .. } => {
1867                (!attempts.contains_key(attempt_id)
1868                    && taint.attempt_before(*attempt_id, record.seq))
1869                    || record.run_id.is_some_and(|run_id| {
1870                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1871                    })
1872            }
1873            EventPayload::ModelUsageRecorded { model_turn_seq, .. } => {
1874                (!turns.contains_key(model_turn_seq)
1875                    && taint.turn_before(*model_turn_seq, record.seq))
1876                    || record.run_id.is_some_and(|run_id| {
1877                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1878                    })
1879            }
1880            EventPayload::InternalAgentFallback {
1881                internal_run_id, ..
1882            }
1883            | EventPayload::InternalAgentUsageRecorded {
1884                internal_run_id, ..
1885            } => {
1886                (!internal_runs.contains_key(internal_run_id)
1887                    && taint.internal_run_before(*internal_run_id, record.seq))
1888                    || record.run_id.is_some_and(|run_id| {
1889                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1890                    })
1891            }
1892            EventPayload::ToolCallStarted { start } => {
1893                (!turns.contains_key(&start.owner.model_turn_seq)
1894                    && taint.turn_before(start.owner.model_turn_seq, record.seq))
1895                    || record.run_id.is_some_and(|run_id| {
1896                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1897                    })
1898            }
1899            EventPayload::ToolCallTerminated { termination } => {
1900                !tool_starts.contains_key(&termination.tool_call_id)
1901                    && taint.tool_before(termination.tool_call_id, record.seq)
1902            }
1903            EventPayload::ToolOutputElided { tool_call_id, .. }
1904            | EventPayload::ToolCallProgress { tool_call_id, .. }
1905            | EventPayload::ToolStdinSubmitted { tool_call_id, .. }
1906            | EventPayload::ToolCallLinked { tool_call_id, .. } => {
1907                !tool_starts.contains_key(tool_call_id)
1908                    && taint.tool_before(*tool_call_id, record.seq)
1909            }
1910            EventPayload::ApprovalEvaluated { approval_id, .. }
1911            | EventPayload::ApprovalEscalated { approval_id, .. }
1912            | EventPayload::ApprovalUserDecisionRecorded { approval_id, .. }
1913            | EventPayload::ApprovalFinalized { approval_id, .. }
1914            | EventPayload::ApprovalCancelled { approval_id, .. }
1915            | EventPayload::ApprovalDoomLoopDetected { approval_id, .. } => {
1916                !approval_owners.contains_key(approval_id)
1917                    && taint.approval_before(*approval_id, record.seq)
1918            }
1919            EventPayload::TreeApprovalGrantCommitted { grant } => {
1920                !approval_owners.contains_key(&grant.approval_id)
1921                    && taint.approval_before(grant.approval_id, record.seq)
1922            }
1923            EventPayload::DelegationReserved { reservation, .. } => {
1924                taint.delegation_before(reservation.invocation_id, record.seq)
1925                    || record.run_id.is_some_and(|run_id| {
1926                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1927                    })
1928            }
1929            EventPayload::DelegationStarted { invocation_id, .. }
1930            | EventPayload::DelegationRunStarted { invocation_id, .. }
1931            | EventPayload::DelegationRunAttached { invocation_id, .. } => {
1932                taint.delegation_before(*invocation_id, record.seq)
1933                    || record.run_id.is_some_and(|run_id| {
1934                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1935                    })
1936            }
1937            EventPayload::DelegationFinished {
1938                invocation_id,
1939                child_session_id,
1940                child_run_id,
1941                ..
1942            } => {
1943                let repair_matches = delegations.get(invocation_id).is_some_and(|delegation| {
1944                    record.run_id == Some(delegation.parent_run_id)
1945                        && *child_session_id == delegation.child_session_id
1946                        && *child_run_id == delegation.child_run_id
1947                        && !delegation.finished
1948                });
1949                (taint.delegation_before(*invocation_id, record.seq) && !repair_matches)
1950                    || record.run_id.is_some_and(|run_id| {
1951                        !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1952                    })
1953            }
1954            _ => record.run_id.is_some_and(|run_id| {
1955                !runs.contains_key(&run_id) && taint.run_before(run_id, record.seq)
1956            }),
1957        }
1958    };
1959    if tainted_prerequisite {
1960        taint
1961            .mark_event(record)
1962            .map_err(|message| EventLogError::Corrupt {
1963                path: path.to_owned(),
1964                message: message.into(),
1965            })?;
1966        state.finish_record(record);
1967        return Ok(());
1968    }
1969    match &record.payload {
1970        EventPayload::SessionReverted { through_seq } => {
1971            if record.run_id.is_some() || *through_seq == 0 || *through_seq >= record.seq {
1972                return corrupt(
1973                    path,
1974                    "SessionReverted target is not an existing prior event",
1975                );
1976            }
1977        }
1978        EventPayload::SessionPermissionOverlaySet { .. } => {
1979            if record.run_id.is_some() {
1980                return corrupt(path, "SessionPermissionOverlaySet must not have run_id");
1981            }
1982        }
1983        EventPayload::DelegationReserved {
1984            reservation,
1985            request,
1986            ..
1987        } => {
1988            if record.run_id != Some(reservation.parent_run_id)
1989                || record.session_id != reservation.parent_session_id
1990                || !runs.contains_key(&reservation.parent_run_id)
1991                || delegations
1992                    .insert(
1993                        reservation.invocation_id,
1994                        DelegationAttribution {
1995                            parent_run_id: reservation.parent_run_id,
1996                            child_session_id: reservation.child_session_id,
1997                            resume: request.resume_session_id.is_some(),
1998                            started: false,
1999                            child_run_id: None,
2000                            finished: false,
2001                        },
2002                    )
2003                    .is_some()
2004            {
2005                return corrupt(path, "delegation reservation ownership is invalid");
2006            }
2007        }
2008        EventPayload::DelegationStarted {
2009            invocation_id,
2010            child_session_id,
2011        } => {
2012            let Some(delegation) = delegations.get_mut(invocation_id) else {
2013                return corrupt(path, "delegation start appeared before its reservation");
2014            };
2015            if record.run_id != Some(delegation.parent_run_id)
2016                || *child_session_id != delegation.child_session_id
2017                || delegation.started
2018                || delegation.finished
2019            {
2020                return corrupt(path, "delegation start ownership is invalid");
2021            }
2022            delegation.started = true;
2023        }
2024        EventPayload::DelegationRunStarted {
2025            invocation_id,
2026            child_run_id,
2027        }
2028        | EventPayload::DelegationRunAttached {
2029            invocation_id,
2030            child_run_id,
2031        } => {
2032            let attached = matches!(&record.payload, EventPayload::DelegationRunAttached { .. });
2033            let Some(delegation) = delegations.get_mut(invocation_id) else {
2034                return corrupt(path, "delegation run appeared before its reservation");
2035            };
2036            if record.run_id != Some(delegation.parent_run_id)
2037                || delegation.child_run_id.is_some()
2038                || delegation.finished
2039                || (attached && !delegation.resume)
2040            {
2041                return corrupt(path, "delegation run ownership is invalid");
2042            }
2043            delegation.child_run_id = Some(*child_run_id);
2044        }
2045        EventPayload::DelegationFinished {
2046            invocation_id,
2047            child_session_id,
2048            child_run_id,
2049            ..
2050        } => {
2051            let Some(delegation) = delegations.get_mut(invocation_id) else {
2052                return corrupt(path, "delegation finish appeared before its reservation");
2053            };
2054            if record.run_id != Some(delegation.parent_run_id)
2055                || *child_session_id != delegation.child_session_id
2056                || *child_run_id != delegation.child_run_id
2057                || delegation.finished
2058            {
2059                return corrupt(path, "delegation finish ownership is invalid");
2060            }
2061            if taint.delegation_unrepaired_before(*invocation_id, record.seq) {
2062                taint.delegation_repairs.insert(*invocation_id, record.seq);
2063            }
2064            delegation.finished = true;
2065        }
2066        EventPayload::SkillLoaded { .. } | EventPayload::SkillInvocationNoted { .. } => {
2067            if record.run_id.is_some() {
2068                require_started_run(path, runs, record.run_id)?;
2069            }
2070        }
2071        EventPayload::DelegateChildTerminated { .. } => {
2072            if record.run_id.is_some() {
2073                return corrupt(path, "DelegateChildTerminated must not have run_id");
2074            }
2075        }
2076        EventPayload::UserInputAdmitted { .. } | EventPayload::UserInputRecalled { .. }
2077            if record.run_id.is_none() =>
2078        {
2079            if active_run.is_some() && !taint.active_run_ordering_before(record.seq) {
2080                return corrupt(path, "runless UserInputAdmitted requires no active run");
2081            }
2082            if taint.active_run_ordering_before(record.seq) {
2083                *active_run = None;
2084            }
2085        }
2086        EventPayload::UserInputRecalledV2 {
2087            user_input_seq,
2088            input,
2089        } => {
2090            let Some(run_id) = record.run_id else {
2091                return corrupt(path, "UserInputRecalledV2 is missing run_id");
2092            };
2093            if missing_admission(*user_input_seq, run_id, input) {
2094                return corrupt(path, "UserInputRecalledV2 target is not a prior admission");
2095            }
2096        }
2097        EventPayload::DelegatedContextSeeded { .. } => {
2098            if record.run_id.is_some() || !runs.is_empty() {
2099                return corrupt(
2100                    path,
2101                    "DelegatedContextSeeded must be runless and precede the first run",
2102                );
2103            }
2104        }
2105        EventPayload::RunStarted {
2106            agent,
2107            selected_suffix,
2108            ..
2109        } => {
2110            let Some(run_id) = record.run_id else {
2111                return corrupt(path, "RunStarted is missing run_id");
2112            };
2113            let attribution = RunAttribution {
2114                start_seq: record.seq,
2115                agent_id: agent.agent.clone(),
2116                prompt_fingerprint: agent.prompt_fingerprint.clone(),
2117                selected_suffix: selected_suffix
2118                    .iter()
2119                    .map(crate::policy::wire_resolved)
2120                    .collect(),
2121                active_fallback_index: 0,
2122                next_attempt_ordinal: 1,
2123                attempts_on_active: 0,
2124                active_attempt: None,
2125                ordering_tainted: false,
2126            };
2127            if runs.insert(run_id, attribution).is_some() {
2128                return corrupt(path, "run_id has more than one RunStarted event");
2129            }
2130            *active_run = Some(run_id);
2131        }
2132        EventPayload::SessionTitleCommitted { change, .. } => {
2133            let user = matches!(
2134                change,
2135                cookie_agent_protocol::SessionTitleChange::UserSet { .. }
2136                    | cookie_agent_protocol::SessionTitleChange::UserClear { .. }
2137                    | cookie_agent_protocol::SessionTitleChange::UserReset { .. }
2138                    | cookie_agent_protocol::SessionTitleChange::DelegatedSet { .. }
2139            );
2140            if user != record.run_id.is_none() {
2141                return corrupt(path, "SessionTitleCommitted has inconsistent run ownership");
2142            }
2143            if let Some(run_id) = record.run_id
2144                && !runs.contains_key(&run_id)
2145            {
2146                return corrupt(path, "session title references a run before RunStarted");
2147            }
2148        }
2149        EventPayload::ModelAttemptStarted {
2150            attempt_id,
2151            attempt_ordinal,
2152            fallback_index,
2153            retry_ordinal,
2154            resolved_model,
2155            prompt_fingerprint,
2156        } => {
2157            let run_id = require_started_run(path, runs, record.run_id)?;
2158            if attempts.contains_key(attempt_id) {
2159                return corrupt(path, "attempt_id has more than one ModelAttemptStarted");
2160            }
2161            let run = runs.get_mut(&run_id).expect("started run is indexed");
2162            run.ordering_tainted |= taint.run_ordering_between(run_id, run.start_seq, record.seq);
2163            if strict && run.ordering_tainted {
2164                return corrupt(
2165                    path,
2166                    "cannot strictly append an attempt after missing run-order prerequisites",
2167                );
2168            }
2169            if !run.ordering_tainted && run.active_attempt.is_some() {
2170                return corrupt(
2171                    path,
2172                    "ModelAttemptStarted appeared before the prior attempt ended",
2173                );
2174            }
2175            if !run.ordering_tainted && *attempt_ordinal != run.next_attempt_ordinal {
2176                return corrupt(path, "attempt_ordinal is not contiguous within its run");
2177            }
2178            let Ok(fallback_index) = usize::try_from(*fallback_index) else {
2179                return corrupt(path, "fallback_index does not index the frozen suffix");
2180            };
2181            if !run.ordering_tainted && fallback_index != run.active_fallback_index {
2182                return corrupt(
2183                    path,
2184                    "attempt fallback_index is not the active frozen suffix entry",
2185                );
2186            }
2187            let Some(expected_model) = run.selected_suffix.get(fallback_index) else {
2188                return corrupt(path, "fallback_index does not index the frozen suffix");
2189            };
2190            if resolved_model != expected_model {
2191                return corrupt(
2192                    path,
2193                    "attempt resolved model does not match its frozen suffix entry",
2194                );
2195            }
2196            if prompt_fingerprint != &run.prompt_fingerprint {
2197                return corrupt(path, "attempt prompt fingerprint does not match RunStarted");
2198            }
2199            if !run.ordering_tainted && *retry_ordinal != run.attempts_on_active {
2200                return corrupt(
2201                    path,
2202                    "retry_ordinal is not contiguous for the active fallback entry",
2203                );
2204            }
2205            run.next_attempt_ordinal = attempt_ordinal.saturating_add(1);
2206            run.active_fallback_index = fallback_index;
2207            run.attempts_on_active = retry_ordinal.saturating_add(1);
2208            run.active_attempt = Some(*attempt_id);
2209            attempts.insert(
2210                *attempt_id,
2211                AttemptAttribution {
2212                    run_id,
2213                    resolved_model: resolved_model.clone(),
2214                    finished: false,
2215                },
2216            );
2217        }
2218        EventPayload::ModelRequestPrepared { attempt_id, .. }
2219        | EventPayload::TextDelta { attempt_id, .. }
2220        | EventPayload::ReasoningDelta { attempt_id, .. } => {
2221            validate_attempt_owner(path, attempts, *attempt_id, record.run_id)?;
2222        }
2223        EventPayload::AttemptAbandoned { attempt_id, .. } => {
2224            let run_id = validate_attempt_owner(path, attempts, *attempt_id, record.run_id)?;
2225            let run = runs.get_mut(&run_id).expect("started run is indexed");
2226            run.ordering_tainted |= taint.run_ordering_between(run_id, run.start_seq, record.seq);
2227            if strict && run.ordering_tainted {
2228                return corrupt(
2229                    path,
2230                    "cannot strictly append an attempt terminal after missing prerequisites",
2231                );
2232            }
2233            finish_attempt(path, runs, attempts, run_id, *attempt_id, false)?;
2234        }
2235        EventPayload::ModelReplayEvaluated {
2236            attempt_id,
2237            resolved_model,
2238            ..
2239        } => {
2240            validate_attempt_model(path, attempts, *attempt_id, record.run_id, resolved_model)?;
2241        }
2242        EventPayload::ModelTurnCommitted {
2243            attempt_id,
2244            model_turn_seq,
2245            resolved_model,
2246            turn,
2247            ..
2248        } => {
2249            let run_id =
2250                validate_attempt_model(path, attempts, *attempt_id, record.run_id, resolved_model)?;
2251            let run = runs.get_mut(&run_id).expect("started run is indexed");
2252            run.ordering_tainted |= taint.run_ordering_between(run_id, run.start_seq, record.seq);
2253            if strict && run.ordering_tainted {
2254                return corrupt(
2255                    path,
2256                    "cannot strictly append a model turn after missing prerequisites",
2257                );
2258            }
2259            finish_attempt(path, runs, attempts, run_id, *attempt_id, true)?;
2260            let turn_ordering_tainted = taint.turn_ordering_before(record.seq);
2261            if *model_turn_seq != *next_model_turn_seq && (strict || !turn_ordering_tainted) {
2262                return corrupt(path, "model_turn_seq is not contiguous");
2263            }
2264            *next_model_turn_seq = model_turn_seq.saturating_add(1);
2265            for (content_index, part) in turn.content.iter().enumerate() {
2266                if let cookie_agent_protocol::PersistedAssistantPart::ToolCall {
2267                    id,
2268                    provider_item_id,
2269                    ..
2270                } = part
2271                {
2272                    let owner = AssistantToolCallRef {
2273                        model_turn_seq: *model_turn_seq,
2274                        content_index: content_index as u32,
2275                        model_call_id: id.clone(),
2276                        provider_item_id: provider_item_id.clone(),
2277                    };
2278                    if model_call_owners
2279                        .insert((run_id, id.clone()), owner.clone())
2280                        .is_some()
2281                    {
2282                        return corrupt(path, "model call id is reused within a run");
2283                    }
2284                    if let Some(provider_item_id) = provider_item_id
2285                        && provider_item_owners
2286                            .insert((run_id, provider_item_id.clone()), owner)
2287                            .is_some()
2288                    {
2289                        return corrupt(path, "provider item id is reused within a run");
2290                    }
2291                }
2292            }
2293            if turns
2294                .insert(*model_turn_seq, (run_id, turn.clone()))
2295                .is_some()
2296            {
2297                return corrupt(path, "model turn sequence is duplicated");
2298            }
2299            turn_models.insert(*model_turn_seq, resolved_model.clone());
2300        }
2301        EventPayload::ModelUsageRecorded {
2302            model_turn_seq,
2303            agent_id,
2304            resolved_model,
2305            ..
2306        } => {
2307            let run_id = require_started_run(path, runs, record.run_id)?;
2308            let Some((turn_run, _)) = turns.get(model_turn_seq) else {
2309                return corrupt(path, "usage references an unknown committed model turn");
2310            };
2311            if *turn_run != run_id
2312                || turn_models.get(model_turn_seq) != Some(resolved_model)
2313                || runs.get(&run_id).map(|run| &run.agent_id) != Some(agent_id)
2314                || !usage_turns.insert(*model_turn_seq)
2315            {
2316                return corrupt(path, "usage ownership does not match its model turn");
2317            }
2318        }
2319        EventPayload::InternalAgentStarted {
2320            invocation_id,
2321            internal_run_id,
2322            kind,
2323            backend,
2324            ..
2325        } => {
2326            let run_id = require_started_run(path, runs, record.run_id)?;
2327            let active_model = match backend {
2328                cookie_agent_protocol::InternalAgentBackend::Model { resolved_model } => {
2329                    Some(resolved_model.clone())
2330                }
2331                cookie_agent_protocol::InternalAgentBackend::Builtin { .. } => None,
2332            };
2333            if internal_runs
2334                .insert(
2335                    *internal_run_id,
2336                    InternalRunAttribution {
2337                        start_seq: record.seq,
2338                        invocation_id: *invocation_id,
2339                        kind: *kind,
2340                        run_id,
2341                        active_model,
2342                        usage_recorded_in_phase: false,
2343                        model_phase_taint_seen: 0,
2344                        usage_phase_taint_seen: 0,
2345                    },
2346                )
2347                .is_some()
2348            {
2349                return corrupt(path, "internal_run_id has more than one start");
2350            }
2351        }
2352        EventPayload::InternalAgentFallback {
2353            invocation_id,
2354            internal_run_id,
2355            kind,
2356            from,
2357            to,
2358            ..
2359        } => {
2360            let run_id = require_started_run(path, runs, record.run_id)?;
2361            let Some(internal) = internal_runs.get_mut(internal_run_id) else {
2362                return corrupt(path, "internal fallback appeared before its start");
2363            };
2364            let from_model = match from {
2365                cookie_agent_protocol::InternalAgentBackend::Model { resolved_model } => {
2366                    Some(resolved_model)
2367                }
2368                cookie_agent_protocol::InternalAgentBackend::Builtin { .. } => None,
2369            };
2370            let model_phase_taint = taint
2371                .internal_model_phase_taint_before(*internal_run_id, record.seq)
2372                .filter(|tainted| {
2373                    *tainted > internal.start_seq && *tainted > internal.model_phase_taint_seen
2374                });
2375            if strict && model_phase_taint.is_some() {
2376                return corrupt(
2377                    path,
2378                    "cannot strictly append internal fallback after a missing phase transition",
2379                );
2380            }
2381            if internal.invocation_id != *invocation_id
2382                || internal.kind != *kind
2383                || internal.run_id != run_id
2384                || (model_phase_taint.is_none() && internal.active_model.as_ref() != from_model)
2385            {
2386                return corrupt(path, "internal fallback ownership does not match its run");
2387            }
2388            internal.active_model = match to {
2389                cookie_agent_protocol::InternalAgentBackend::Model { resolved_model } => {
2390                    Some(resolved_model.clone())
2391                }
2392                cookie_agent_protocol::InternalAgentBackend::Builtin { .. } => None,
2393            };
2394            internal.usage_recorded_in_phase = false;
2395            if let Some(tainted) = model_phase_taint {
2396                internal.model_phase_taint_seen = tainted;
2397            }
2398            if let Some(tainted) = taint
2399                .internal_usage_phase_taint_before(*internal_run_id, record.seq)
2400                .filter(|tainted| *tainted > internal.start_seq)
2401            {
2402                internal.usage_phase_taint_seen = tainted;
2403            }
2404        }
2405        EventPayload::InternalAgentUsageRecorded {
2406            internal_run_id,
2407            kind,
2408            agent_id,
2409            resolved_model,
2410            ..
2411        } => {
2412            let run_id = require_started_run(path, runs, record.run_id)?;
2413            let Some(internal) = internal_runs.get_mut(internal_run_id) else {
2414                return corrupt(path, "internal usage appeared before its start");
2415            };
2416            let expected_agent = cookie_agent_protocol::AgentId::new(match kind {
2417                cookie_agent_protocol::InternalAgentKind::Approval => {
2418                    cookie_agent_config::BUILT_IN_APPROVAL_AGENT_ID
2419                }
2420                cookie_agent_protocol::InternalAgentKind::ContextCompaction => {
2421                    cookie_agent_config::BUILT_IN_COMPACTION_AGENT_ID
2422                }
2423                cookie_agent_protocol::InternalAgentKind::SessionTitle => {
2424                    cookie_agent_config::BUILT_IN_TITLE_AGENT_ID
2425                }
2426            })
2427            .expect("built-in internal agent IDs are valid");
2428            let model_phase_taint = taint
2429                .internal_model_phase_taint_before(*internal_run_id, record.seq)
2430                .filter(|tainted| {
2431                    *tainted > internal.start_seq && *tainted > internal.model_phase_taint_seen
2432                });
2433            let usage_phase_taint = taint
2434                .internal_usage_phase_taint_before(*internal_run_id, record.seq)
2435                .filter(|tainted| {
2436                    *tainted > internal.start_seq && *tainted > internal.usage_phase_taint_seen
2437                });
2438            if strict && (model_phase_taint.is_some() || usage_phase_taint.is_some()) {
2439                return corrupt(
2440                    path,
2441                    "cannot strictly append internal usage after a missing phase transition",
2442                );
2443            }
2444            if internal.kind != *kind
2445                || internal.run_id != run_id
2446                || (model_phase_taint.is_none()
2447                    && internal.active_model.as_ref() != Some(resolved_model))
2448                || *agent_id != expected_agent
2449                || (usage_phase_taint.is_none() && internal.usage_recorded_in_phase)
2450            {
2451                return corrupt(path, "internal usage ownership does not match its run");
2452            }
2453            internal.active_model = Some(resolved_model.clone());
2454            internal.usage_recorded_in_phase = true;
2455            if let Some(tainted) = model_phase_taint {
2456                internal.model_phase_taint_seen = tainted;
2457            }
2458            if let Some(tainted) = usage_phase_taint {
2459                internal.usage_phase_taint_seen = tainted;
2460            }
2461        }
2462        EventPayload::ModelFallback {
2463            from,
2464            to,
2465            from_fallback_index,
2466            to_fallback_index,
2467            attempts_on_from,
2468            ..
2469        } => {
2470            let run_id = require_started_run(path, runs, record.run_id)?;
2471            let run = runs.get_mut(&run_id).expect("started run is indexed");
2472            run.ordering_tainted |= taint.run_ordering_between(run_id, run.start_seq, record.seq);
2473            if strict && run.ordering_tainted {
2474                return corrupt(
2475                    path,
2476                    "cannot strictly append fallback after missing run-order prerequisites",
2477                );
2478            }
2479            if !run.ordering_tainted && run.active_attempt.is_some() {
2480                return corrupt(
2481                    path,
2482                    "ModelFallback appeared before the active attempt ended",
2483                );
2484            }
2485            let Ok(from_index) = usize::try_from(*from_fallback_index) else {
2486                return corrupt(path, "ModelFallback index does not index the frozen suffix");
2487            };
2488            let Ok(to_index) = usize::try_from(*to_fallback_index) else {
2489                return corrupt(path, "ModelFallback index does not index the frozen suffix");
2490            };
2491            let Some(adjacent_index) = from_index.checked_add(1) else {
2492                return corrupt(path, "ModelFallback source index cannot advance");
2493            };
2494            if (!run.ordering_tainted && from_index != run.active_fallback_index)
2495                || to_index != adjacent_index
2496            {
2497                return corrupt(
2498                    path,
2499                    "ModelFallback transition is not adjacent from the active entry",
2500                );
2501            }
2502            let Some(expected_from) = run.selected_suffix.get(from_index) else {
2503                return corrupt(
2504                    path,
2505                    "ModelFallback source does not index the frozen suffix",
2506                );
2507            };
2508            let Some(expected_to) = run.selected_suffix.get(to_index) else {
2509                return corrupt(
2510                    path,
2511                    "ModelFallback target does not index the frozen suffix",
2512                );
2513            };
2514            if from != expected_from || to != expected_to {
2515                return corrupt(
2516                    path,
2517                    "ModelFallback models do not match the frozen suffix transition",
2518                );
2519            }
2520            if *attempts_on_from == 0
2521                || (!run.ordering_tainted && *attempts_on_from != run.attempts_on_active)
2522            {
2523                return corrupt(
2524                    path,
2525                    "ModelFallback attempt count does not match started attempts",
2526                );
2527            }
2528            run.active_fallback_index = to_index;
2529            run.attempts_on_active = 0;
2530        }
2531        EventPayload::ToolCallStarted { start } => {
2532            let run_id = require_started_run(path, runs, record.run_id)?;
2533            validate_tool_owner(
2534                path,
2535                run_id,
2536                turns,
2537                model_call_owners,
2538                provider_item_owners,
2539                &start.owner,
2540            )?;
2541            if tool_starts
2542                .insert(start.tool_call_id, (run_id, start.clone()))
2543                .is_some()
2544            {
2545                return corrupt(path, "tool_call_id has more than one start");
2546            }
2547        }
2548        EventPayload::ToolCallTerminated { termination } => {
2549            let Some((run_id, start)) = tool_starts.get(&termination.tool_call_id) else {
2550                return corrupt(path, "tool termination appeared before its start");
2551            };
2552            if record.run_id != Some(*run_id) || !termination.matches_start(start) {
2553                return corrupt(path, "tool termination ownership does not match its start");
2554            }
2555            if strict && taint.tool_terminal_before(termination.tool_call_id, record.seq) {
2556                return corrupt(
2557                    path,
2558                    "cannot strictly append tool termination after a missing terminal transition",
2559                );
2560            }
2561            if !terminated_tools.insert(termination.tool_call_id) {
2562                return corrupt(path, "tool call has more than one terminal event");
2563            }
2564        }
2565        EventPayload::ToolOutputElided { tool_call_id, .. } => {
2566            let Some((run_id, _)) = tool_starts.get(tool_call_id) else {
2567                return corrupt(path, "tool elision appeared before its start");
2568            };
2569            let terminal_tainted = taint.tool_terminal_before(*tool_call_id, record.seq);
2570            if record.run_id != Some(*run_id)
2571                || (!terminated_tools.contains(tool_call_id) && !terminal_tainted)
2572                || (strict && terminal_tainted)
2573                || !elided_tools.insert(*tool_call_id)
2574            {
2575                return corrupt(path, "tool elision ownership or ordering is invalid");
2576            }
2577        }
2578        EventPayload::ToolCallProgress { tool_call_id, .. }
2579        | EventPayload::ToolStdinSubmitted { tool_call_id, .. }
2580        | EventPayload::ToolCallLinked { tool_call_id, .. } => {
2581            let Some((run_id, _)) = tool_starts.get(tool_call_id) else {
2582                return corrupt(path, "tool lifecycle event appeared before its start");
2583            };
2584            let terminal_tainted = taint.tool_terminal_before(*tool_call_id, record.seq);
2585            if record.run_id != Some(*run_id)
2586                || terminated_tools.contains(tool_call_id)
2587                || (strict && terminal_tainted)
2588            {
2589                return corrupt(
2590                    path,
2591                    "tool lifecycle event has invalid ownership or ordering",
2592                );
2593            }
2594        }
2595        EventPayload::ApprovalRequested { request } => {
2596            let Some(run_id) = record.run_id else {
2597                return corrupt(path, "ApprovalRequested is missing run_id");
2598            };
2599            if !runs.contains_key(&run_id) {
2600                return corrupt(path, "approval references a run before RunStarted");
2601            }
2602            if approval_owners
2603                .insert(request.approval_id(), run_id)
2604                .is_some()
2605            {
2606                return corrupt(
2607                    path,
2608                    "approval_id has more than one ApprovalRequested event",
2609                );
2610            }
2611        }
2612        EventPayload::ApprovalEvaluated { approval_id, .. }
2613        | EventPayload::ApprovalEscalated { approval_id, .. }
2614        | EventPayload::ApprovalUserDecisionRecorded { approval_id, .. }
2615        | EventPayload::ApprovalFinalized { approval_id, .. }
2616        | EventPayload::ApprovalCancelled { approval_id, .. }
2617        | EventPayload::ApprovalDoomLoopDetected { approval_id, .. } => {
2618            validate_approval_owner(path, approval_owners, *approval_id, record.run_id)?;
2619        }
2620        EventPayload::TreeApprovalGrantCommitted { grant } => {
2621            validate_approval_owner(path, approval_owners, grant.approval_id, record.run_id)?;
2622        }
2623        EventPayload::PluginEventAdded { .. } | EventPayload::PluginDiagnostic { .. } => {
2624            if record.run_id.is_some() {
2625                return corrupt(path, "plugin events must be runless");
2626            }
2627        }
2628        EventPayload::GoalActivated { .. }
2629        | EventPayload::GoalChecklistRevised { .. }
2630        | EventPayload::GoalLifecycleChanged { .. }
2631        | EventPayload::ProducerMessageAccepted { .. }
2632        | EventPayload::ProducerMessageDiscarded { .. } => {
2633            if record.run_id.is_some() {
2634                require_started_run(path, runs, record.run_id)?;
2635            }
2636        }
2637        _ => {
2638            require_started_run(path, runs, record.run_id)?;
2639        }
2640    }
2641    if matches!(
2642        record.payload,
2643        EventPayload::RunCompleted { .. }
2644            | EventPayload::RunFailed { .. }
2645            | EventPayload::RunCancelled { .. }
2646            | EventPayload::RunInterrupted { .. }
2647    ) && record.run_id == *active_run
2648    {
2649        *active_run = None;
2650    }
2651    state.finish_record(record);
2652    Ok(())
2653}
2654
2655struct LoadedEvents {
2656    records: Vec<StoredEvent>,
2657    diagnostics: Vec<EventLoadDiagnostic>,
2658    validation_taint: ValidationTaint,
2659    next_seq: u64,
2660}
2661
2662fn load_event_jsonl(path: &Path, torn_tail: TornTail) -> Result<LoadedEvents, EventLogError> {
2663    let bytes = read_complete_jsonl(path, torn_tail)?;
2664    let mut records = Vec::new();
2665    let mut diagnostics = Vec::new();
2666    let mut validation_taint = ValidationTaint::default();
2667    let mut last_observed_seq = 0_u64;
2668    for (index, line) in bytes
2669        .split(|byte| *byte == b'\n')
2670        .filter(|line| !line.is_empty())
2671        .enumerate()
2672    {
2673        let line_number = index as u64 + 1;
2674        let mut value = match serde_json::from_slice::<serde_json::Value>(line) {
2675            Ok(value) => value,
2676            Err(error) => {
2677                if index == 0 {
2678                    return corrupt_value(
2679                        path,
2680                        format!("SessionCreated line is unreadable: {error}"),
2681                    );
2682                }
2683                diagnostics.push(EventLoadDiagnostic {
2684                    seq: line_number,
2685                    reason: format!("corrupt JSON line: {error}"),
2686                    engine_version: None,
2687                    skipped: true,
2688                });
2689                validation_taint.mark_broad(line_number, line_number);
2690                continue;
2691            }
2692        };
2693        let Some(object) = value.as_object_mut() else {
2694            if index == 0 {
2695                return corrupt_value(path, "SessionCreated line is not a JSON object");
2696            }
2697            diagnostics.push(EventLoadDiagnostic {
2698                seq: line_number,
2699                reason: "event envelope is not a JSON object".into(),
2700                engine_version: None,
2701                skipped: true,
2702            });
2703            validation_taint.mark_broad(line_number, line_number);
2704            continue;
2705        };
2706        let seq = object
2707            .get("seq")
2708            .and_then(Value::as_u64)
2709            .unwrap_or(line_number);
2710        let engine_version = object
2711            .get("engine_version")
2712            .and_then(Value::as_str)
2713            .map(str::to_owned);
2714        let envelope_run_id = object
2715            .get("run_id")
2716            .and_then(|value| serde_json::from_value(value.clone()).ok());
2717        if object.get("seq").and_then(Value::as_u64).is_some() {
2718            if seq <= last_observed_seq {
2719                return corrupt_value(path, "event sequences are not strictly increasing");
2720            }
2721            if last_observed_seq > 0 && seq > last_observed_seq + 1 {
2722                let start = last_observed_seq + 1;
2723                let end = seq - 1;
2724                diagnostics.push(EventLoadDiagnostic {
2725                    seq: start,
2726                    reason: if start == end {
2727                        "event sequence is absent from the physical log".into()
2728                    } else {
2729                        format!("event sequences {start}..={end} are absent from the physical log")
2730                    },
2731                    engine_version: None,
2732                    skipped: true,
2733                });
2734                validation_taint.mark_broad(start, end);
2735            }
2736            last_observed_seq = seq;
2737        }
2738        let unknown = object
2739            .keys()
2740            .filter(|key| {
2741                !matches!(
2742                    key.as_str(),
2743                    "engine_version"
2744                        | "origin"
2745                        | "event_schema_version"
2746                        | "session_id"
2747                        | "run_id"
2748                        | "seq"
2749                        | "timestamp"
2750                        | "payload"
2751                )
2752            })
2753            .cloned()
2754            .collect::<Vec<_>>();
2755        if !unknown.is_empty() {
2756            if index == 0 {
2757                return corrupt_value(
2758                    path,
2759                    format!(
2760                        "SessionCreated envelope has unknown fields: {}",
2761                        unknown.join(", ")
2762                    ),
2763                );
2764            }
2765            diagnostics.push(EventLoadDiagnostic {
2766                seq,
2767                reason: format!("unknown envelope fields: {}", unknown.join(", ")),
2768                engine_version,
2769                skipped: true,
2770            });
2771            if let Some(payload) = object.get("payload") {
2772                validation_taint
2773                    .mark_record(seq, envelope_run_id, payload)
2774                    .map_err(|message| EventLogError::Corrupt {
2775                        path: path.to_owned(),
2776                        message: message.into(),
2777                    })?;
2778            } else {
2779                validation_taint.mark_broad(seq, seq);
2780            }
2781            continue;
2782        }
2783        let mut degraded = Vec::new();
2784        if object
2785            .get("engine_version")
2786            .is_some_and(|version| !version.is_null() && !version.is_string())
2787        {
2788            object.remove("engine_version");
2789            degraded.push("engine_version".to_owned());
2790        }
2791        let Some(payload_value) = object.get("payload").cloned() else {
2792            if index == 0 {
2793                return corrupt_value(path, "SessionCreated payload is absent");
2794            }
2795            diagnostics.push(EventLoadDiagnostic {
2796                seq,
2797                reason: "required payload is absent".into(),
2798                engine_version,
2799                skipped: true,
2800            });
2801            validation_taint.mark_broad(seq, seq);
2802            continue;
2803        };
2804        match deserialize_event_payload_best_effort(payload_value.clone()) {
2805            Ok(read) => {
2806                degraded.extend(
2807                    read.degraded_fields
2808                        .into_iter()
2809                        .map(|field| format!("payload.{field}")),
2810                );
2811                object.insert(
2812                    "payload".into(),
2813                    serde_json::to_value(read.payload).expect("event payload serializes"),
2814                );
2815            }
2816            Err(reason) => {
2817                if index == 0 {
2818                    return corrupt_value(
2819                        path,
2820                        format!("SessionCreated is unsupported or corrupt: {reason}"),
2821                    );
2822                }
2823                diagnostics.push(EventLoadDiagnostic {
2824                    seq,
2825                    reason,
2826                    engine_version,
2827                    skipped: true,
2828                });
2829                validation_taint
2830                    .mark_record(seq, envelope_run_id, &payload_value)
2831                    .map_err(|message| EventLogError::Corrupt {
2832                        path: path.to_owned(),
2833                        message: message.into(),
2834                    })?;
2835                continue;
2836            }
2837        }
2838        let final_payload = object.get("payload").cloned();
2839        match serde_json::from_value::<StoredEvent>(value) {
2840            Ok(event) => {
2841                if !degraded.is_empty() {
2842                    diagnostics.push(EventLoadDiagnostic {
2843                        seq,
2844                        reason: format!("degraded optional fields: {}", degraded.join(", ")),
2845                        engine_version: event.engine_version.clone(),
2846                        skipped: false,
2847                    });
2848                }
2849                records.push(event);
2850            }
2851            Err(error) => {
2852                if index == 0 {
2853                    return corrupt_value(
2854                        path,
2855                        format!("SessionCreated is unsupported or corrupt: {error}"),
2856                    );
2857                }
2858                diagnostics.push(EventLoadDiagnostic {
2859                    seq,
2860                    reason: error.to_string(),
2861                    engine_version,
2862                    skipped: true,
2863                });
2864                if let Some(payload) = final_payload.as_ref() {
2865                    validation_taint
2866                        .mark_record(seq, envelope_run_id, payload)
2867                        .map_err(|message| EventLogError::Corrupt {
2868                            path: path.to_owned(),
2869                            message: message.into(),
2870                        })?;
2871                } else {
2872                    validation_taint.mark_broad(seq, seq);
2873                }
2874            }
2875        }
2876    }
2877    let observed_tip = last_observed_seq.max(
2878        diagnostics
2879            .iter()
2880            .filter(|diagnostic| diagnostic.skipped)
2881            .map(|diagnostic| diagnostic.seq)
2882            .max()
2883            .unwrap_or(0),
2884    );
2885    Ok(LoadedEvents {
2886        records,
2887        diagnostics,
2888        validation_taint,
2889        next_seq: observed_tip.saturating_add(1).max(1),
2890    })
2891}
2892
2893fn require_started_run(
2894    path: &Path,
2895    runs: &HashMap<RunId, RunAttribution>,
2896    run_id: Option<RunId>,
2897) -> Result<RunId, EventLogError> {
2898    let Some(run_id) = run_id else {
2899        return corrupt_value(path, "run-owned event is missing run_id");
2900    };
2901    if !runs.contains_key(&run_id) {
2902        return corrupt_value(path, "event references a run before RunStarted");
2903    }
2904    Ok(run_id)
2905}
2906
2907fn validate_attempt_owner(
2908    path: &Path,
2909    attempts: &HashMap<AttemptId, AttemptAttribution>,
2910    attempt_id: AttemptId,
2911    run_id: Option<RunId>,
2912) -> Result<RunId, EventLogError> {
2913    let Some(attempt) = attempts.get(&attempt_id) else {
2914        return corrupt_value(path, "attempt event appeared before ModelAttemptStarted");
2915    };
2916    if run_id != Some(attempt.run_id) {
2917        return corrupt_value(path, "attempt event uses a non-owning run_id");
2918    }
2919    if attempt.finished {
2920        return corrupt_value(
2921            path,
2922            "attempt lifecycle event appeared after its terminal event",
2923        );
2924    }
2925    Ok(attempt.run_id)
2926}
2927
2928fn validate_attempt_model(
2929    path: &Path,
2930    attempts: &HashMap<AttemptId, AttemptAttribution>,
2931    attempt_id: AttemptId,
2932    run_id: Option<RunId>,
2933    resolved_model: &cookie_agent_protocol::ResolvedModelRef,
2934) -> Result<RunId, EventLogError> {
2935    let owner = validate_attempt_owner(path, attempts, attempt_id, run_id)?;
2936    if attempts
2937        .get(&attempt_id)
2938        .is_none_or(|attempt| &attempt.resolved_model != resolved_model)
2939    {
2940        return corrupt_value(path, "attempt resolved model changed within its lifecycle");
2941    }
2942    Ok(owner)
2943}
2944
2945fn finish_attempt(
2946    path: &Path,
2947    runs: &mut HashMap<RunId, RunAttribution>,
2948    attempts: &mut HashMap<AttemptId, AttemptAttribution>,
2949    run_id: RunId,
2950    attempt_id: AttemptId,
2951    committed: bool,
2952) -> Result<(), EventLogError> {
2953    let attempt = attempts
2954        .get_mut(&attempt_id)
2955        .expect("validated attempt is indexed");
2956    attempt.finished = true;
2957    let run = runs.get_mut(&run_id).expect("started run is indexed");
2958    if !run.ordering_tainted && run.active_attempt != Some(attempt_id) {
2959        return corrupt(
2960            path,
2961            "attempt terminal event is inconsistent with the active attempt",
2962        );
2963    }
2964    run.active_attempt = None;
2965    if committed {
2966        run.attempts_on_active = 0;
2967    }
2968    Ok(())
2969}
2970
2971fn validate_tool_owner(
2972    path: &Path,
2973    run_id: RunId,
2974    turns: &HashMap<u64, (RunId, cookie_agent_protocol::PersistedModelTurn)>,
2975    model_call_owners: &HashMap<(RunId, ModelCallId), AssistantToolCallRef>,
2976    provider_item_owners: &HashMap<(RunId, ProviderItemId), AssistantToolCallRef>,
2977    owner: &AssistantToolCallRef,
2978) -> Result<(), EventLogError> {
2979    let Some((turn_run, turn)) = turns.get(&owner.model_turn_seq) else {
2980        return corrupt(
2981            path,
2982            "tool owner references an unknown committed model turn",
2983        );
2984    };
2985    let Some(cookie_agent_protocol::PersistedAssistantPart::ToolCall {
2986        id,
2987        provider_item_id,
2988        ..
2989    }) = turn.content.get(owner.content_index as usize)
2990    else {
2991        return corrupt(
2992            path,
2993            "tool owner content index is not a committed tool call",
2994        );
2995    };
2996    if *turn_run != run_id
2997        || id != &owner.model_call_id
2998        || provider_item_id != &owner.provider_item_id
2999        || model_call_owners.get(&(run_id, id.clone())) != Some(owner)
3000        || provider_item_id.as_ref().is_some_and(|provider_id| {
3001            provider_item_owners.get(&(run_id, provider_id.clone())) != Some(owner)
3002        })
3003    {
3004        return corrupt(
3005            path,
3006            "tool owner does not match the committed model content",
3007        );
3008    }
3009    Ok(())
3010}
3011
3012fn validate_approval_owner(
3013    path: &Path,
3014    owners: &HashMap<cookie_agent_protocol::ApprovalId, cookie_agent_protocol::RunId>,
3015    approval_id: cookie_agent_protocol::ApprovalId,
3016    event_run_id: Option<cookie_agent_protocol::RunId>,
3017) -> Result<(), EventLogError> {
3018    let Some(owner) = owners.get(&approval_id) else {
3019        return corrupt(
3020            path,
3021            "approval lifecycle event appeared before ApprovalRequested",
3022        );
3023    };
3024    if event_run_id != Some(*owner) {
3025        return corrupt(path, "approval lifecycle event uses a non-owning run_id");
3026    }
3027    Ok(())
3028}
3029
3030fn corrupt<T>(path: &Path, message: impl Into<String>) -> Result<T, EventLogError> {
3031    Err(EventLogError::Corrupt {
3032        path: path.to_owned(),
3033        message: message.into(),
3034    })
3035}
3036
3037fn corrupt_value<T>(path: &Path, message: impl Into<String>) -> Result<T, EventLogError> {
3038    Err(EventLogError::Corrupt {
3039        path: path.to_owned(),
3040        message: message.into(),
3041    })
3042}
3043
3044/// Reads a JSONL file after removing a crash-torn final record.  A malformed
3045/// complete record is corruption, not a torn tail, and is rejected.
3046pub fn load_jsonl<T>(path: &Path) -> Result<Vec<T>, EventLogError>
3047where
3048    T: for<'de> Deserialize<'de>,
3049{
3050    load_jsonl_with_policy(path, TornTail::Truncate)
3051}
3052
3053pub fn load_jsonl_shared<T>(path: &Path) -> Result<Vec<T>, EventLogError>
3054where
3055    T: for<'de> Deserialize<'de>,
3056{
3057    load_jsonl_with_policy(path, TornTail::Ignore)
3058}
3059
3060fn load_jsonl_with_policy<T>(path: &Path, torn_tail: TornTail) -> Result<Vec<T>, EventLogError>
3061where
3062    T: for<'de> Deserialize<'de>,
3063{
3064    let bytes = read_complete_jsonl(path, torn_tail)?;
3065    bytes
3066        .split(|byte| *byte == b'\n')
3067        .filter(|line| !line.is_empty())
3068        .map(|line| {
3069            serde_json::from_slice(line).map_err(|source| EventLogError::Json {
3070                path: path.to_owned(),
3071                source,
3072            })
3073        })
3074        .collect()
3075}
3076
3077fn read_complete_jsonl(path: &Path, torn_tail: TornTail) -> Result<Vec<u8>, EventLogError> {
3078    let mut bytes = match fs::read(path) {
3079        Ok(bytes) => bytes,
3080        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
3081        Err(source) => {
3082            return Err(EventLogError::Io {
3083                path: path.to_owned(),
3084                source,
3085            });
3086        }
3087    };
3088    if !bytes.is_empty() && !bytes.ends_with(b"\n") {
3089        let length = bytes
3090            .iter()
3091            .rposition(|byte| *byte == b'\n')
3092            .map_or(0, |at| at + 1);
3093        bytes.truncate(length);
3094        if matches!(torn_tail, TornTail::Truncate) {
3095            let file = OpenOptions::new()
3096                .write(true)
3097                .open(path)
3098                .map_err(|source| EventLogError::Io {
3099                    path: path.to_owned(),
3100                    source,
3101                })?;
3102            file.set_len(length as u64)
3103                .and_then(|()| file.sync_all())
3104                .map_err(|source| EventLogError::Io {
3105                    path: path.to_owned(),
3106                    source,
3107                })?;
3108        }
3109    }
3110    Ok(bytes)
3111}
3112
3113pub fn append_jsonl<T: Serialize>(path: &Path, value: &T) -> Result<(), EventLogError> {
3114    let bytes = serde_json::to_vec(value).map_err(|source| EventLogError::Json {
3115        path: path.to_owned(),
3116        source,
3117    })?;
3118    let created = !path.exists();
3119    let mut options = OpenOptions::new();
3120    options.create(true).append(true);
3121    #[cfg(unix)]
3122    {
3123        use std::os::unix::fs::OpenOptionsExt as _;
3124        options.mode(0o600);
3125    }
3126    let mut file = options.open(path).map_err(|source| EventLogError::Io {
3127        path: path.to_owned(),
3128        source,
3129    })?;
3130    file.write_all(&bytes)
3131        .and_then(|()| file.write_all(b"\n"))
3132        .and_then(|()| file.sync_data())
3133        .map_err(|source| EventLogError::Io {
3134            path: path.to_owned(),
3135            source,
3136        })?;
3137    if created && let Some(parent) = path.parent() {
3138        fsync_directory(parent)?;
3139    }
3140    Ok(())
3141}
3142
3143pub(crate) fn append_copied_event_jsonl(
3144    path: &Path,
3145    event: &StoredEvent,
3146) -> Result<(), EventLogError> {
3147    append_jsonl(path, event)
3148}
3149
3150#[cfg(unix)]
3151pub fn fsync_directory(path: &Path) -> Result<(), EventLogError> {
3152    File::open(path)
3153        .and_then(|directory| directory.sync_all())
3154        .map_err(|source| EventLogError::Io {
3155            path: path.to_owned(),
3156            source,
3157        })
3158}
3159
3160#[cfg(windows)]
3161pub fn fsync_directory(path: &Path) -> Result<(), EventLogError> {
3162    use std::os::windows::fs::OpenOptionsExt as _;
3163
3164    const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
3165    std::fs::OpenOptions::new()
3166        .read(true)
3167        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
3168        .open(path)
3169        .and_then(|directory| match directory.sync_all() {
3170            Ok(()) => Ok(()),
3171            Err(error) if matches!(error.raw_os_error(), Some(1 | 5 | 6 | 50)) => Ok(()),
3172            Err(error) => Err(error),
3173        })
3174        .map_err(|source| EventLogError::Io {
3175            path: path.to_owned(),
3176            source,
3177        })
3178}
3179
3180#[derive(Clone, Debug, PartialEq, Eq)]
3181pub enum OutputMessage {
3182    Delta(OutputDelta),
3183    Gap(OutputGap),
3184}
3185
3186#[derive(Clone, Debug)]
3187struct Chunk {
3188    offset: u64,
3189    data: Vec<u8>,
3190}
3191
3192#[derive(Debug, Default)]
3193struct StreamBuffer {
3194    start: u64,
3195    end: u64,
3196    chunks: VecDeque<Chunk>,
3197}
3198
3199#[derive(Debug)]
3200struct Subscriber {
3201    sender: mpsc::Sender<OutputMessage>,
3202    gap: Option<u64>,
3203}
3204
3205#[derive(Debug)]
3206struct HubState {
3207    streams: std::collections::HashMap<String, StreamBuffer>,
3208    subscribers: std::collections::HashMap<String, Vec<Subscriber>>,
3209    declaration: Vec<OutputStream>,
3210    finalized: bool,
3211}
3212
3213/// Bounded retained output with atomic snapshot-to-live subscription handoff.
3214#[derive(Clone, Debug)]
3215pub struct OutputHub {
3216    call_id: ToolCallId,
3217    limit: usize,
3218    state: Arc<Mutex<HubState>>,
3219}
3220
3221impl OutputHub {
3222    #[must_use]
3223    pub fn new(call_id: ToolCallId, retention_bytes: usize) -> Self {
3224        Self {
3225            call_id,
3226            limit: retention_bytes,
3227            state: Arc::new(Mutex::new(HubState {
3228                streams: std::collections::HashMap::new(),
3229                subscribers: std::collections::HashMap::new(),
3230                declaration: vec![OutputStream::Stdout, OutputStream::Stderr],
3231                finalized: false,
3232            })),
3233        }
3234    }
3235
3236    pub fn declare(&self, declaration: &cookie_agent_protocol::ToolOutputDeclaration) {
3237        let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
3238        state.declaration = declaration
3239            .channels()
3240            .iter()
3241            .map(|name| OutputStream::from_channel(name.as_deref()))
3242            .collect();
3243    }
3244
3245    pub fn streams(&self) -> Vec<OutputStream> {
3246        self.state
3247            .lock()
3248            .unwrap_or_else(|p| p.into_inner())
3249            .declaration
3250            .clone()
3251    }
3252
3253    pub fn emit(&self, stream: OutputStream, data: &[u8]) {
3254        if data.is_empty() {
3255            return;
3256        }
3257        let mut state = self
3258            .state
3259            .lock()
3260            .unwrap_or_else(|poisoned| poisoned.into_inner());
3261        if state.finalized {
3262            return;
3263        }
3264        let index = stream.name().to_owned();
3265        let (delta, end) = {
3266            let buffer = state.streams.entry(index.clone()).or_default();
3267            let delta = OutputDelta {
3268                call_id: self.call_id,
3269                stream: stream.clone(),
3270                byte_offset: buffer.end,
3271                data: STANDARD.encode(data),
3272            };
3273            buffer.chunks.push_back(Chunk {
3274                offset: buffer.end,
3275                data: data.to_vec(),
3276            });
3277            buffer.end += data.len() as u64;
3278            while buffer
3279                .chunks
3280                .iter()
3281                .map(|chunk| chunk.data.len())
3282                .sum::<usize>()
3283                > self.limit
3284            {
3285                if let Some(chunk) = buffer.chunks.pop_front() {
3286                    buffer.start = chunk.offset + chunk.data.len() as u64;
3287                }
3288            }
3289            (delta, buffer.end)
3290        };
3291        let subscribers = state.subscribers.entry(index).or_default();
3292        subscribers.retain_mut(|subscriber| {
3293            if let Some(next_offset) = subscriber.gap {
3294                match subscriber.sender.try_send(OutputMessage::Gap(OutputGap {
3295                    call_id: self.call_id,
3296                    stream: stream.clone(),
3297                    next_offset,
3298                })) {
3299                    Ok(()) => subscriber.gap = None,
3300                    Err(mpsc::error::TrySendError::Full(_)) => return true,
3301                    Err(mpsc::error::TrySendError::Closed(_)) => return false,
3302                }
3303            }
3304            match subscriber
3305                .sender
3306                .try_send(OutputMessage::Delta(delta.clone()))
3307            {
3308                Ok(()) => true,
3309                Err(mpsc::error::TrySendError::Full(_)) => {
3310                    subscriber.gap = Some(end);
3311                    true
3312                }
3313                Err(mpsc::error::TrySendError::Closed(_)) => false,
3314            }
3315        });
3316    }
3317
3318    /// Holds the same lock while taking the snapshot and registering the
3319    /// receiver, so subsequent deltas cannot fall into a handoff gap.
3320    #[must_use]
3321    pub fn subscribe(
3322        &self,
3323        stream: OutputStream,
3324        queue: usize,
3325    ) -> (OutputSnapshot, mpsc::Receiver<OutputMessage>) {
3326        let mut state = self
3327            .state
3328            .lock()
3329            .unwrap_or_else(|poisoned| poisoned.into_inner());
3330        let index = stream.name().to_owned();
3331        let buffer = state.streams.entry(index.clone()).or_default();
3332        let snapshot = OutputSnapshot {
3333            call_id: self.call_id,
3334            start_offset: buffer.start,
3335            end_offset: buffer.end,
3336            chunks: buffer
3337                .chunks
3338                .iter()
3339                .map(|chunk| OutputDelta {
3340                    call_id: self.call_id,
3341                    stream: stream.clone(),
3342                    byte_offset: chunk.offset,
3343                    data: STANDARD.encode(&chunk.data),
3344                })
3345                .collect(),
3346        };
3347        let (sender, receiver) = mpsc::channel(queue);
3348        // A nonzero retained start is an explicit loss boundary for a new
3349        // snapshot consumer. Queue the same marker used for lagging live
3350        // subscribers before any later deltas can be registered.
3351        let gap = (buffer.start > 0).then_some(buffer.start);
3352        let gap = match gap {
3353            Some(next_offset) => match sender.try_send(OutputMessage::Gap(OutputGap {
3354                call_id: self.call_id,
3355                stream: stream.clone(),
3356                next_offset,
3357            })) {
3358                Ok(()) => None,
3359                Err(mpsc::error::TrySendError::Full(_)) => Some(next_offset),
3360                Err(mpsc::error::TrySendError::Closed(_)) => None,
3361            },
3362            None => None,
3363        };
3364        // A retained finalized hub is a snapshot-only resource. Do not retain a
3365        // sender for a receiver which can never receive another delta.
3366        if !state.finalized {
3367            state
3368                .subscribers
3369                .entry(index)
3370                .or_default()
3371                .push(Subscriber { sender, gap });
3372        }
3373        (snapshot, receiver)
3374    }
3375
3376    /// Prevents late producer clones from publishing after the owning call has
3377    /// been committed as complete and closes every live subscription.
3378    pub fn finalize(&self) {
3379        let mut state = self
3380            .state
3381            .lock()
3382            .unwrap_or_else(|poisoned| poisoned.into_inner());
3383        state.finalized = true;
3384        state.subscribers.clear();
3385    }
3386}
3387
3388#[cfg(test)]
3389mod tests;