Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

smith-core/src/session.rs

Raw
//! Branching in-memory session state for fixtures and effective agent context
//! restored through bounded durable reads (`SMH-SPEC-SPEC0001`, Sessions).
//!
//! Entry content is typed binary bytes; only arbitrary JSON payloads use
//! canonical CBOR. A record carries traversal metadata ([`EntryKind`]) and
//! views borrow validated resident spans.

use crate::content_codec::EntryContentView;
use crate::frame::Frame;
use serde::{Deserialize, Serialize};
use smith::error::{MutationState, Result, SmithError};
use smith::id::{BranchId, EntryId, MessageId, SessionId, ToolCallId};
use smith::message::{ContentBlock, Message, Role};
use smith::structured::{AccessError, StructuredData, ToolArguments};
use std::collections::HashMap;

/// Meta entry kind that marks a compaction boundary.
pub const COMPACTION_KIND: &str = "compaction";

/// Metadata kind for one redacted provider HTTP effect record.
pub const PROVIDER_HTTP_KIND: &str = "provider-http";

/// Metadata kind for a tool completion's typed mutation outcome.
pub const TOOL_MUTATION_KIND: &str = "tool-mutation";

fn structured_depth_error(error: AccessError) -> SmithError {
    match error {
        AccessError::DepthExceeded { .. } => SmithError::Session {
            code: smith::error::SessionCode::EntryDecode,
            message: format!(
                "JSON nesting exceeds {} containers; reduce nested arrays and objects to this depth or less",
                crate::json_scan::JSON_DEPTH_MAX
            ),
        },
        AccessError::Malformed => SmithError::Session {
            code: smith::error::SessionCode::EntryDecode,
            message: "malformed structured data".to_string(),
        },
    }
}

/// Durable session identity, persisted as the first frame of a session file
/// so reload reproduces the same IDs instead of minting new ones.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct SessionHeader {
    /// Session identifier.
    pub session_id: SessionId,
    /// Identifier of the root branch.
    pub branch_id: BranchId,
}

/// Byte range of one entry's content in its session's payload arena.
///
/// `u32` offsets cap one session arena at 4 GiB.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Span {
    /// First content byte.
    pub offset: u32,
    /// Content length in bytes.
    pub len: u32,
}

impl Span {
    /// The span of `len` bytes at `offset`, or an error past the arena cap.
    fn at(offset: usize, len: usize) -> Result<Self> {
        let full = || SmithError::Session {
            code: smith::error::SessionCode::SessionArenaFull,
            message: format!("entry content at {offset}+{len} exceeds the 4 GiB session arena"),
        };
        let end = u32::try_from(offset.saturating_add(len)).map_err(|_| full())?;
        let offset = u32::try_from(offset).map_err(|_| full())?;
        Ok(Self {
            offset,
            len: end - offset,
        })
    }

    const fn end(self) -> usize {
        self.offset as usize + self.len as usize
    }

    const fn range(self) -> std::ops::Range<usize> {
        self.offset as usize..self.end()
    }
}

/// What an entry recorded, reduced to the identifiers traversal pairs on.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum EntryKind {
    /// Chat message.
    Message {
        /// Message identifier.
        id: MessageId,
        /// Who produced the message.
        role: Role,
    },
    /// Tool call.
    ToolCall {
        /// Identifier pairing the call with its result.
        call_id: ToolCallId,
    },
    /// Tool result.
    ToolResult {
        /// Identifier of the call this result answers.
        call_id: ToolCallId,
        /// Whether the effect succeeded.
        ok: bool,
    },
    /// Session metadata.
    Meta {
        /// Whether the entry marks a compaction boundary.
        compaction: bool,
    },
}

/// A fixed-size session entry record; its content lives in the session
/// payload arena.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Entry {
    /// Entry ID.
    pub id: EntryId,
    /// Parent entry ID (None for root).
    pub parent: Option<EntryId>,
    /// Timestamp (Unix ms).
    pub timestamp_ms: i64,
    /// What the entry recorded.
    pub kind: EntryKind,
    /// Typed binary [`EntryContent`] bytes in the session arena.
    pub content: Span,
}

impl Entry {
    /// Whether this entry marks a compaction boundary.
    #[must_use]
    pub const fn is_compaction(&self) -> bool {
        matches!(self.kind, EntryKind::Meta { compaction: true })
    }
}

/// Decoded frame metadata with opaque typed binary content bytes.
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct EntryFrame {
    /// Entry ID.
    pub id: EntryId,
    /// Parent entry ID (None for root).
    pub parent: Option<EntryId>,
    /// Timestamp (Unix ms).
    pub timestamp_ms: i64,
    /// What the entry recorded.
    pub kind: EntryKind,
    /// Typed binary [`EntryContent`] bytes.
    pub content: Vec<u8>,
}

impl EntryFrame {
    /// A frame for `content` with a fresh identity, outside any session.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] when the content cannot be encoded.
    pub fn new(parent: Option<EntryId>, content: &EntryContent) -> Result<Self> {
        let mut bytes = Vec::new();
        content.encode_into(&mut bytes)?;
        Ok(Self {
            id: EntryId::new(),
            parent,
            timestamp_ms: now_ms(),
            kind: content.kind(),
            content: bytes,
        })
    }
}

/// What one entry recorded; the decoded view of an entry's content.
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")]
pub enum EntryContent {
    /// Chat message.
    Message(Message),
    /// Tool call (recorded for replay).
    ToolCall {
        /// Invoked tool.
        name: String,
        /// Canonical tool arguments.
        input: ToolArguments,
        /// Identifier pairing this call with its result.
        call_id: ToolCallId,
    },
    /// Tool result.
    ToolResult {
        /// Whether the effect succeeded.
        ok: bool,
        /// Recorded output or error text.
        output: String,
        /// Identifier of the call this result answers.
        call_id: ToolCallId,
    },
    /// Session metadata event (compaction, branch selection).
    Meta {
        /// Metadata kind, for example [`COMPACTION_KIND`].
        kind: String,
        /// Kind-specific payload.
        detail: StructuredData,
    },
}

impl EntryContent {
    /// The record kind of this content.
    #[must_use]
    pub fn kind(&self) -> EntryKind {
        EntryContentRef::from(self).kind()
    }

    /// Decode content through the validated borrowed binary parser.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] when `bytes` are not entry content.
    pub fn decode(bytes: &[u8]) -> Result<Self> {
        EntryContentView::decode(bytes)?.to_owned()
    }

    /// Append versioned typed binary content to `out`.
    fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
        EntryContentRef::from(self).encode_into(out)
    }
}

/// Recording-time borrowed Rust content, distinct from the validated
/// decoded-from-bytes [`EntryContentView`].
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(tag = "kind", content = "data")]
pub enum EntryContentRef<'a> {
    /// Chat message.
    Message(&'a Message),
    /// Tool call.
    ToolCall {
        /// Invoked tool.
        name: &'a str,
        /// Tool arguments.
        input: &'a ToolArguments,
        /// Identifier pairing this call with its result.
        call_id: ToolCallId,
    },
    /// Tool result.
    ToolResult {
        /// Whether the effect succeeded.
        ok: bool,
        /// Output or error text.
        output: &'a str,
        /// Identifier of the call this result answers.
        call_id: ToolCallId,
    },
    /// Session metadata event.
    Meta {
        /// Metadata kind.
        kind: &'a str,
        /// Kind-specific payload.
        detail: &'a StructuredData,
    },
}

