Skip to main content

codewhale_core/
journal.rs

1//! Session tree journal placeholder (issue #5262).
2//!
3//! The journal is append-only with an in-memory tree projection:
4//! every non-header entry carries `id` + `parentId`, the active position is
5//! a `leafId`, appending creates a child of the leaf, and branching only
6//! moves the leaf — it never rewrites history. This file lands the entry
7//! shape that #5262's tree operations hang off of; compaction and
8//! branch-summary entry kinds are included as first-class kinds but their
9//! *strategies* are deferred.
10//!
11//! Re-exports the protocol journal as the canonical shape so `protocol` and
12//! `core` agree on the wire. `core` adds the `SessionJournal` wrapper that
13//! owns the `current_leaf_id` column in `state.threads`.
14
15pub use codewhale_protocol::journal::{Journal, JournalEntry};
16
17use serde::{Deserialize, Serialize};
18
19/// Persisted thread metadata extension for the tree. This is the
20/// `current_leaf_id` column added to `state.threads`; `None` before the
21/// first turn, `Some(id)` after. The existing `threads` JSON shape is
22/// otherwise unchanged (back-compat: old rows read as `None` and the next
23/// append mints the header leaf).
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct ThreadLeafState {
26    pub thread_id: String,
27    pub leaf_id: Option<String>,
28}
29
30/// First-class journal entry kinds (data shape lands now; strategies later).
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum JournalKind {
33    Header,
34    User,
35    Assistant,
36    ToolResult,
37    Compaction,
38    BranchSummary,
39}
40
41impl JournalKind {
42    #[must_use]
43    pub fn as_str(self) -> &'static str {
44        match self {
45            Self::Header => "header",
46            Self::User => "user",
47            Self::Assistant => "assistant",
48            Self::ToolResult => "tool_result",
49            Self::Compaction => "compaction",
50            Self::BranchSummary => "branch_summary",
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use serde_json::json;
59
60    #[test]
61    fn leaf_state_roundtrip() {
62        let s = ThreadLeafState {
63            thread_id: "thread-1".into(),
64            leaf_id: Some("entry-abc".into()),
65        };
66        let j = serde_json::to_string(&s).unwrap();
67        let back: ThreadLeafState = serde_json::from_str(&j).unwrap();
68        assert_eq!(back, s);
69    }
70
71    #[test]
72    fn journal_append_is_child_of_leaf() {
73        let mut j = Journal::new();
74        let a = j.append("header", json!({}));
75        let b = j.append("user", json!("hi"));
76        assert_eq!(j.get(&b).unwrap().parent_id.as_deref(), Some(a.as_str()));
77    }
78}