opensession_core/
session.rs1use crate::trace::Session;
2
3pub const ATTR_CWD: &str = "cwd";
4pub const ATTR_WORKING_DIRECTORY: &str = "working_directory";
5pub const ATTR_SOURCE_PATH: &str = "source_path";
6pub const ATTR_SESSION_ROLE: &str = "session_role";
7pub const ATTR_PARENT_SESSION_ID: &str = "parent_session_id";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SessionRole {
11 Primary,
12 Auxiliary,
13}
14
15fn attr_non_empty_str<'a>(session: &'a Session, key: &str) -> Option<&'a str> {
16 session
17 .context
18 .attributes
19 .get(key)
20 .and_then(|value| value.as_str())
21 .map(str::trim)
22 .filter(|value| !value.is_empty())
23}
24
25pub fn working_directory(session: &Session) -> Option<&str> {
26 attr_non_empty_str(session, ATTR_CWD)
27 .or_else(|| attr_non_empty_str(session, ATTR_WORKING_DIRECTORY))
28}
29
30pub fn source_path(session: &Session) -> Option<&str> {
31 attr_non_empty_str(session, ATTR_SOURCE_PATH)
32}
33
34pub fn session_role(session: &Session) -> SessionRole {
35 if let Some(raw_role) = attr_non_empty_str(session, ATTR_SESSION_ROLE) {
36 if raw_role.eq_ignore_ascii_case("auxiliary") {
37 return SessionRole::Auxiliary;
38 }
39 if raw_role.eq_ignore_ascii_case("primary") {
40 return SessionRole::Primary;
41 }
42 }
43
44 if !session.context.related_session_ids.is_empty() {
45 return SessionRole::Auxiliary;
46 }
47
48 if attr_non_empty_str(session, ATTR_PARENT_SESSION_ID).is_some() {
49 return SessionRole::Auxiliary;
50 }
51
52 SessionRole::Primary
53}
54
55pub fn is_auxiliary_session(session: &Session) -> bool {
56 session_role(session) == SessionRole::Auxiliary
57}
58
59pub fn build_git_storage_meta_json(session: &Session) -> Vec<u8> {
60 let role = match session_role(session) {
61 SessionRole::Primary => "primary",
62 SessionRole::Auxiliary => "auxiliary",
63 };
64
65 serde_json::to_vec_pretty(&serde_json::json!({
66 "session_id": session.session_id,
67 "title": session.context.title,
68 "tool": session.agent.tool,
69 "model": session.agent.model,
70 "session_role": role,
71 "stats": session.stats,
72 }))
73 .unwrap_or_default()
74}
75
76#[cfg(test)]
77mod tests {
78 use super::{
79 is_auxiliary_session, session_role, source_path, working_directory, SessionRole,
80 ATTR_PARENT_SESSION_ID, ATTR_SESSION_ROLE,
81 };
82 use crate::trace::{Agent, Session};
83 use serde_json::Value;
84
85 fn make_session() -> Session {
86 Session::new(
87 "s1".to_string(),
88 Agent {
89 provider: "openai".to_string(),
90 model: "gpt-5".to_string(),
91 tool: "codex".to_string(),
92 tool_version: None,
93 },
94 )
95 }
96
97 #[test]
98 fn working_directory_prefers_cwd() {
99 let mut session = make_session();
100 session.context.attributes.insert(
101 "cwd".to_string(),
102 Value::String("/repo/preferred".to_string()),
103 );
104 session.context.attributes.insert(
105 "working_directory".to_string(),
106 Value::String("/repo/fallback".to_string()),
107 );
108
109 assert_eq!(working_directory(&session), Some("/repo/preferred"));
110 }
111
112 #[test]
113 fn working_directory_uses_working_directory_fallback() {
114 let mut session = make_session();
115 session.context.attributes.insert(
116 "working_directory".to_string(),
117 Value::String("/repo/fallback".to_string()),
118 );
119
120 assert_eq!(working_directory(&session), Some("/repo/fallback"));
121 }
122
123 #[test]
124 fn source_path_returns_non_empty_value() {
125 let mut session = make_session();
126 session.context.attributes.insert(
127 "source_path".to_string(),
128 Value::String("/tmp/session.jsonl".to_string()),
129 );
130
131 assert_eq!(source_path(&session), Some("/tmp/session.jsonl"));
132 }
133
134 #[test]
135 fn session_role_uses_explicit_attribute_first() {
136 let mut session = make_session();
137 session.context.related_session_ids = vec!["parent-id".to_string()];
138 session.context.attributes.insert(
139 ATTR_SESSION_ROLE.to_string(),
140 Value::String("primary".to_string()),
141 );
142
143 assert_eq!(session_role(&session), SessionRole::Primary);
144 assert!(!is_auxiliary_session(&session));
145 }
146
147 #[test]
148 fn session_role_uses_related_session_ids() {
149 let mut session = make_session();
150 session.context.related_session_ids = vec!["parent-id".to_string()];
151
152 assert_eq!(session_role(&session), SessionRole::Auxiliary);
153 assert!(is_auxiliary_session(&session));
154 }
155
156 #[test]
157 fn session_role_uses_parent_session_id_attribute() {
158 let mut session = make_session();
159 session.context.attributes.insert(
160 ATTR_PARENT_SESSION_ID.to_string(),
161 Value::String("parent-id".to_string()),
162 );
163
164 assert_eq!(session_role(&session), SessionRole::Auxiliary);
165 }
166
167 #[test]
168 fn session_role_defaults_to_primary() {
169 let session = make_session();
170 assert_eq!(session_role(&session), SessionRole::Primary);
171 }
172}