Skip to main content

tea_session/
reducer.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use tea_protocol::{
4    ApprovalDecision, ApprovalId, BranchId, CanonicalMessage, MessageId, RecordEnvelope, RecordId,
5    SessionRecord, SessionSequence, ToolCallId,
6};
7
8use crate::error::SessionReplayError;
9use crate::state::{
10    MaterializedSessionState, RunRecoveryState, ToolExecutionState, TurnCheckpoint,
11    commit_tool_result, declared_tool_calls, finish_tool, interrupt_tool, message_id,
12    new_pending_approval, new_state, new_tool_call, resolve_approval, set_approval, set_compaction,
13    set_model, set_policy, set_profile, set_reasoning_effort, start_tool,
14};
15
16#[derive(Debug, Clone, PartialEq)]
17struct DeclaredToolCall {
18    tool_name: String,
19    arguments: serde_json::Value,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23struct BranchProjection {
24    configuration: crate::SessionConfiguration,
25    messages: Vec<CanonicalMessage>,
26    pending_approvals: BTreeMap<ApprovalId, crate::PendingApproval>,
27    tool_calls: BTreeMap<ToolCallId, crate::ToolCallState>,
28    run_recovery: BTreeMap<tea_protocol::RunId, RunRecoveryState>,
29    latest_checkpoint: Option<TurnCheckpoint>,
30    latest_compaction: Option<crate::SessionCompaction>,
31}
32
33impl BranchProjection {
34    fn capture(state: &MaterializedSessionState) -> Self {
35        Self {
36            configuration: state.configuration.clone(),
37            messages: state.messages.clone(),
38            pending_approvals: state.pending_approvals.clone(),
39            tool_calls: state.tool_calls.clone(),
40            run_recovery: state.run_recovery.clone(),
41            latest_checkpoint: state.latest_checkpoint.clone(),
42            latest_compaction: state.latest_compaction.clone(),
43        }
44    }
45
46    fn install(&self, state: &mut MaterializedSessionState) {
47        state.configuration.clone_from(&self.configuration);
48        state.messages.clone_from(&self.messages);
49        state.pending_approvals.clone_from(&self.pending_approvals);
50        state.tool_calls.clone_from(&self.tool_calls);
51        state.run_recovery.clone_from(&self.run_recovery);
52        state.latest_checkpoint.clone_from(&self.latest_checkpoint);
53        state.latest_compaction.clone_from(&self.latest_compaction);
54    }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58struct BranchSnapshot {
59    projection: BranchProjection,
60    history: BTreeSet<RecordId>,
61    declared_tool_calls: BTreeSet<ToolCallId>,
62}
63
64impl BranchSnapshot {
65    fn safe_to_fork(&self) -> bool {
66        self.projection.pending_approvals.is_empty()
67            && self.declared_tool_calls.iter().all(|tool_call_id| {
68                self.projection
69                    .tool_calls
70                    .get(tool_call_id)
71                    .is_some_and(|tool| {
72                        matches!(tool.execution(), ToolExecutionState::Finished { .. })
73                            && tool.result_message_id().is_some()
74                    })
75            })
76    }
77}
78
79#[derive(Debug, Default)]
80struct HistoricalSnapshots {
81    required: BTreeSet<RecordId>,
82    captured: BTreeMap<RecordId, BranchSnapshot>,
83}
84
85impl HistoricalSnapshots {
86    fn for_prefix(records: &[RecordEnvelope], target: RecordId) -> Option<(Self, usize)> {
87        let target_index = records
88            .iter()
89            .position(|record| record.record_id() == target)?;
90        let mut required = BTreeSet::from([target]);
91        for record in &records[..=target_index] {
92            if let SessionRecord::BranchCreated { from_record_id, .. } = record.record() {
93                required.insert(*from_record_id);
94            }
95        }
96        Some((
97            Self {
98                required,
99                captured: BTreeMap::new(),
100            },
101            target_index,
102        ))
103    }
104
105    fn capture(&mut self, record_id: RecordId, snapshot: &BranchSnapshot) {
106        if self.required.contains(&record_id) {
107            self.captured.insert(record_id, snapshot.clone());
108        }
109    }
110
111    fn get(&self, record_id: RecordId) -> Option<&BranchSnapshot> {
112        self.captured.get(&record_id)
113    }
114}
115
116/// Pure deterministic reducer for canonical durable session records.
117#[derive(Debug, Clone, Default)]
118pub struct SessionReducer {
119    state: Option<MaterializedSessionState>,
120    records: Vec<RecordEnvelope>,
121    record_ids: BTreeSet<RecordId>,
122    message_ids: BTreeSet<MessageId>,
123    declared_tool_calls: BTreeMap<ToolCallId, DeclaredToolCall>,
124    requested_tool_call_ids: BTreeSet<ToolCallId>,
125    approval_ids: BTreeSet<ApprovalId>,
126    branch_heads: BTreeMap<BranchId, BranchSnapshot>,
127}
128
129impl SessionReducer {
130    /// Creates an empty reducer awaiting a sequence-zero creation record.
131    #[must_use]
132    pub const fn new() -> Self {
133        Self {
134            state: None,
135            records: Vec::new(),
136            record_ids: BTreeSet::new(),
137            message_ids: BTreeSet::new(),
138            declared_tool_calls: BTreeMap::new(),
139            requested_tool_call_ids: BTreeSet::new(),
140            approval_ids: BTreeSet::new(),
141            branch_heads: BTreeMap::new(),
142        }
143    }
144
145    /// Rebuilds materialized state from canonical record order.
146    ///
147    /// # Errors
148    ///
149    /// Returns a deterministic corruption/reference/transition error when the
150    /// complete record stream cannot represent one valid session.
151    pub fn replay(
152        records: impl IntoIterator<Item = RecordEnvelope>,
153    ) -> Result<MaterializedSessionState, SessionReplayError> {
154        let reducer = Self::replay_reducer(records)?;
155        reducer.state.ok_or(SessionReplayError::EmptyLog)
156    }
157
158    /// Rebuilds a reusable reducer from canonical record order.
159    ///
160    /// # Errors
161    ///
162    /// Returns the first deterministic replay failure.
163    pub fn replay_reducer(
164        records: impl IntoIterator<Item = RecordEnvelope>,
165    ) -> Result<Self, SessionReplayError> {
166        let mut reducer = Self::new();
167        for record in records {
168            reducer.apply(&record)?;
169        }
170        if reducer.state.is_none() {
171            return Err(SessionReplayError::EmptyLog);
172        }
173        Ok(reducer)
174    }
175
176    /// Applies one next canonical record atomically to this reducer.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error without changing reducer state when sequence,
181    /// identity, reference, or lifecycle invariants fail.
182    pub fn apply(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
183        if let Err(error) = self.apply_inner(envelope) {
184            self.restore_durable_state();
185            return Err(error);
186        }
187        self.records.push(envelope.clone());
188        Ok(())
189    }
190
191    /// Returns current materialized state, or `None` before creation.
192    #[must_use]
193    pub const fn state(&self) -> Option<&MaterializedSessionState> {
194        self.state.as_ref()
195    }
196
197    fn restore_durable_state(&mut self) {
198        let records = std::mem::take(&mut self.records);
199        let mut restored = Self::new();
200        for record in records {
201            restored
202                .apply_inner(&record)
203                .expect("previously accepted records remain replayable");
204            restored.records.push(record);
205        }
206        *self = restored;
207    }
208
209    fn apply_inner(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
210        self.apply_inner_with_history(envelope, None)
211    }
212
213    fn apply_inner_with_history(
214        &mut self,
215        envelope: &RecordEnvelope,
216        historical: Option<&mut HistoricalSnapshots>,
217    ) -> Result<(), SessionReplayError> {
218        if self.record_ids.contains(&envelope.record_id()) {
219            return Err(SessionReplayError::DuplicateRecord {
220                record_id: envelope.record_id(),
221            });
222        }
223        if self.state.is_none() {
224            return self.apply_creation(envelope, historical);
225        }
226
227        self.validate_envelope(envelope)?;
228        if matches!(envelope.record(), SessionRecord::SessionCreated { .. }) {
229            return Err(SessionReplayError::InvalidCreation);
230        }
231        self.validate_branch_scope(envelope)?;
232        match envelope.record() {
233            SessionRecord::BranchCreated {
234                source_branch_id,
235                branch_id,
236                from_record_id,
237            } => {
238                self.apply_branch_created(
239                    envelope,
240                    *source_branch_id,
241                    *branch_id,
242                    *from_record_id,
243                    historical,
244                )?;
245            }
246            SessionRecord::ActiveBranchChanged { branch_id } => {
247                self.apply_active_branch_changed(envelope, *branch_id, historical)?;
248            }
249            _ => {
250                self.apply_record(envelope)?;
251                self.capture_active_branch(envelope.record_id(), historical)?;
252            }
253        }
254
255        let state = self
256            .state
257            .as_mut()
258            .ok_or(SessionReplayError::InvalidTransition {
259                transition: "missing_materialized_state",
260            })?;
261        state.tail_sequence = envelope.sequence();
262        state.tail_record_id = envelope.record_id();
263        self.record_ids.insert(envelope.record_id());
264        Ok(())
265    }
266
267    fn apply_creation(
268        &mut self,
269        envelope: &RecordEnvelope,
270        historical: Option<&mut HistoricalSnapshots>,
271    ) -> Result<(), SessionReplayError> {
272        if envelope.sequence() != SessionSequence::new(0) {
273            return Err(SessionReplayError::SequenceMismatch {
274                expected: SessionSequence::new(0),
275                actual: envelope.sequence(),
276            });
277        }
278        let SessionRecord::SessionCreated {
279            profile_id,
280            metadata,
281        } = envelope.record()
282        else {
283            return Err(SessionReplayError::InvalidCreation);
284        };
285        self.state = Some(new_state(
286            envelope.record_id(),
287            envelope.session_id(),
288            envelope.sequence(),
289            profile_id.clone(),
290            metadata.clone(),
291            envelope.branch_id(),
292        ));
293        if let Some(branch_id) = envelope.branch_id() {
294            let state = self
295                .state
296                .as_ref()
297                .ok_or(SessionReplayError::InvalidCreation)?;
298            let snapshot = BranchSnapshot {
299                projection: BranchProjection::capture(state),
300                history: BTreeSet::from([envelope.record_id()]),
301                declared_tool_calls: BTreeSet::new(),
302            };
303            if let Some(historical) = historical {
304                historical.capture(envelope.record_id(), &snapshot);
305            }
306            self.branch_heads.insert(branch_id, snapshot);
307        }
308        self.record_ids.insert(envelope.record_id());
309        Ok(())
310    }
311
312    fn validate_envelope(&self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
313        let state = self
314            .state
315            .as_ref()
316            .ok_or(SessionReplayError::InvalidTransition {
317                transition: "missing_materialized_state",
318            })?;
319        if envelope.session_id() != state.session_id {
320            return Err(SessionReplayError::SessionMismatch {
321                expected: state.session_id,
322                actual: envelope.session_id(),
323            });
324        }
325        let expected = state
326            .tail_sequence
327            .checked_next()
328            .ok_or(SessionReplayError::SequenceOverflow)?;
329        if envelope.sequence() != expected {
330            return Err(SessionReplayError::SequenceMismatch {
331                expected,
332                actual: envelope.sequence(),
333            });
334        }
335        Ok(())
336    }
337
338    fn validate_branch_scope(&self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
339        let state = self
340            .state
341            .as_ref()
342            .ok_or(SessionReplayError::InvalidTransition {
343                transition: "missing_materialized_state",
344            })?;
345        match envelope.record() {
346            SessionRecord::BranchCreated { .. } | SessionRecord::ActiveBranchChanged { .. } => {
347                Ok(())
348            }
349            _ if envelope.branch_id() == state.active_branch_id => Ok(()),
350            _ => Err(SessionReplayError::InvalidReference {
351                reference: "inactive_branch",
352            }),
353        }
354    }
355
356    fn capture_active_branch(
357        &mut self,
358        record_id: RecordId,
359        historical: Option<&mut HistoricalSnapshots>,
360    ) -> Result<(), SessionReplayError> {
361        let state = self
362            .state
363            .as_mut()
364            .ok_or(SessionReplayError::InvalidTransition {
365                transition: "missing_materialized_state",
366            })?;
367        let Some(branch_id) = state.active_branch_id else {
368            return Ok(());
369        };
370        let head =
371            self.branch_heads
372                .get_mut(&branch_id)
373                .ok_or(SessionReplayError::InvalidReference {
374                    reference: "active_branch",
375                })?;
376        head.history.insert(record_id);
377        head.declared_tool_calls = state
378            .messages
379            .iter()
380            .flat_map(declared_tool_calls)
381            .map(|(tool_call_id, _, _)| tool_call_id)
382            .collect();
383        head.projection = BranchProjection::capture(state);
384        state
385            .branches
386            .get_mut(&branch_id)
387            .ok_or(SessionReplayError::InvalidReference {
388                reference: "active_branch_summary",
389            })?
390            .set_leaf(record_id);
391        if let Some(historical) = historical {
392            historical.capture(record_id, head);
393        }
394        Ok(())
395    }
396
397    fn apply_branch_created(
398        &mut self,
399        envelope: &RecordEnvelope,
400        source_branch_id: BranchId,
401        branch_id: BranchId,
402        from_record_id: RecordId,
403        historical: Option<&mut HistoricalSnapshots>,
404    ) -> Result<(), SessionReplayError> {
405        if self.branch_heads.contains_key(&branch_id) {
406            return Err(SessionReplayError::DuplicateEntity { entity: "branch" });
407        }
408        let source = self.branch_heads.get(&source_branch_id).ok_or(
409            SessionReplayError::InvalidReference {
410                reference: "source_branch",
411            },
412        )?;
413        if !source.history.contains(&from_record_id) {
414            return Err(SessionReplayError::InvalidReference {
415                reference: "fork_point_outside_source_branch",
416            });
417        }
418        let source_snapshot = match historical.as_deref() {
419            Some(historical) => historical.get(from_record_id).cloned(),
420            None => self.replay_branch_snapshot(source_branch_id, from_record_id)?,
421        }
422        .ok_or(SessionReplayError::InvalidReference {
423            reference: "fork_record_snapshot",
424        })?;
425        if !source_snapshot.safe_to_fork() {
426            return Err(SessionReplayError::InvalidTransition {
427                transition: "unsafe_fork_point",
428            });
429        }
430        let mut new_snapshot = source_snapshot;
431        new_snapshot.history.insert(envelope.record_id());
432        if let Some(historical) = historical {
433            historical.capture(envelope.record_id(), &new_snapshot);
434        }
435        self.branch_heads.insert(branch_id, new_snapshot);
436        self.state
437            .as_mut()
438            .ok_or(SessionReplayError::InvalidTransition {
439                transition: "missing_materialized_state",
440            })?
441            .branches
442            .insert(
443                branch_id,
444                crate::BranchSummary::new(
445                    branch_id,
446                    Some(source_branch_id),
447                    from_record_id,
448                    envelope.record_id(),
449                ),
450            );
451        Ok(())
452    }
453
454    fn apply_active_branch_changed(
455        &mut self,
456        envelope: &RecordEnvelope,
457        branch_id: BranchId,
458        historical: Option<&mut HistoricalSnapshots>,
459    ) -> Result<(), SessionReplayError> {
460        let head =
461            self.branch_heads
462                .get_mut(&branch_id)
463                .ok_or(SessionReplayError::InvalidReference {
464                    reference: "active_branch_change",
465                })?;
466        let state = self
467            .state
468            .as_mut()
469            .ok_or(SessionReplayError::InvalidTransition {
470                transition: "missing_materialized_state",
471            })?;
472        head.projection.install(state);
473        head.history.insert(envelope.record_id());
474        state.active_branch_id = Some(branch_id);
475        state
476            .branches
477            .get_mut(&branch_id)
478            .ok_or(SessionReplayError::InvalidReference {
479                reference: "active_branch_summary",
480            })?
481            .set_leaf(envelope.record_id());
482        if let Some(historical) = historical {
483            historical.capture(envelope.record_id(), head);
484        }
485        Ok(())
486    }
487
488    fn replay_branch_snapshot(
489        &self,
490        source_branch_id: BranchId,
491        from_record_id: RecordId,
492    ) -> Result<Option<BranchSnapshot>, SessionReplayError> {
493        let Some((mut historical, target_index)) =
494            HistoricalSnapshots::for_prefix(&self.records, from_record_id)
495        else {
496            return Ok(None);
497        };
498        let mut replay = Self::new();
499        for record in &self.records[..=target_index] {
500            replay.apply_inner_with_history(record, Some(&mut historical))?;
501            replay.records.push(record.clone());
502        }
503        Ok(historical
504            .get(from_record_id)
505            .filter(|snapshot| {
506                replay
507                    .branch_heads
508                    .get(&source_branch_id)
509                    .is_some_and(|source| source.history.contains(&from_record_id))
510                    && snapshot.history.contains(&from_record_id)
511            })
512            .cloned())
513    }
514
515    #[allow(clippy::too_many_lines)]
516    fn apply_record(&mut self, envelope: &RecordEnvelope) -> Result<(), SessionReplayError> {
517        let state = self
518            .state
519            .as_mut()
520            .ok_or(SessionReplayError::InvalidTransition {
521                transition: "missing_materialized_state",
522            })?;
523        match envelope.record() {
524            SessionRecord::SessionCreated { .. } => Err(SessionReplayError::InvalidCreation),
525            SessionRecord::MessageCommitted { message } => {
526                let id = message_id(message);
527                if !self.message_ids.insert(id) {
528                    return Err(SessionReplayError::DuplicateEntity { entity: "message" });
529                }
530                for (tool_call_id, tool_name, arguments) in declared_tool_calls(message) {
531                    if self
532                        .declared_tool_calls
533                        .insert(
534                            tool_call_id,
535                            DeclaredToolCall {
536                                tool_name: tool_name.to_owned(),
537                                arguments: arguments.clone(),
538                            },
539                        )
540                        .is_some()
541                        || state.tool_calls.contains_key(&tool_call_id)
542                    {
543                        return Err(SessionReplayError::DuplicateEntity {
544                            entity: "tool_call",
545                        });
546                    }
547                }
548                if let CanonicalMessage::ToolResult {
549                    tool_call_id,
550                    tool_name,
551                    content,
552                    is_error,
553                    error,
554                    ..
555                } = message
556                {
557                    let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
558                        SessionReplayError::InvalidReference {
559                            reference: "tool_result_tool_call",
560                        },
561                    )?;
562                    if tool.result_message_id().is_some() {
563                        return Err(SessionReplayError::InvalidTransition {
564                            transition: "duplicate_tool_result_message",
565                        });
566                    }
567                    match tool.execution() {
568                        ToolExecutionState::Finished {
569                            is_error: terminal_error,
570                            content: terminal_content,
571                            error: terminal_failure,
572                            ..
573                        } if terminal_error == is_error
574                            && terminal_content == content
575                            && terminal_failure == error
576                            && tool.tool_name() == tool_name => {}
577                        ToolExecutionState::NotStarted
578                        | ToolExecutionState::Started { .. }
579                        | ToolExecutionState::Interrupted { .. }
580                        | ToolExecutionState::Finished { .. } => {
581                            return Err(SessionReplayError::InvalidTransition {
582                                transition: "tool_result_before_matching_terminal",
583                            });
584                        }
585                    }
586                    commit_tool_result(tool, id);
587                }
588                state.messages.push(message.clone());
589                Ok(())
590            }
591            SessionRecord::ConfigurationChanged {
592                model,
593                profile_id,
594                reasoning_effort,
595            } => {
596                if let Some(model) = model {
597                    set_model(&mut state.configuration, model.clone());
598                }
599                if let Some(profile_id) = profile_id {
600                    set_profile(&mut state.configuration, profile_id.clone());
601                }
602                if let Some(reasoning_effort) = reasoning_effort {
603                    set_reasoning_effort(&mut state.configuration, *reasoning_effort);
604                }
605                Ok(())
606            }
607            SessionRecord::ToolCallRequested {
608                tool_call_id,
609                tool_name,
610                arguments,
611            } => {
612                let visible_on_branch = state.active_branch_id.is_none_or(|branch_id| {
613                    self.branch_heads
614                        .get(&branch_id)
615                        .is_some_and(|head| head.declared_tool_calls.contains(tool_call_id))
616                });
617                let declared = self
618                    .declared_tool_calls
619                    .get(tool_call_id)
620                    .filter(|_| visible_on_branch)
621                    .ok_or(SessionReplayError::InvalidReference {
622                        reference: "undeclared_tool_call",
623                    })?;
624                if declared.tool_name != *tool_name || declared.arguments != *arguments {
625                    return Err(SessionReplayError::InvalidReference {
626                        reference: "tool_call_declaration_mismatch",
627                    });
628                }
629                if !self.requested_tool_call_ids.insert(*tool_call_id)
630                    || state.tool_calls.contains_key(tool_call_id)
631                {
632                    return Err(SessionReplayError::DuplicateEntity {
633                        entity: "tool_call_request",
634                    });
635                }
636                state.tool_calls.insert(
637                    *tool_call_id,
638                    new_tool_call(*tool_call_id, tool_name.clone(), arguments.clone()),
639                );
640                Ok(())
641            }
642            SessionRecord::PolicyDecisionRecorded {
643                tool_call_id,
644                decision,
645            } => {
646                let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
647                    SessionReplayError::InvalidReference {
648                        reference: "policy_tool_call",
649                    },
650                )?;
651                if tool.policy_decision().is_some() {
652                    return Err(SessionReplayError::InvalidTransition {
653                        transition: "duplicate_policy_decision",
654                    });
655                }
656                set_policy(tool, *decision);
657                Ok(())
658            }
659            SessionRecord::ApprovalRequested {
660                approval_id,
661                tool_call_id,
662                expires_at,
663            } => {
664                if !self.approval_ids.insert(*approval_id) {
665                    return Err(SessionReplayError::DuplicateEntity { entity: "approval" });
666                }
667                let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
668                    SessionReplayError::InvalidReference {
669                        reference: "approval_tool_call",
670                    },
671                )?;
672                if tool.policy_decision() != Some(tea_protocol::PolicyDecision::RequireApproval)
673                    || tool.approval_id().is_some()
674                    || *expires_at <= envelope.timestamp()
675                {
676                    return Err(SessionReplayError::InvalidTransition {
677                        transition: "approval_request",
678                    });
679                }
680                set_approval(tool, *approval_id);
681                state.pending_approvals.insert(
682                    *approval_id,
683                    new_pending_approval(
684                        *approval_id,
685                        *tool_call_id,
686                        *expires_at,
687                        envelope.timestamp(),
688                    ),
689                );
690                Ok(())
691            }
692            SessionRecord::ApprovalResolved {
693                approval_id,
694                decision,
695            } => {
696                let pending = state.pending_approvals.remove(approval_id).ok_or(
697                    SessionReplayError::InvalidReference {
698                        reference: "pending_approval",
699                    },
700                )?;
701                if envelope.timestamp() < pending.requested_at()
702                    || envelope.timestamp() >= pending.expires_at()
703                {
704                    return Err(SessionReplayError::InvalidTransition {
705                        transition: "expired_approval_resolution",
706                    });
707                }
708                let tool = state.tool_calls.get_mut(&pending.tool_call_id()).ok_or(
709                    SessionReplayError::InvalidReference {
710                        reference: "approval_resolution_tool_call",
711                    },
712                )?;
713                resolve_approval(tool, *decision);
714                Ok(())
715            }
716            SessionRecord::ToolExecutionStarted {
717                tool_call_id,
718                execution_target,
719                idempotency,
720            } => {
721                let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
722                    SessionReplayError::InvalidReference {
723                        reference: "execution_tool_call",
724                    },
725                )?;
726                let authorized = match tool.policy_decision() {
727                    Some(tea_protocol::PolicyDecision::Allow) => true,
728                    Some(tea_protocol::PolicyDecision::RequireApproval) => matches!(
729                        tool.approval_decision(),
730                        Some(ApprovalDecision::AllowOnce | ApprovalDecision::AllowSession)
731                    ),
732                    Some(tea_protocol::PolicyDecision::Deny) | None => false,
733                };
734                if !authorized || !matches!(tool.execution(), ToolExecutionState::NotStarted) {
735                    return Err(SessionReplayError::InvalidTransition {
736                        transition: "tool_execution_start",
737                    });
738                }
739                start_tool(tool, *execution_target, *idempotency);
740                Ok(())
741            }
742            SessionRecord::ToolExecutionFinished {
743                tool_call_id,
744                is_error,
745                content,
746                error,
747                presentation,
748            } => {
749                let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
750                    SessionReplayError::InvalidReference {
751                        reference: "tool_terminal_tool_call",
752                    },
753                )?;
754                let denied_without_execution =
755                    matches!(tool.execution(), ToolExecutionState::NotStarted)
756                        && *is_error
757                        && (tool.policy_decision() == Some(tea_protocol::PolicyDecision::Deny)
758                            || tool.approval_decision() == Some(ApprovalDecision::Deny));
759                if !matches!(tool.execution(), ToolExecutionState::Started { .. })
760                    && !denied_without_execution
761                {
762                    return Err(SessionReplayError::InvalidTransition {
763                        transition: "tool_execution_finish",
764                    });
765                }
766                finish_tool(
767                    tool,
768                    *is_error,
769                    content.clone(),
770                    error.clone(),
771                    presentation.clone(),
772                );
773                Ok(())
774            }
775            SessionRecord::ToolExecutionInterrupted {
776                tool_call_id,
777                reason,
778            } => {
779                let tool = state.tool_calls.get_mut(tool_call_id).ok_or(
780                    SessionReplayError::InvalidReference {
781                        reference: "tool_interruption_tool_call",
782                    },
783                )?;
784                if !matches!(tool.execution(), ToolExecutionState::Started { .. }) {
785                    return Err(SessionReplayError::InvalidTransition {
786                        transition: "tool_execution_interruption",
787                    });
788                }
789                interrupt_tool(tool, reason.clone());
790                Ok(())
791            }
792            SessionRecord::RunInterrupted {
793                run_id,
794                turn_id,
795                reason,
796            } => {
797                if state.run_recovery.contains_key(run_id) {
798                    return Err(SessionReplayError::InvalidTransition {
799                        transition: "duplicate_run_terminal",
800                    });
801                }
802                state.run_recovery.insert(
803                    *run_id,
804                    RunRecoveryState::Interrupted {
805                        turn_id: *turn_id,
806                        reason: reason.clone(),
807                    },
808                );
809                Ok(())
810            }
811            SessionRecord::RunCancelled { run_id } => {
812                if state
813                    .run_recovery
814                    .insert(*run_id, RunRecoveryState::Cancelled)
815                    .is_some()
816                {
817                    return Err(SessionReplayError::InvalidTransition {
818                        transition: "duplicate_run_terminal",
819                    });
820                }
821                Ok(())
822            }
823            SessionRecord::BranchCreated { .. } | SessionRecord::ActiveBranchChanged { .. } => {
824                Err(SessionReplayError::InvalidTransition {
825                    transition: "branching_not_applied",
826                })
827            }
828            SessionRecord::SessionCompacted {
829                summary,
830                compacted_through_record_id,
831            } => {
832                if !matches!(summary, CanonicalMessage::Assistant { .. }) {
833                    return Err(SessionReplayError::InvalidTransition {
834                        transition: "compaction_summary_role",
835                    });
836                }
837                let source_is_active = state.active_branch_id.map_or_else(
838                    || self.record_ids.contains(compacted_through_record_id),
839                    |branch_id| {
840                        self.branch_heads
841                            .get(&branch_id)
842                            .is_some_and(|head| head.history.contains(compacted_through_record_id))
843                    },
844                );
845                if !source_is_active {
846                    return Err(SessionReplayError::InvalidReference {
847                        reference: "compaction_source",
848                    });
849                }
850                let id = message_id(summary);
851                if !self.message_ids.insert(id) {
852                    return Err(SessionReplayError::DuplicateEntity { entity: "message" });
853                }
854                set_compaction(state, summary.clone(), *compacted_through_record_id);
855                Ok(())
856            }
857            SessionRecord::TurnCheckpointed {
858                run_id,
859                turn_id,
860                next_action,
861            } => {
862                state.latest_checkpoint = Some(TurnCheckpoint::new(
863                    *run_id,
864                    *turn_id,
865                    envelope.record_id(),
866                    envelope.sequence(),
867                    *next_action,
868                ));
869                Ok(())
870            }
871        }
872    }
873}