codewhale_core/
session.rs1use std::path::PathBuf;
19
20use serde::{Deserialize, Serialize};
21
22use crate::ids::{SessionId, ThreadId};
23use crate::journal::Journal;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Thread {
31 pub thread_id: ThreadId,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub leaf_id: Option<String>,
35 #[serde(default)]
38 pub journal: Journal,
39 pub model: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub reasoning_effort: Option<String>,
42 pub workspace: PathBuf,
43 #[serde(default)]
44 pub ephemeral: bool,
45}
46
47impl Thread {
48 #[must_use]
49 pub fn new(thread_id: ThreadId, workspace: PathBuf, model: impl Into<String>) -> Self {
50 Self {
51 thread_id,
52 leaf_id: None,
53 journal: Journal::new(),
54 model: model.into(),
55 reasoning_effort: None,
56 workspace,
57 ephemeral: false,
58 }
59 }
60
61 #[must_use]
62 pub fn leaf_id(&self) -> Option<&str> {
63 self.leaf_id.as_deref()
64 }
65
66 pub fn set_leaf(&mut self, leaf: Option<String>) {
67 self.leaf_id = leaf;
68 }
69}
70
71#[derive(Debug, Clone)]
76pub struct Session {
77 pub session_id: SessionId,
78 pub thread_id: ThreadId,
79 pub model: String,
81 pub workspace: PathBuf,
82 pub messages_revision: u64,
85}
86
87impl Session {
88 #[must_use]
89 pub fn new(thread_id: ThreadId, workspace: PathBuf, model: impl Into<String>) -> Self {
90 Self {
91 session_id: SessionId::new(),
92 thread_id,
93 model: model.into(),
94 workspace,
95 messages_revision: 0,
96 }
97 }
98
99 pub fn bump_revision(&mut self) {
100 self.messages_revision = self.messages_revision.wrapping_add(1);
101 }
102}
103
104#[must_use]
108pub fn session_for_thread(thread: &Thread, workspace: PathBuf) -> Session {
109 Session::new(thread.thread_id.clone(), workspace, thread.model.clone())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn thread_and_session_ids_are_distinct_scopes() {
118 let t = Thread::new(ThreadId::new(), PathBuf::from("/tmp"), "deepseek-v4-flash");
119 let s1 = Session::new(t.thread_id.clone(), PathBuf::from("/tmp"), &t.model);
120 let s2 = Session::new(t.thread_id.clone(), PathBuf::from("/tmp"), &t.model);
121 assert_eq!(s1.thread_id, s2.thread_id);
122 assert_ne!(s1.session_id, s2.session_id);
123 }
124
125 #[test]
126 fn leaf_is_moved_not_rewritten() {
127 let mut t = Thread::new(ThreadId::new(), PathBuf::from("/tmp"), "m");
128 let a = t.journal.append("header", serde_json::json!({}));
129 let b = t.journal.append("user", serde_json::json!("b"));
130 t.leaf_id = t.journal.leaf_id.clone();
131 assert_eq!(t.leaf_id.as_deref(), Some(b.as_str()));
132 assert!(t.journal.branch_to(&a));
133 t.leaf_id = t.journal.leaf_id.clone();
134 assert_eq!(t.leaf_id.as_deref(), Some(a.as_str()));
135 assert_eq!(t.journal.len(), 2); }
137}