impl<'a> From<&'a EntryContent> for EntryContentRef<'a> {
    fn from(content: &'a EntryContent) -> Self {
        match content {
            EntryContent::Message(message) => Self::Message(message),
            EntryContent::ToolCall {
                name,
                input,
                call_id,
            } => Self::ToolCall {
                name,
                input,
                call_id: *call_id,
            },
            EntryContent::ToolResult {
                ok,
                output,
                call_id,
            } => Self::ToolResult {
                ok: *ok,
                output,
                call_id: *call_id,
            },
            EntryContent::Meta { kind, detail } => Self::Meta { kind, detail },
        }
    }
}

impl EntryContentRef<'_> {
    /// The record kind of this content.
    #[must_use]
    pub fn kind(&self) -> EntryKind {
        match *self {
            Self::Message(message) => EntryKind::Message {
                id: message.id,
                role: message.role,
            },
            Self::ToolCall { call_id, .. } => EntryKind::ToolCall { call_id },
            Self::ToolResult { call_id, ok, .. } => EntryKind::ToolResult { call_id, ok },
            Self::Meta { kind, .. } => EntryKind::Meta {
                compaction: kind == COMPACTION_KIND,
            },
        }
    }

    /// Append typed binary content to `out`; structured payloads are
    /// re-ordered into canonical key order only when needed.
    fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
        if let Self::Meta { kind, detail } = *self {
            crate::metadata::validate(kind, detail.view()).map_err(structured_depth_error)?;
        }
        let canonical_arguments: ToolArguments;
        let canonical_data: StructuredData;
        let canonical_message: Message;
        let view: EntryContentRef<'_> = match *self {
            Self::ToolCall {
                name,
                input,
                call_id,
            } if !input
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .map_err(structured_depth_error)? =>
            {
                canonical_arguments = input
                    .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
                    .map_err(structured_depth_error)?;
                EntryContentRef::ToolCall {
                    name,
                    input: &canonical_arguments,
                    call_id,
                }
            }
            Self::Meta { kind, detail }
                if !detail
                    .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                    .map_err(structured_depth_error)? =>
            {
                canonical_data = detail
                    .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
                    .map_err(structured_depth_error)?;
                EntryContentRef::Meta {
                    kind,
                    detail: &canonical_data,
                }
            }
            Self::Message(message) if !message_is_canonical(message)? => {
                canonical_message = canonical_message_of(message)?;
                EntryContentRef::Message(&canonical_message)
            }
            other => other,
        };
        crate::content_codec::encode(&view, out)
    }
}

fn message_is_canonical(message: &Message) -> Result<bool> {
    for block in &message.blocks {
        if let ContentBlock::ToolUse { input, .. } = block
            && !input
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .map_err(structured_depth_error)?
        {
            return Ok(false);
        }
    }
    Ok(true)
}

fn canonical_message_of(message: &Message) -> Result<Message> {
    let mut blocks = Vec::with_capacity(message.blocks.len());
    for block in &message.blocks {
        blocks.push(match block {
            ContentBlock::ToolUse {
                name,
                input,
                call_id,
            } => ContentBlock::ToolUse {
                name: name.clone(),
                input: input
                    .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
                    .map_err(structured_depth_error)?,
                call_id: *call_id,
            },
            ContentBlock::Text(text) => ContentBlock::Text(text.clone()),
            ContentBlock::Thinking(thinking) => ContentBlock::Thinking(thinking.clone()),
            ContentBlock::ToolResult {
                call_id,
                ok,
                output,
            } => ContentBlock::ToolResult {
                call_id: *call_id,
                ok: *ok,
                output: output.clone(),
            },
        });
    }
    Ok(Message {
        id: message.id,
        role: message.role,
        blocks,
        timestamp_ms: message.timestamp_ms,
    })
}

/// Wall-clock milliseconds since the Unix epoch, saturating on overflow.
fn now_ms() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |elapsed| {
            i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX)
        })
}

/// One root-to-leaf chain of a session.
///
/// Only its [`Session`] mutates a branch, so every record's span points into
/// that session's arena.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Branch {
    id: BranchId,
    entries: Vec<Entry>,
    selected_index: usize,
}

impl Branch {
    const fn with_id(id: BranchId) -> Self {
        Self {
            id,
            entries: Vec::new(),
            selected_index: 0,
        }
    }

    /// Branch identity.
    #[must_use]
    pub const fn id(&self) -> BranchId {
        self.id
    }

    /// Entry records ordered root to leaf.
    #[must_use]
    pub fn entries(&self) -> &[Entry] {
        &self.entries
    }

    /// Append `entry` and select it.
    #[expect(
        clippy::large_types_passed_by_value,
        reason = "Entry ownership moves directly into the resident branch vector"
    )]
    fn append(&mut self, entry: Entry) -> &Entry {
        self.selected_index = self.entries.len();
        self.entries.push(entry);
        &self.entries[self.selected_index]
    }

    /// The currently selected entry, if the branch has any.
    #[must_use]
    pub fn selected_entry(&self) -> Option<&Entry> {
        self.entries.get(self.selected_index)
    }
}

/// A session is a persistent, branching message/tool history.
///
/// Fields stay private so the invariants hold: at least one branch, a valid
/// active index, every span inside the arena, and a compaction boundary
/// that names a recorded compaction entry.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Session {
    id: SessionId,
    /// All branches (default branch is index 0).
    branches: Vec<Branch>,
    /// Typed binary content of every entry, addressed by [`Span`].
    payloads: Vec<u8>,
    active_branch_index: usize,
    compaction_boundary: Option<EntryId>,
}

impl Default for Session {
    fn default() -> Self {
        Self::new()
    }
}

impl Session {
    /// A new session with one empty branch.
    #[must_use]
    pub fn new() -> Self {
        Self::new_with_id(SessionId::new())
    }

    pub(crate) fn new_with_id(id: SessionId) -> Self {
        Self {
            id,
            branches: vec![Branch::with_id(BranchId::new())],
            payloads: Vec::new(),
            active_branch_index: 0,
            compaction_boundary: None,
        }
    }

    /// Session identifier.
    #[must_use]
    pub const fn id(&self) -> SessionId {
        self.id
    }

    /// All branches; the active one is [`Session::active_branch`].
    #[must_use]
    pub fn branches(&self) -> &[Branch] {
        &self.branches
    }

    /// The latest compaction boundary entry, if any.
    #[must_use]
    pub const fn compaction_boundary(&self) -> Option<EntryId> {
        self.compaction_boundary
    }

    /// The selected branch.
    #[must_use]
    pub fn active_branch(&self) -> &Branch {
        &self.branches[self.active_branch_index]
    }

    /// Identifier of the selected entry, if any.
    #[must_use]
    pub fn selected_id(&self) -> Option<EntryId> {
        self.active_branch().selected_entry().map(|e| e.id)
    }

