Skip to main content

codeoid_protocol/
session.rs

1//! Session metadata — the `SessionInfo` struct and its satellites.
2
3use serde::{Deserialize, Serialize};
4
5/// Lifecycle status of a session.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum SessionStatus {
9    Idle,
10    Working,
11    WaitingApproval,
12    Error,
13}
14
15/// Execution mode — controls tool approval and autonomous budgeting.
16///
17/// * `Guarded` (daemon default) — Read/Grep/Glob/memory are auto-approved;
18///   Write/Edit/Bash/Agent still ask. (≈ Claude Code's default mode.)
19/// * `Interactive` — every tool call asks for approval, including reads.
20/// * `Autonomous` — every tool auto-approved until the turn budget is exhausted.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum SessionMode {
24    Interactive,
25    /// Serializes as `"guarded"`. `auto-allow` accepted as a backward-compat
26    /// alias on the wire (the mode was renamed from `auto-allow`).
27    #[serde(alias = "auto-allow")]
28    Guarded,
29    Autonomous,
30}
31
32impl Default for SessionMode {
33    fn default() -> Self {
34        Self::Interactive
35    }
36}
37
38/// Top-level session metadata — broadcast on attach, list, and every
39/// `session.info_update`.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct SessionInfo {
43    pub id: String,
44    pub name: String,
45    pub workdir: String,
46    pub status: SessionStatus,
47    pub created_by: String,
48    pub created_at: String,
49    pub attached_clients: u32,
50
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub mode: Option<SessionMode>,
53
54    /// Remaining turns budget for autonomous mode. `None` = unbounded, `Some(0)` = exhausted.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub turns_remaining: Option<u32>,
57
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub pinned_files: Option<Vec<String>>,
60
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub agent_uri: Option<String>,
63
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub subagents: Option<Vec<Subagent>>,
66
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub usage: Option<SessionUsage>,
69
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub rotation: Option<RotationInfo>,
72
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub queued_messages: Option<u32>,
75
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub model: Option<String>,
78
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub fallback_model: Option<String>,
81
82    /// Id of the provider backing this session (e.g. "claude", "pi").
83    /// Absent on daemons that predate multi-provider sessions.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub provider_id: Option<String>,
86
87    /// Lineage for a session created via `session.fork`. Absent = not a fork.
88    /// Rendered as a "⑃ from <name>·t<atTurn>" tag in the session title.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub forked_from: Option<ForkedFrom>,
91
92    /// Git worktree backing this session's workdir (fork isolation / bind).
93    /// Absent = shares its workdir with no git isolation. Rendered as a
94    /// "⎇ <branch>" tag in the session title.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub worktree: Option<SessionWorktree>,
97}
98
99/// Where a forked session came from — the parent id, the parent's name at
100/// fork time (a snapshot; the parent may be renamed or gone), and the branch
101/// point (conversation rounds carried over).
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ForkedFrom {
105    pub session_id: String,
106    pub name: String,
107    pub at_turn: u32,
108}
109
110/// A git worktree backing a session's workdir. `created_by_codeoid` marks a
111/// worktree codeoid created for fork isolation (removed on destroy) versus one
112/// the user bound (never touched).
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct SessionWorktree {
116    pub path: String,
117    pub branch: String,
118    pub created_by_codeoid: bool,
119}
120
121/// Rotation telemetry — how many times the backing Claude Code session has
122/// been rolled over to avoid context compaction.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct RotationInfo {
126    pub count: u32,
127    /// Unix ms of last rotation, or null if never rotated.
128    pub last_rotated_at: Option<i64>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub claude_code_session_id: Option<String>,
131}
132
133/// Cumulative usage totals for a session. Aggregated from each SDK `result` event.
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct SessionUsage {
137    pub input_tokens: u64,
138    pub output_tokens: u64,
139    pub cache_read_tokens: u64,
140    pub cache_creation_tokens: u64,
141    pub total_cost_usd: f64,
142    pub num_turns: u32,
143    pub duration_ms: u64,
144
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub recent_turns: Option<Vec<TurnUsage>>,
147
148    /// Max PRIMARY-AGENT context size ever seen on a single turn.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub peak_input_tokens: Option<u64>,
151
152    /// Most recent turn's PRIMARY-AGENT context size.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub last_turn_input_tokens: Option<u64>,
155
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub last_turn_output_tokens: Option<u64>,
158
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub last_turn_cost_usd: Option<f64>,
161
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub last_turn_cache_hit_rate: Option<f64>,
164}
165
166/// Per-turn usage record — one entry per SDK `result` event.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct TurnUsage {
170    pub turn_number: u32,
171    pub created_at: i64,
172    pub input_tokens: u64,
173    pub output_tokens: u64,
174    pub cache_read_tokens: u64,
175    pub cache_creation_tokens: u64,
176    pub total_cost_usd: f64,
177    pub duration_ms: u64,
178
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub stop_reason: Option<String>,
181
182    pub total_input_tokens: u64,
183    pub billable_input_tokens: u64,
184    pub cache_hit_rate: f64,
185}
186
187/// Active sub-agent for the identity chain display.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct Subagent {
191    pub agent_id: String,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub wimse_uri: Option<String>,
194    pub agent_type: String,
195    pub spawned_at: i64,
196    pub active: bool,
197}