tmx/agents/
session_meta.rs1use std::fs;
22use std::path::PathBuf;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SessionStatus {
26 Busy,
28 Idle,
30 Waiting,
32}
33
34#[derive(Debug, Clone)]
35pub struct SessionMeta {
36 pub status: SessionStatus,
37 pub session_id: String,
38 pub updated_at_ms: i64,
42}
43
44#[derive(serde::Deserialize)]
45struct Raw {
46 #[serde(default)]
47 status: String,
48 #[serde(default, rename = "sessionId")]
49 session_id: String,
50 #[serde(default, rename = "updatedAt")]
51 updated_at: i64,
52}
53
54pub fn session_path(pid: u32) -> Option<PathBuf> {
55 Some(dirs::home_dir()?.join(format!(".claude/sessions/{pid}.json")))
56}
57
58pub fn read(pid: u32) -> Option<SessionMeta> {
62 let path = session_path(pid)?;
63 let bytes = fs::read(&path).ok()?;
64 parse(&bytes)
65}
66
67fn parse(bytes: &[u8]) -> Option<SessionMeta> {
68 let raw: Raw = serde_json::from_slice(bytes).ok()?;
69 let status = parse_status(&raw.status)?;
70 Some(SessionMeta {
71 status,
72 session_id: raw.session_id,
73 updated_at_ms: raw.updated_at,
74 })
75}
76
77fn parse_status(s: &str) -> Option<SessionStatus> {
78 match s {
79 "busy" => Some(SessionStatus::Busy),
80 "idle" => Some(SessionStatus::Idle),
81 "waiting" => Some(SessionStatus::Waiting),
82 _ => None,
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn parse_known_busy() {
92 let json =
93 br#"{"pid":1,"sessionId":"s-1","cwd":"/x","status":"busy","updatedAt":1700000000000}"#;
94 let m = parse(json).expect("parses");
95 assert_eq!(m.status, SessionStatus::Busy);
96 assert_eq!(m.session_id, "s-1");
97 assert_eq!(m.updated_at_ms, 1700000000000);
98 }
99
100 #[test]
101 fn parse_known_idle() {
102 let json = br#"{"status":"idle","sessionId":"sid"}"#;
103 let m = parse(json).expect("parses");
104 assert_eq!(m.status, SessionStatus::Idle);
105 }
106
107 #[test]
108 fn parse_known_waiting() {
109 let json = br#"{"status":"waiting"}"#;
110 let m = parse(json).expect("parses");
111 assert_eq!(m.status, SessionStatus::Waiting);
112 }
113
114 #[test]
115 fn parse_unknown_status_returns_none() {
116 let json = br#"{"status":"thinking-hard"}"#;
118 assert!(parse(json).is_none());
119 }
120
121 #[test]
122 fn parse_malformed_json_returns_none() {
123 assert!(parse(b"not json").is_none());
124 assert!(parse(b"").is_none());
125 assert!(parse(br#"{"status":"bu"#).is_none());
128 }
129
130 #[test]
131 fn parse_missing_status_field_returns_none() {
132 let json = br#"{"sessionId":"s-1"}"#;
133 assert!(parse(json).is_none());
134 }
135
136 #[test]
137 fn parse_tolerates_extra_fields() {
138 let json = br#"{
141 "pid": 22650,
142 "sessionId": "31e571fa-2230-4854-908a-5d13e7a017b6",
143 "cwd": "/home/x/dev/tmx",
144 "startedAt": 1779149382456,
145 "procStart": "126214",
146 "version": "2.1.143",
147 "peerProtocol": 1,
148 "kind": "interactive",
149 "entrypoint": "cli",
150 "status": "busy",
151 "updatedAt": 1779165593130
152 }"#;
153 let m = parse(json).expect("parses real-shape file");
154 assert_eq!(m.status, SessionStatus::Busy);
155 assert_eq!(m.session_id, "31e571fa-2230-4854-908a-5d13e7a017b6");
156 assert_eq!(m.updated_at_ms, 1779165593130);
157 }
158
159 #[test]
160 fn session_path_uses_pid_filename() {
161 let p = session_path(42).expect("home dir present in test env");
162 assert!(p.ends_with(".claude/sessions/42.json"));
163 }
164}