    /// Encode `content` once into the arena and append its record to the
    /// selected branch, selecting it. A compaction entry becomes the
    /// compaction boundary.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] when the content cannot be encoded or
    /// the arena is full; the session is unchanged then.
    pub fn append<'a>(&mut self, content: impl Into<EntryContentRef<'a>>) -> Result<&Entry> {
        self.append_with(content, |_, _| Ok(()))
    }

    /// [`Session::append`], running `persist` on the new record and its
    /// content bytes before the entry becomes part of the session.
    ///
    /// # Errors
    ///
    /// As [`Session::append`], or the error of `persist`; the session is
    /// unchanged then.
    pub fn append_with<'a>(
        &mut self,
        content: impl Into<EntryContentRef<'a>>,
        persist: impl FnOnce(&Entry, &[u8]) -> Result<()>,
    ) -> Result<&Entry> {
        let content = content.into();
        let parent = self.selected_id();
        let offset = self.payloads.len();
        let staged = content
            .encode_into(&mut self.payloads)
            .and_then(|()| Span::at(offset, self.payloads.len() - offset))
            .map(|span| Entry {
                id: EntryId::new(),
                parent,
                timestamp_ms: now_ms(),
                kind: content.kind(),
                content: span,
            })
            .and_then(|entry| persist(&entry, &self.payloads[offset..]).map(|()| entry));
        let entry = match staged {
            Ok(entry) => entry,
            Err(error) => {
                self.payloads.truncate(offset);
                return Err(error);
            }
        };
        if entry.is_compaction() {
            self.compaction_boundary = Some(entry.id);
        }
        Ok(self.branches[self.active_branch_index].append(entry))
    }

    /// The typed binary content bytes of `entry`; empty when `entry` does
    /// not belong to this session.
    #[must_use]
    pub fn bytes(&self, entry: &Entry) -> &[u8] {
        self.payloads.get(entry.content.range()).unwrap_or_default()
    }

    /// Decode the content of `entry` into the caller's scope, verifying it
    /// against the record's [`EntryKind`], which traversal trusts unchecked.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] with code `ENTRY_DECODE` when the
    /// bytes do not decode, or `ENTRY_KIND_MISMATCH` when they decode to
    /// content of another kind than the record claims.
    pub fn content(&self, entry: &Entry) -> Result<EntryContent> {
        let content = EntryContent::decode(self.bytes(entry))?;
        let found = content.kind();
        if found != entry.kind {
            return Err(SmithError::Session {
                code: smith::error::SessionCode::EntryKindMismatch,
                message: format!(
                    "entry {} records {:?} but holds {found:?}",
                    entry.id, entry.kind
                ),
            });
        }
        Ok(content)
    }

    /// Best-known mutation state from selected durable history.
    ///
    /// A legacy result without completion metadata reconstructs as `Unknown`.
    ///
    /// # Errors
    ///
    /// Returns `ENTRY_DECODE` for malformed completion metadata.
    pub fn mutation_state(&self, call_id: ToolCallId) -> Result<MutationState> {
        for entry in self.active_branch().entries().iter().rev() {
            let EntryContent::Meta { kind, detail } = self.content(entry)? else {
                continue;
            };
            if kind != TOOL_MUTATION_KIND {
                continue;
            }
            crate::metadata::validate(&kind, detail.view()).map_err(|_| SmithError::Session {
                code: smith::error::SessionCode::EntryDecode,
                message: "invalid tool mutation state metadata".to_string(),
            })?;
            let detail: crate::metadata::ToolMutationMetadata =
                detail.deserialize_as().map_err(|_| SmithError::Session {
                    code: smith::error::SessionCode::EntryDecode,
                    message: "invalid tool mutation state metadata".to_string(),
                })?;
            if detail.call_id == call_id {
                return Ok(detail.mutation_state);
            }
        }
        Ok(MutationState::Unknown)
    }

    /// The root-to-`at` entry chain, or `None` when `at` is unknown or its
    /// ancestry is broken.
    ///
    /// A branch stores its root-to-leaf chain in order, so the chain is the
    /// branch prefix ending at `at`; each parent link is verified against the
    /// preceding entry and no lookup table is built.
    fn ancestry(&self, at: EntryId) -> Option<&[Entry]> {
        let (branch, index) = self.branches.iter().find_map(|branch| {
            let index = branch.entries.iter().position(|entry| entry.id == at)?;
            Some((branch, index))
        })?;
        let chain = &branch.entries[..=index];
        let rooted = chain.first().is_some_and(|root| root.parent.is_none());
        let linked = chain
            .windows(2)
            .all(|pair| pair[1].parent == Some(pair[0].id));
        (rooted && linked).then_some(chain)
    }

    /// Fork the history ending at `at` into a new session.
    ///
    /// Entry IDs, order, and timestamps are preserved, `at` becomes the
    /// selected leaf, and the compaction boundary carries over when it lies
    /// on the retained path. The new session gets fresh session and branch
    /// identity, because it is a different durable history. The retained
    /// records are copied with the one arena prefix their spans cover, so
    /// spans stay valid unchanged.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] when `at` is not part of this session.
    pub fn fork_at(&self, at: EntryId) -> Result<Self> {
        let entries = self.ancestry(at).ok_or_else(|| SmithError::Session {
            code: smith::error::SessionCode::EntryNotFound,
            message: format!("entry {at} is not part of session {}", self.id),
        })?;
        let boundary = self
            .compaction_boundary
            .filter(|id| entries.iter().any(|entry| entry.id == *id));
        let end = entries
            .iter()
            .map(|entry| entry.content.end())
            .max()
            .unwrap_or(0);
        Ok(Self {
            id: SessionId::new(),
            branches: vec![Branch {
                id: BranchId::new(),
                entries: entries.to_vec(),
                selected_index: entries.len().saturating_sub(1),
            }],
            payloads: self.payloads.get(..end).unwrap_or_default().to_vec(),
            active_branch_index: 0,
            compaction_boundary: boundary,
        })
    }

    /// Keep only the selected compaction entry as live context.
    ///
    /// Full ancestry, sibling branches, and their payloads remain durable in
    /// the session file; live agent context resumes from this boundary.
    pub(crate) fn retain_compacted_context(&mut self) {
        let Some(entry) = self.active_branch().selected_entry().copied() else {
            return;
        };
        if !entry.is_compaction() || self.compaction_boundary != Some(entry.id) {
            return;
        }
        let branch_id = self.active_branch().id;
        let payloads = self.bytes(&entry).to_vec();
        self.branches = vec![Branch {
            id: branch_id,
            entries: vec![Entry {
                content: Span {
                    offset: 0,
                    len: entry.content.len,
                },
                ..entry
            }],
            selected_index: 0,
        }];
        self.payloads = payloads;
        self.active_branch_index = 0;
    }

    /// The durable identity of this session's active branch.
    #[must_use]
    pub fn header(&self) -> SessionHeader {
        SessionHeader {
            session_id: self.id,
            branch_id: self.active_branch().id,
        }
    }

    /// Restore a selected chain from owned entry records.
    /// A chain may start at a compaction boundary with an older parent ID;
    /// historical content stays on disk. Unrelated branches are not retained.
    ///
    /// # Errors
    /// Returns a session error for broken ancestry or an arena overflow.
    pub fn from_chain(header: SessionHeader, entries: Vec<EntryFrame>) -> Result<Self> {
        let mut session = Self {
            id: header.session_id,
            branches: vec![Branch::with_id(header.branch_id)],
            payloads: Vec::new(),
            active_branch_index: 0,
            compaction_boundary: None,
        };
        for entry in entries {
            session.push_restored(entry)?;
        }
        Ok(session)
    }

    /// Append one chronological restored entry without retaining a second
    /// full-history payload buffer.
    pub(crate) fn push_restored(&mut self, entry: EntryFrame) -> Result<()> {
        if entry.parent != self.selected_id()
            && !(self.selected_id().is_none()
                && entry.kind == (EntryKind::Meta { compaction: true }))
        {
            return Err(SmithError::Session {
                code: smith::error::SessionCode::SessionParent,
                message: format!("entry {} has broken ancestry", entry.id),
            });
        }
        let span = Span::at(self.payloads.len(), entry.content.len())?;
        self.payloads.extend(entry.content);
        let record = Entry {
            id: entry.id,
            parent: entry.parent,
            timestamp_ms: entry.timestamp_ms,
            kind: entry.kind,
            content: span,
        };
        if record.is_compaction() {
            self.compaction_boundary = Some(record.id);
        }
        self.branches[0].append(record);
        Ok(())
    }

    /// Build in-memory fixture state from already-decoded frames.
    ///
    /// Durable file reopening uses [`crate::store::SessionReader`] instead.
    /// A header frame restores the session and branch identity, so state
    /// rebuilt from the same file is identical on every load. Stored parent
    /// links define the branches: each entry with no children is a leaf, and
    /// its root-to-leaf chain is one branch. The last selection record wins;
    /// otherwise the last stored entry is selected. Unknown frames are skipped,
    /// and compaction entries restore the compaction boundary.
    ///
    /// Content bytes are copied into the arena, never decoded; every
    /// collection is sized up front, so a linear history restores with a
    /// constant number of allocations.
    ///
    /// # Errors
    ///
    /// Returns [`SmithError::Session`] when the content exceeds the arena cap,
    /// a header is duplicated, or a header is not the first frame.
    pub fn from_frames(frames: &[Frame]) -> Result<Self> {
        let mut header = None;
        let (mut count, mut total) = (0, 0);
        let mut selection = None;
        for (index, frame) in frames.iter().enumerate() {
            match frame {
                Frame::Header { header: found } if header.is_some() => {
                    return Err(SmithError::Session {
                        code: smith::error::SessionCode::SessionHeaderDuplicate,
                        message: "session file contains more than one header frame".to_string(),
                    });
                }
                Frame::Header { .. } if index != 0 => {
                    return Err(SmithError::Session {
                        code: smith::error::SessionCode::SessionHeaderNotFirst,
                        message: "session header must be the first frame".to_string(),
                    });
                }
                Frame::Header { header: found } => header = Some(*found),
                Frame::Known { entry } => {
                    count += 1;
                    total += entry.content.len();
                    selection = None;
                }
                Frame::Selection { id } => selection = Some(*id),
                Frame::Unknown { .. } | Frame::Index { .. } | Frame::Checkpoint { .. } => {}
            }
        }
        Span::at(0, total)?;
        let mut payloads = Vec::with_capacity(total);
        let mut stored = Vec::with_capacity(count);
        let mut boundary = None;
        for frame in frames {
            let Frame::Known { entry } = frame else {
                continue;
            };
            let record = Entry {
                id: entry.id,
                parent: entry.parent,
                timestamp_ms: entry.timestamp_ms,
                kind: entry.kind,
                content: Span::at(payloads.len(), entry.content.len())?,
            };
            payloads.extend_from_slice(&entry.content);
            if record.is_compaction() {
                boundary = Some(record.id);
            }
            stored.push(record);
        }
        let header = header.unwrap_or_else(|| SessionHeader {
            session_id: SessionId::new(),
            branch_id: BranchId::new(),
        });
        if let Some(id) = selection
            && !stored.iter().any(|entry| entry.id == id)
        {
            return Err(SmithError::Session {
                code: smith::error::SessionCode::EntryNotFound,
                message: format!("selected entry {id} is missing"),
            });
        }
        let (branches, active_branch_index) =
            restore_branches(&stored, header.branch_id, selection);
        Ok(Self {
            id: header.session_id,
            branches,
            payloads,
            active_branch_index,
            compaction_boundary: boundary,
        })
    }
}

