//! Session tree journal placeholder (issue #5262). //! //! The journal is append-only with an in-memory tree projection: //! every non-header entry carries `id` + `parentId`, the active position is //! a `leafId`, appending creates a child of the leaf, and branching only //! moves the leaf — it never rewrites history. This file lands the entry //! shape that #5262's tree operations hang off of; compaction and //! branch-summary entry kinds are included as first-class kinds but their //! *strategies* are deferred. //! //! Re-exports the protocol journal as the canonical shape so `protocol` and //! `core` agree on the wire. `core` adds the `SessionJournal` wrapper that //! owns the `current_leaf_id` column in `state.threads`. pub use codewhale_protocol::journal::{Journal, JournalEntry}; use serde::{Deserialize, Serialize}; /// Persisted thread metadata extension for the tree. This is the /// `current_leaf_id` column added to `state.threads`; `None` before the /// first turn, `Some(id)` after. The existing `threads` JSON shape is /// otherwise unchanged (back-compat: old rows read as `None` and the next /// append mints the header leaf). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ThreadLeafState { pub thread_id: String, pub leaf_id: Option, } /// First-class journal entry kinds (data shape lands now; strategies later). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum JournalKind { Header, User, Assistant, ToolResult, Compaction, BranchSummary, } impl JournalKind { #[must_use] pub fn as_str(self) -> &'static str { match self { Self::Header => "header", Self::User => "user", Self::Assistant => "assistant", Self::ToolResult => "tool_result", Self::Compaction => "compaction", Self::BranchSummary => "branch_summary", } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn leaf_state_roundtrip() { let s = ThreadLeafState { thread_id: "thread-1".into(), leaf_id: Some("entry-abc".into()), }; let j = serde_json::to_string(&s).unwrap(); let back: ThreadLeafState = serde_json::from_str(&j).unwrap(); assert_eq!(back, s); } #[test] fn journal_append_is_child_of_leaf() { let mut j = Journal::new(); let a = j.append("header", json!({})); let b = j.append("user", json!("hi")); assert_eq!(j.get(&b).unwrap().parent_id.as_deref(), Some(a.as_str())); } }