1use 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
3044pub 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#[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 #[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 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 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 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;