/// Rebuild branches from stored records and their parent links, returning
/// them with the index of the active branch.
#[expect(
    clippy::large_types_passed_by_value,
    reason = "Copy selection ID is compared once while restoring branch metadata"
)]
fn restore_branches(
    stored: &[Entry],
    active_id: BranchId,
    selection: Option<EntryId>,
) -> (Vec<Branch>, usize) {
    let Some(last) = stored.last() else {
        return (vec![Branch::with_id(active_id)], 0);
    };
    // Entry id to its index and whether any stored entry names it as parent.
    let mut nodes: HashMap<EntryId, (usize, bool)> = HashMap::with_capacity(stored.len());
    for (index, entry) in stored.iter().enumerate() {
        nodes.insert(entry.id, (index, false));
    }
    for parent in stored.iter().filter_map(|entry| entry.parent) {
        if let Some(node) = nodes.get_mut(&parent) {
            node.1 = true;
        }
    }
    let nodes = &nodes;
    let is_leaf = |entry: &&Entry| nodes.get(&entry.id).is_some_and(|node| !node.1);
    // Root-ward walk; a missing parent ends the recoverable chain, and the
    // step cap ends a corrupt cycle.
    let chain = |leaf: &Entry| {
        let mut cursor = Some(leaf.id);
        std::iter::from_fn(move || {
            let (index, _) = nodes.get(&cursor?)?;
            let entry = stored.get(*index)?;
            cursor = entry.parent;
            Some(*entry)
        })
        .take(stored.len())
    };
    let mut branches = Vec::with_capacity(stored.iter().filter(is_leaf).count());
    let selected = selection.unwrap_or(last.id);
    let active_leaf = selection.map_or(Some(last.id), |selected| {
        stored
            .iter()
            .filter(is_leaf)
            .find(|leaf| leaf.id == selected)
            .or_else(|| {
                stored.iter().filter(is_leaf).find(|leaf| {
                    leaf.id == last.id && chain(leaf).any(|entry| entry.id == selected)
                })
            })
            .or_else(|| {
                stored
                    .iter()
                    .filter(is_leaf)
                    .find(|leaf| chain(leaf).any(|entry| entry.id == selected))
            })
            .map(|leaf| leaf.id)
    });
    let mut active_branch_index = 0;
    for leaf in stored.iter().filter(is_leaf) {
        let mut entries = Vec::with_capacity(chain(leaf).count());
        entries.extend(chain(leaf));
        entries.reverse();
        let is_active = active_leaf == Some(leaf.id);
        if is_active {
            active_branch_index = branches.len();
        }
        let selected_index = if is_active {
            entries
                .iter()
                .position(|entry| entry.id == selected)
                .unwrap_or_else(|| entries.len().saturating_sub(1))
        } else {
            entries.len().saturating_sub(1)
        };
        branches.push(Branch {
            // The selected branch owns the persisted identity; sibling
            // branches derive theirs from their leaf, which keeps every
            // reload of the same file identical.
            id: if is_active {
                active_id
            } else {
                BranchId::from_u128(leaf.id.as_u128())
            },
            selected_index,
            entries,
        });
    }
    (branches, active_branch_index)
}

