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}