#[cfg(test)]
#[expect(
    clippy::unwrap_used,
    clippy::panic,
    reason = "tests may panic on invariant violations"
)]
mod tests {
    use super::*;

    fn message(text: &str) -> EntryContent {
        EntryContent::Message(Message::with_text(Role::User, text))
    }

    fn compaction(detail: serde_json::Value) -> EntryContent {
        EntryContent::Meta {
            kind: COMPACTION_KIND.to_string(),
            detail: StructuredData::from_serializable(&detail).unwrap(),
        }
    }

    fn text_of(session: &Session, entry: &Entry) -> String {
        match session.content(entry).unwrap() {
            EntryContent::Message(m) => format!("{}", m.blocks.len()),
            EntryContent::Meta { kind, .. } => kind,
            other => panic!("unexpected content {other:?}"),
        }
    }

    /// The frame of every record in `entries`, content copied from `session`.
    fn frames_of(session: &Session, entries: &[Entry]) -> Vec<Frame> {
        let mut frames = vec![Frame::Header {
            header: session.header(),
        }];
        frames.extend(entries.iter().map(|entry| Frame::Known {
            entry: EntryFrame {
                id: entry.id,
                parent: entry.parent,
                timestamp_ms: entry.timestamp_ms,
                kind: entry.kind,
                content: session.bytes(entry).to_vec(),
            },
        }));
        frames
    }

    fn frames(session: &Session) -> Vec<Frame> {
        frames_of(session, &session.active_branch().entries)
    }

    /// Frames of entries built outside any session, under a fresh header;
    /// the only way to store parent links a session would not append.
    fn stored(entries: &[EntryFrame]) -> (SessionHeader, Vec<Frame>) {
        let header = SessionHeader {
            session_id: SessionId::new(),
            branch_id: BranchId::new(),
        };
        let mut frames = vec![Frame::Header { header }];
        frames.extend(entries.iter().cloned().map(|entry| Frame::Known { entry }));
        (header, frames)
    }

    fn ids(branch: &Branch) -> Vec<EntryId> {
        branch.entries.iter().map(|entry| entry.id).collect()
    }

    #[test]
    fn restoring_rejects_duplicate_headers() {
        let header = Session::new().header();
        let error = Session::from_frames(&[Frame::Header { header }, Frame::Header { header }])
            .unwrap_err();
        assert_eq!(error.code(), "SESSION_HEADER_DUPLICATE");
    }

    #[test]
    fn restoring_rejects_a_header_after_an_entry() {
        let entry = EntryFrame::new(None, &message("before header")).unwrap();
        let error = Session::from_frames(&[
            Frame::Known { entry },
            Frame::Header {
                header: Session::new().header(),
            },
        ])
        .unwrap_err();
        assert_eq!(error.code(), "SESSION_HEADER_NOT_FIRST");
    }

    #[test]
    fn selection_frame_restores_ancestor_without_minting_identity() {
        let root = EntryFrame::new(None, &message("root")).unwrap();
        let child = EntryFrame::new(Some(root.id), &message("child")).unwrap();
        let (header, mut frames) = stored(&[root.clone(), child]);
        frames.push(Frame::Selection { id: root.id });
        let restored = Session::from_frames(&frames).unwrap();
        assert_eq!(restored.selected_id(), Some(root.id));
        assert_eq!(restored.active_branch().id(), header.branch_id);
        assert_eq!(
            restored
                .fork_at(root.id)
                .unwrap()
                .active_branch()
                .entries()
                .len(),
            1
        );
    }

    #[test]
    fn restoring_rejects_selection_of_missing_entry() {
        let (_, mut frames) = stored(&[EntryFrame::new(None, &message("root")).unwrap()]);
        frames.push(Frame::Selection { id: EntryId::new() });

        assert_eq!(
            Session::from_frames(&frames).unwrap_err().code(),
            "ENTRY_NOT_FOUND"
        );
    }

    #[test]
    fn sibling_entries_restore_as_separate_branches() {
        // Stored parent links record two children of one root.
        let root = EntryFrame::new(None, &message("root")).unwrap();
        let first = EntryFrame::new(Some(root.id), &message("first")).unwrap();
        let second = EntryFrame::new(Some(root.id), &message("second")).unwrap();
        let (header, stored) = stored(&[root.clone(), first.clone(), second.clone()]);
        let session = Session::from_frames(&stored).unwrap();
        assert_eq!(session.branches.len(), 2);
        // The last stored entry selects the active branch and keeps the
        // persisted branch identity.
        assert_eq!(session.selected_id(), Some(second.id));
        assert_eq!(session.active_branch().id, header.branch_id);
        assert_eq!(ids(session.active_branch()), vec![root.id, second.id]);
        let sibling = &session.branches[1 - session.active_branch_index];
        assert_eq!(ids(sibling), vec![root.id, first.id]);
        // Shared-prefix records point at the same content bytes.
        assert_eq!(
            session.content(&sibling.entries[0]).unwrap(),
            EntryContent::decode(&root.content).unwrap()
        );
        // Reconstruction is identical on a second pass, sibling ids included.
        let again = Session::from_frames(&stored).unwrap();
        assert_eq!(again, session);
    }

    #[test]
    fn no_selection_restores_last_branch_and_compaction_boundary() {
        let root = EntryFrame::new(None, &message("root")).unwrap();
        let first = EntryFrame::new(Some(root.id), &message("first")).unwrap();
        let boundary = EntryFrame::new(
            Some(first.id),
            &compaction(serde_json::json!({"summary": ""})),
        )
        .unwrap();
        let sibling = EntryFrame::new(Some(root.id), &message("sibling")).unwrap();
        let sibling_id = sibling.id;
        let last = EntryFrame::new(Some(sibling_id), &message("last")).unwrap();
        let (header, stored) =
            stored(&[root.clone(), first, boundary.clone(), sibling, last.clone()]);

        let restored = Session::from_frames(&stored).unwrap();

        assert_eq!(restored.selected_id(), Some(last.id));
        assert_eq!(restored.active_branch().id, header.branch_id);
        assert_eq!(
            ids(restored.active_branch()),
            vec![root.id, sibling_id, last.id]
        );
        assert_eq!(restored.compaction_boundary, Some(boundary.id));
    }

    #[test]
    fn legacy_tool_result_without_mutation_metadata_is_unknown() {
        let mut session = Session::new();
        let call_id = ToolCallId::new();
        session
            .append(&EntryContent::ToolResult {
                ok: true,
                output: "legacy".to_string(),
                call_id,
            })
            .unwrap();
        assert_eq!(
            session.mutation_state(call_id).unwrap(),
            MutationState::Unknown
        );
    }

    #[test]
    fn binary_content_borrows_text_and_decodes_all_variants() {
        let id = ToolCallId::new();
        let mut message = Message::with_text(Role::Assistant, "text");
        message.add_block(ContentBlock::Thinking("thought".into()));
        message.add_block(ContentBlock::tool_use(
            "write",
            ToolArguments::from_serializable(&serde_json::json!({"n": -1, "ok": true})).unwrap(),
            id,
        ));
        message.add_block(ContentBlock::tool_result(id, false, "failed"));
        for content in [
            EntryContent::Message(message),
            EntryContent::ToolCall {
                name: "write".into(),
                input: ToolArguments::from_serializable(&serde_json::json!([null, 1.5, u64::MAX]))
                    .unwrap(),
                call_id: id,
            },
            EntryContent::ToolResult {
                ok: false,
                output: "output".into(),
                call_id: id,
            },
            EntryContent::Meta {
                kind: "event".into(),
                detail: StructuredData::from_serializable(
                    &serde_json::json!({"nested": [1, false]}),
                )
                .unwrap(),
            },
        ] {
            let bytes = EntryFrame::new(None, &content).unwrap().content;
            assert_eq!(EntryContent::decode(&bytes).unwrap(), content);
            assert_eq!(
                serde_json::to_value(EntryContentView::decode(&bytes).unwrap()).unwrap(),
                serde_json::to_value(&content).unwrap()
            );
            if let EntryContentView::ToolResult { output, .. } =
                EntryContentView::decode(&bytes).unwrap()
            {
                assert_eq!(output.as_ptr(), bytes[6..].as_ptr());
            }
        }
    }

    #[test]
    fn structured_payload_rejects_trailing_cbor_and_invalid_nested_values() {
        let content = EntryContent::Meta {
            kind: "k".into(),
            detail: StructuredData::from_serializable(&serde_json::json!([1, true])).unwrap(),
        };
        let mut invalid = EntryFrame::new(None, &content).unwrap().content;
        let len_at = 1 + 4 + 1;
        let length = u32::from_le_bytes(invalid[len_at..len_at + 4].try_into().unwrap());
        invalid[len_at..len_at + 4].copy_from_slice(&(length + 1).to_le_bytes());
        invalid.push(0);
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
        invalid.pop();
        invalid[len_at..len_at + 4].copy_from_slice(&length.to_le_bytes());
        invalid[len_at + 4] = 0xff;
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
    }

    #[test]
    fn typed_content_rejects_malformed_lengths_tags_utf8_and_trailing_bytes() {
        let content = EntryContent::ToolResult {
            ok: true,
            output: "hello".into(),
            call_id: ToolCallId::new(),
        };
        let bytes = EntryFrame::new(None, &content).unwrap().content;
        for end in 0..bytes.len() {
            assert_eq!(
                EntryContentView::decode(&bytes[..end]).unwrap_err().code(),
                "ENTRY_DECODE"
            );
        }
        let mut invalid = bytes.clone();
        invalid[1] = 2;
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
        invalid = bytes.clone();
        invalid[2..6].copy_from_slice(&u32::MAX.to_le_bytes());
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
        invalid = bytes.clone();
        invalid[6] = 0xff;
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
        invalid = bytes;
        invalid.push(0);
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
        invalid[0] = 255;
        assert_eq!(
            EntryContentView::decode(&invalid).unwrap_err().code(),
            "ENTRY_DECODE"
        );
    }

    #[test]
    fn appended_content_decodes_from_the_arena_and_records_its_kind() {
        let mut session = Session::new();
        let content = EntryContent::ToolResult {
            ok: false,
            output: "boom".to_string(),
            call_id: ToolCallId::new(),
        };
        let entry = *session.append(&content).unwrap();
        assert_eq!(entry.kind, content.kind());
        assert_eq!(session.content(&entry).unwrap(), content);
        assert_eq!(session.bytes(&entry).len(), entry.content.len as usize);
        // A record from elsewhere reads as empty bytes and fails to decode.
        let foreign = Entry {
            content: Span {
                offset: 1 << 20,
                len: 4,
            },
            ..entry
        };
        assert!(session.bytes(&foreign).is_empty());
        assert_eq!(
            session.content(&foreign).unwrap_err().code(),
            "ENTRY_DECODE"
        );
    }

    #[test]
    fn failed_persist_leaves_the_session_unchanged() {
        let mut session = Session::new();
        session.append(&message("kept")).unwrap();
        let before = session.clone();
        let error = session
            .append_with(
                &EntryContent::Meta {
                    kind: COMPACTION_KIND.to_string(),
                    detail: StructuredData::from_serializable(&serde_json::json!({"summary": ""}))
                        .unwrap(),
                },
                |_, bytes| {
                    assert!(!bytes.is_empty());
                    Err(SmithError::Cancelled)
                },
            )
            .unwrap_err();
        assert_eq!(error.code(), "CANCELLED");
        assert_eq!(session, before);
    }

    #[test]
    fn fork_preserves_ids_order_leaf_and_compaction_boundary() {
        let mut session = Session::new();
        let root = *session.append(&message("root")).unwrap();
        let boundary = *session
            .append(&compaction(
                serde_json::json!({"summary": "", "summarized": 1}),
            ))
            .unwrap();
        let after = *session.append(&message("after")).unwrap();

        let forked = session.fork_at(boundary.id).unwrap();
        assert_ne!(forked.id, session.id);
        assert_eq!(ids(forked.active_branch()), vec![root.id, boundary.id]);
        assert_eq!(forked.selected_id(), Some(boundary.id));
        assert_eq!(forked.compaction_boundary, Some(boundary.id));
        assert_eq!(
            forked
                .active_branch()
                .entries
                .iter()
                .map(|entry| text_of(&forked, entry))
                .collect::<Vec<_>>(),
            vec!["1".to_string(), COMPACTION_KIND.to_string()]
        );
        // The fork carries only the arena prefix its records cover.
        assert_eq!(forked.bytes(&after), &[] as &[u8]);

        // Forking before the boundary drops it, because it is not retained.
        let earlier = session.fork_at(root.id).unwrap();
        assert_eq!(earlier.compaction_boundary, None);
        assert_eq!(earlier.active_branch().entries.len(), 1);

        // The source session is untouched.
        assert_eq!(session.active_branch().entries.len(), 3);
        assert_eq!(session.selected_id(), Some(after.id));
    }

    #[test]
    fn cbor_map_keys_follow_encoded_length_then_byte_order() {
        let content = EntryContent::Meta {
            kind: "event".into(),
            detail: StructuredData::from_serializable(&serde_json::json!({"aa": 0, "b": 1}))
                .unwrap(),
        };
        let bytes = EntryFrame::new(None, &content).unwrap().content;
        let short = bytes.windows(2).position(|pair| pair == b"\x61b").unwrap();
        let long = bytes.windows(3).position(|pair| pair == b"\x62aa").unwrap();
        assert!(
            short < long,
            "CBOR canonical order puts shorter encoded key first"
        );
    }

    fn nested_json(depth: u8) -> serde_json::Value {
        let mut value = serde_json::Value::Null;
        for _ in 0..depth {
            value = serde_json::Value::Array(vec![value]);
        }
        value
    }

    #[test]
    fn canonical_json_accepts_64_containers_and_rejects_65() {
        let at_limit =
            StructuredData::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX))
                .unwrap();
        assert!(
            at_limit
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap()
        );
        assert_eq!(
            at_limit
                .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap(),
            at_limit
        );
        let over_limit =
            StructuredData::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX + 1))
                .unwrap();
        assert_eq!(
            over_limit
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap_err(),
            AccessError::DepthExceeded {
                depth_limit: crate::json_scan::JSON_DEPTH_MAX
            }
        );
        assert_eq!(
            over_limit
                .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap_err(),
            AccessError::DepthExceeded {
                depth_limit: crate::json_scan::JSON_DEPTH_MAX
            }
        );
    }

    #[test]
    fn canonical_json_orders_unordered_maps_within_the_depth_bound() {
        let unordered = StructuredData::from_serializable(
            &serde_json::json!({"long": {"z": 1, "a": 2}, "b": 3}),
        )
        .unwrap();
        assert!(
            !unordered
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap()
        );
        let canonical = unordered
            .canonicalized(crate::json_scan::JSON_DEPTH_MAX)
            .unwrap();
        assert!(
            canonical
                .is_canonical(crate::json_scan::JSON_DEPTH_MAX)
                .unwrap()
        );
        assert_eq!(
            serde_json::to_value(canonical).unwrap(),
            serde_json::to_value(unordered).unwrap()
        );
    }

    #[test]
    fn message_canonicalization_rejects_a_later_overdeep_tool_input() {
        let mut message = Message::new(Role::Assistant);
        message.add_block(ContentBlock::tool_use(
            "first",
            ToolArguments::from_serializable(&serde_json::json!({"z": 1, "a": 2})).unwrap(),
            ToolCallId::new(),
        ));
        message.add_block(ContentBlock::tool_use(
            "later",
            ToolArguments::from_serializable(&nested_json(crate::json_scan::JSON_DEPTH_MAX + 1))
                .unwrap(),
            ToolCallId::new(),
        ));
        assert_eq!(
            EntryFrame::new(None, &EntryContent::Message(message))
                .unwrap_err()
                .code(),
            "ENTRY_DECODE"
        );
    }

    #[test]
    fn tool_use_json_inside_messages_encodes_identically() {
        let call_id = ToolCallId::new();
        let block = |input: serde_json::Value| {
            let mut message = Message::with_text(Role::Assistant, "call");
            message.add_block(ContentBlock::ToolUse {
                name: "write".to_string(),
                input: ToolArguments::from_serializable(&input).unwrap(),
                call_id,
            });
            message
        };
        let first = block(serde_json::json!({"path": "p", "content": "c"}));
        let mut second = block(serde_json::json!({"content": "c", "path": "p"}));
        // Only key order may differ, so encodings must match.
        second.id = first.id;
        second.timestamp_ms = first.timestamp_ms;
        let mut session = Session::new();
        let a = *session.append(&EntryContent::Message(first)).unwrap();
        let b = *session.append(&EntryContent::Message(second)).unwrap();
        assert_eq!(session.bytes(&a), session.bytes(&b));
    }

    #[test]
    fn fork_rejects_unknown_entries() {
        let session = Session::new();
        let error = session.fork_at(EntryId::new()).unwrap_err();
        assert_eq!(error.code(), "ENTRY_NOT_FOUND");
    }

    #[test]
    fn fork_rejects_broken_ancestry_and_forks_sibling_branches() {
        // A stored chain whose root parent is missing restores, but cannot fork.
        let orphan = EntryFrame::new(Some(EntryId::new()), &message("orphan")).unwrap();
        let child = EntryFrame::new(Some(orphan.id), &message("child")).unwrap();
        let broken = Session::from_frames(&stored(&[orphan, child.clone()]).1).unwrap();
        assert_eq!(broken.selected_id(), Some(child.id));
        let error = broken.fork_at(child.id).unwrap_err();
        assert_eq!(error.code(), "ENTRY_NOT_FOUND");

        // A leaf on an inactive sibling branch forks along its own chain.
        let root = EntryFrame::new(None, &message("root")).unwrap();
        let first = EntryFrame::new(Some(root.id), &message("first")).unwrap();
        let second = EntryFrame::new(Some(root.id), &message("second")).unwrap();
        let siblings =
            Session::from_frames(&stored(&[root.clone(), first.clone(), second]).1).unwrap();
        let forked = siblings.fork_at(first.id).unwrap();
        assert_eq!(ids(forked.active_branch()), vec![root.id, first.id]);
        assert_eq!(text_of(&forked, &forked.active_branch().entries[1]), "1");
    }

    #[test]
    fn loading_copies_content_bytes_without_decoding_them() {
        let mut scratch = Session::new();
        let root = *scratch.append(&message("root")).unwrap();
        let mut stored = frames(&scratch);
        if let Frame::Known { entry } = &mut stored[1] {
            entry.content = vec![0xFF, 0x00];
        }
        let restored = Session::from_frames(&stored).unwrap();
        let record = restored.active_branch().entries[0];
        assert_eq!(record.kind, root.kind);
        assert_eq!(restored.bytes(&record), &[0xFF, 0x00]);
        assert_eq!(
            restored.content(&record).unwrap_err().code(),
            "ENTRY_DECODE"
        );
    }

    #[test]
    fn content_of_a_record_whose_kind_lies_is_a_kind_mismatch() {
        let mut scratch = Session::new();
        scratch.append(&message("root")).unwrap();
        let mut stored = frames(&scratch);
        let lie = EntryKind::ToolCall {
            call_id: ToolCallId::new(),
        };
        if let Frame::Known { entry } = &mut stored[1] {
            entry.kind = lie;
        }
        // Loading trusts the record; only a decode can tell.
        let restored = Session::from_frames(&stored).unwrap();
        let record = restored.active_branch().entries[0];
        assert_eq!(record.kind, lie);
        assert_eq!(
            restored.content(&record).unwrap_err().code(),
            "ENTRY_KIND_MISMATCH"
        );
    }

    #[test]
    fn compaction_boundary_survives_a_frame_round_trip() {
        let mut session = Session::new();
        session.append(&message("root")).unwrap();
        let boundary = *session
            .append(&compaction(serde_json::json!({"summary": ""})))
            .unwrap();
        let restored = Session::from_frames(&frames(&session)).unwrap();
        assert_eq!(restored.compaction_boundary, Some(boundary.id));
        assert_eq!(restored.id, session.id);
        assert_eq!(restored.active_branch().id, session.header().branch_id);
        assert_eq!(restored, session);
    }
}

/// Laws of canonicalization, span arithmetic, and content encoding
/// (`SMH-SPEC-SPEC0001`, Sessions).
///
/// `serde_json` preserves insertion order but compares maps order-blind, so
/// key-order laws compare serialized bytes, never values.
#[cfg(test)]
pub mod properties {
    use super::*;
    use proptest::collection::vec;
    use proptest::prelude::*;
    use serde_json::Value;

    /// Arbitrary JSON, at most three levels deep and 24 nodes large.
    fn json() -> impl Strategy<Value = Value> {
        let leaf = prop_oneof![
            Just(Value::Null),
            any::<bool>().prop_map(Value::from),
            any::<i64>().prop_map(Value::from),
            any::<u64>().prop_map(Value::from),
            (proptest::num::f64::NORMAL | proptest::num::f64::ZERO).prop_map(Value::from),
            "\\PC{0,8}".prop_map(Value::from),
        ];
        leaf.prop_recursive(3, 24, 4, |inner| {
            prop_oneof![
                vec(inner.clone(), 0..4).prop_map(Value::Array),
                vec(("\\PC{0,4}", inner), 0..5)
                    .prop_map(|entries| Value::Object(entries.into_iter().collect())),
            ]
        })
    }

    /// `value` with the keys of every object reordered as `seed` dictates.
    fn reordered(value: &Value, seed: u64) -> Value {
        match value {
            Value::Object(map) => {
                let mut entries: Vec<(String, Value)> = map
                    .iter()
                    .map(|(key, value)| (key.clone(), reordered(value, seed.rotate_left(7))))
                    .collect();
                if !entries.is_empty() {
                    let by = usize::try_from(seed % entries.len() as u64).unwrap_or(0);
                    entries.rotate_left(by);
                }
                if seed & 1 == 1 {
                    entries.reverse();
                }
                Value::Object(entries.into_iter().collect())
            }
            Value::Array(items) => Value::Array(
                items
                    .iter()
                    .map(|item| reordered(item, seed.rotate_left(7)))
                    .collect(),
            ),
            other => other.clone(),
        }
    }

    /// Any message role.
    pub fn role() -> impl Strategy<Value = Role> {
        prop_oneof![
            Just(Role::User),
            Just(Role::Assistant),
            Just(Role::System),
            Just(Role::Tool),
        ]
    }

    fn block() -> impl Strategy<Value = ContentBlock> {
        prop_oneof![
            "\\PC{0,16}".prop_map(ContentBlock::Text),
            "\\PC{0,16}".prop_map(ContentBlock::Thinking),
            ("\\PC{0,8}", json(), any::<u128>()).prop_map(|(name, input, id)| {
                ContentBlock::tool_use(
                    name,
                    ToolArguments::from_serializable(&input).unwrap(),
                    ToolCallId::from_u128(id),
                )
            }),
            (any::<u128>(), any::<bool>(), "\\PC{0,16}").prop_map(|(id, ok, output)| {
                ContentBlock::tool_result(ToolCallId::from_u128(id), ok, output)
            }),
        ]
    }

    fn content() -> impl Strategy<Value = EntryContent> {
        prop_oneof![
            (any::<u128>(), role(), vec(block(), 0..4), any::<i64>()).prop_map(
                |(id, role, blocks, timestamp_ms)| EntryContent::Message(Message {
                    id: MessageId::from_u128(id),
                    role,
                    blocks,
                    timestamp_ms,
                })
            ),
            ("\\PC{0,8}", json(), any::<u128>()).prop_map(|(name, input, id)| {
                EntryContent::ToolCall {
                    name,
                    input: ToolArguments::from_serializable(&input).unwrap(),
                    call_id: ToolCallId::from_u128(id),
                }
            }),
            (any::<bool>(), "\\PC{0,16}", any::<u128>()).prop_map(|(ok, output, id)| {
                EntryContent::ToolResult {
                    ok,
                    output,
                    call_id: ToolCallId::from_u128(id),
                }
            }),
            ("\\PC{0,8}", json())
                .prop_filter("extension metadata kind", |(kind, _)| {
                    kind != COMPACTION_KIND
                        && kind != PROVIDER_HTTP_KIND
                        && kind != TOOL_MUTATION_KIND
                })
                .prop_map(|(kind, detail)| EntryContent::Meta {
                    kind,
                    detail: StructuredData::from_serializable(&detail).unwrap(),
                }),
            ("\\PC{0,8}", prop::option::of(any::<bool>())).prop_map(|(summary, complete)| {
                let mut detail = serde_json::Map::new();
                detail.insert("summary".to_string(), Value::String(summary));
                if let Some(complete) = complete {
                    detail.insert("complete".to_string(), Value::Bool(complete));
                }
                EntryContent::Meta {
                    kind: COMPACTION_KIND.to_string(),
                    detail: StructuredData::from_serializable(&Value::Object(detail)).unwrap(),
                }
            }),
        ]
    }

    /// Offsets and lengths around the 4 GiB arena cap, plus anything.
    fn arena_size() -> impl Strategy<Value = usize> {
        let cap = u32::MAX as usize;
        prop_oneof![
            0..=cap,
            cap - 64..=cap + 64,
            (0..=64_usize).prop_map(move |below| cap - below),
            any::<usize>(),
        ]
    }

    proptest! {
        #[test]
        fn canonical_json_is_idempotent_and_keeps_the_value(value in json()) {
            let value = StructuredData::from_serializable(&value).map_err(|error| TestCaseError::fail(error.to_string()))?;
            let once = value.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?;
            let twice = once.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?;
            prop_assert_eq!(serde_json::to_string(&twice).unwrap(), serde_json::to_string(&once).unwrap());
            prop_assert!(once.is_canonical(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?);
            prop_assert_eq!(once, value);
        }

        #[test]
        fn canonical_json_ignores_key_order(value in json(), seed in any::<u64>()) {
            let shuffled = reordered(&value, seed);
            let canonical_shuffled = StructuredData::from_serializable(&shuffled).unwrap().canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?;
            let canonical_value = StructuredData::from_serializable(&value).unwrap().canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?;
            prop_assert_eq!(serde_json::to_string(&canonical_shuffled).unwrap(), serde_json::to_string(&canonical_value).unwrap());
        }

        #[test]
        fn is_canonical_holds_exactly_when_canonicalizing_changes_nothing(value in json()) {
            let value = StructuredData::from_serializable(&value).unwrap();
            let canonical = value.canonicalized(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?;
            prop_assert_eq!(
                value.is_canonical(crate::json_scan::JSON_DEPTH_MAX).map_err(|error| TestCaseError::fail(error.to_string()))?,
                serde_json::to_string(&canonical).unwrap() == serde_json::to_string(&value).unwrap()
            );
        }

        #[test]
        fn span_at_is_exact_below_the_cap_and_an_error_above(
            offset in arena_size(),
            len in arena_size(),
        ) {
            let fits = offset
                .checked_add(len)
                .is_some_and(|end| u32::try_from(end).is_ok());
            match Span::at(offset, len) {
                Ok(span) => {
                    prop_assert!(fits);
                    prop_assert_eq!(span.offset as usize, offset);
                    prop_assert_eq!(span.len as usize, len);
                    prop_assert_eq!(span.end(), offset + len);
                    prop_assert_eq!(span.range().len(), len);
                }
                Err(error) => {
                    prop_assert!(!fits);
                    prop_assert_eq!(error.code(), "SESSION_ARENA_FULL");
                }
            }
        }

        #[test]
        fn arbitrary_content_bytes_never_panic(bytes in vec(any::<u8>(), 0..512)) {
            let _ = EntryContentView::decode(&bytes);
        }

        #[test]
        fn owned_and_borrowed_encodings_are_canonical_and_decode_back(content in content()) {
            let owned = EntryFrame::new(None, &content)
                .map_err(|e| TestCaseError::fail(e.to_string()))?
                .content;
            let mut session = Session::new();
            let record = *session
                .append(EntryContentRef::from(&content))
                .map_err(|e| TestCaseError::fail(e.to_string()))?;
            prop_assert_eq!(session.bytes(&record), owned.as_slice());
            let view = EntryContentView::decode(&owned)
                .map_err(|e| TestCaseError::fail(e.to_string()))?;
            prop_assert_eq!(view.kind(), content.kind());
            prop_assert_eq!(view.to_owned().unwrap(), content.clone());
            prop_assert_eq!(
                EntryContent::decode(&owned).map_err(|e| TestCaseError::fail(e.to_string()))?,
                content
            );
        }
    }
}