Skip to main content

scv_protocol/
daemon.rs

1//! Daemon control: health, delegations, and the commands `scv` sends over
2//! `daemon.control`.
3
4use serde::{Deserialize, Serialize};
5
6/// Where a daemon component (a channel account) is in its lifecycle.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum ComponentState {
10    /// Turned off in configuration.
11    Disabled,
12    /// Starting up.
13    Starting,
14    /// Running and connected to its platform.
15    Connected,
16    /// Running but not connected.
17    Disconnected,
18    /// Waiting before a restart after a failure.
19    Backoff,
20    /// Shutting down.
21    Stopping,
22    /// Not running.
23    Stopped,
24    /// Stopped after an error it cannot recover from, such as expired credentials.
25    Failed,
26}
27
28/// The health of one channel account.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ComponentHealth {
31    /// Component ID, `<channel>:<account>`.
32    pub id: String,
33    /// The chat channel this account belongs to, such as `wechat`.
34    #[serde(default)]
35    pub channel: String,
36    /// The account name within the channel.
37    pub account: String,
38    /// The platform's ID of the bot account, when known.
39    pub bot_id: Option<String>,
40    /// The platform's ID of the account's owner, when known.
41    pub user_id: Option<String>,
42    /// Whether the account should run.
43    pub enabled: bool,
44    /// Lifecycle state.
45    pub state: ComponentState,
46    /// When the platform last answered successfully.
47    pub last_success_unix_seconds: Option<u64>,
48    /// The latest error, if any.
49    pub error: Option<String>,
50    /// Restarts since the daemon started.
51    pub restarts: u64,
52    /// Effective remote tool authority; `owner` only when the owner ID is known.
53    #[serde(default)]
54    pub remote_tools: RemoteTools,
55    /// Who the account answers, as set; `owner` with no known owner ID
56    /// answers nobody. `None` from daemons before 0.3.0, which answer anyone.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub senders: Option<Senders>,
59    /// What a chat account carries, when it is not an ordinary chat:
60    /// `mail` for a mail chat. Absent for ordinary chats, email accounts,
61    /// and daemons before mail.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub purpose: Option<Purpose>,
64    /// An email account's counts: never addresses, subjects, or any other
65    /// mail text. Absent for chat accounts.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub mail: Option<MailCounts>,
68}
69
70/// What a chat account carries.
71#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "snake_case")]
73pub enum Purpose {
74    /// Conversations with a model (the default).
75    #[default]
76    Chat,
77    /// Only the email accounts' reports to the owner: no model ever answers
78    /// in it, it has no tools, and SCV never logs it.
79    Mail,
80}
81
82/// An email account's activity, as counts only.
83#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(default)]
85pub struct MailCounts {
86    /// Mail claimed and not yet decided.
87    pub claimed: u64,
88    /// Report items waiting to be sent to the mail chat.
89    pub queued: u64,
90    /// Mail decided today (the account's local day).
91    pub seen_today: u64,
92    /// Of those, mail a model triaged.
93    pub triaged_today: u64,
94    /// Of those, mail reported to the owner.
95    pub reported_today: u64,
96    /// Model tokens spent today.
97    pub tokens_today: u64,
98    /// The daily token budget.
99    pub token_budget: u64,
100    /// Digest messages sent to the mail chat in the last 24 hours.
101    pub messages_24h: u64,
102    /// When the mailbox was last checked, in Unix seconds.
103    pub last_check_unix_seconds: Option<u64>,
104}
105
106/// Who may use tools through a remote bridge account.
107#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "snake_case")]
109pub enum RemoteTools {
110    /// Every remote session is tool-free (the default).
111    #[default]
112    None,
113    /// The account's authenticated owner gets full, auto-approved tools.
114    Owner,
115}
116
117/// Whose messages a remote bridge account answers.
118#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "snake_case")]
120pub enum Senders {
121    /// Only the account's authenticated owner (the default); everyone else's
122    /// messages are dropped unanswered. Without a known owner ID, nobody.
123    #[default]
124    Owner,
125    /// Anyone who can reach the bot; everyone but the owner stays tool-free.
126    Anyone,
127}
128
129/// What `scv status` shows: the daemon, its components, and its delegations.
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct DaemonStatus {
132    /// The daemon's release.
133    pub version: String,
134    /// The daemon's process ID.
135    pub pid: u32,
136    /// Channel accounts.
137    pub components: Vec<ComponentHealth>,
138    /// Delegated agent runs.
139    #[serde(default)]
140    pub delegations: DelegationSummary,
141    /// A restart the daemon has scheduled, if any.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub restart: Option<RestartInfo>,
144    /// The question a `confirm_ask` or `confirm_status` request is about.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub confirm: Option<ConfirmInfo>,
147}
148
149/// How long a question to the owner waits for an answer unless the asker
150/// says otherwise.
151pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
152/// The longest a question to the owner may wait: the default ceiling of a
153/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
154/// how a delegated agent asks.
155pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
156
157/// A yes/no question to the owner in chat (`scv confirm`).
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub struct ConfirmInfo {
160    /// The question's ID, for `confirm_status`.
161    pub id: String,
162    /// Where it stands.
163    pub state: ConfirmState,
164    /// The account whose owner was asked, as `<channel>:<account>`.
165    pub chat: String,
166    /// When no answer counts as no.
167    pub deadline_unix_seconds: u64,
168}
169
170/// Where a question to the owner stands.
171#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(rename_all = "snake_case")]
173pub enum ConfirmState {
174    /// Sent, or being stored for sending, and waiting for an answer.
175    Pending,
176    /// The owner answered yes.
177    Yes,
178    /// The owner answered no.
179    No,
180    /// No answer came before the deadline, which counts as no.
181    Expired,
182    /// The asker stopped asking about it before an answer came.
183    Withdrawn,
184    /// The question could not be handed to the chat, or its answer was lost.
185    Failed,
186    /// A state this client does not know, from a newer daemon.
187    #[serde(other)]
188    Unknown,
189}
190
191/// A restart into a newly installed release, waiting for owner work to end.
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
193pub struct RestartInfo {
194    /// The release it restarts into.
195    pub to_version: String,
196    /// What it still waits for, such as the requesting delegation or an
197    /// owner's message; `None` once it restarts.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub waiting_for: Option<String>,
200    /// The delegation that asked, whose report goes out first.
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub requester: Option<String>,
203    /// The chat the announcement goes to, as `<channel>:<account>`.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub origin: Option<String>,
206    /// When it restarts even if work is still running.
207    pub deadline_unix_seconds: u64,
208}
209
210/// Delegated agent runs of the daemon's SCV instance.
211#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
212pub struct DelegationSummary {
213    /// Running delegations, whichever SCV process of the instance started them.
214    /// Live agents waiting between turns count too.
215    pub active: u64,
216    /// How many of `active` are live agents (a nested SCV or an ACP agent)
217    /// waiting between turns, apart from a nested SCV whose own background
218    /// jobs still count; `None` from a daemon that does not tell.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub idle: Option<u64>,
221    /// Orphaned delegations the daemon has stopped since it started.
222    pub reaped: u64,
223    /// Listed delegations, for `delegations` and `delegation_kill`.
224    #[serde(default, skip_serializing_if = "Vec::is_empty")]
225    pub entries: Vec<DelegationInfo>,
226    /// Handles this request stopped.
227    #[serde(default, skip_serializing_if = "Vec::is_empty")]
228    pub killed: Vec<String>,
229}
230
231/// One running delegated agent.
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
233pub struct DelegationInfo {
234    /// The delegation to stop.
235    pub handle: String,
236    /// The agent, such as `codex`.
237    pub agent: String,
238    /// The SCV session that started it.
239    pub session: String,
240    /// Delegation depth: 1 for an agent SCV started directly.
241    pub depth: u32,
242    /// The agent's process ID (and process group).
243    pub pid: u32,
244    /// The SCV process that started it.
245    pub owner_pid: u32,
246    /// Live processes in its group plus tagged processes outside it.
247    pub processes: u32,
248    /// Its working directory.
249    pub cwd: String,
250    /// When it started.
251    pub started_unix_seconds: u64,
252    /// The owning SCV process is gone; the daemon will stop it.
253    pub orphaned: bool,
254    /// The conversation this run is a turn of, and which turn.
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub conversation: Option<String>,
257    /// Which turn of that conversation.
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub turn: Option<u32>,
260    /// A live agent (a nested SCV or an ACP agent) with no turn running:
261    /// when its last turn ended. Absent while it works, for a per-turn run,
262    /// and from older daemons.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub idle_since_unix_seconds: Option<u64>,
265    /// A nested SCV's own background jobs that still run or wait to be
266    /// reported to it; a planned restart waits for them even between turns.
267    /// Absent when there are none, for other agents, and from older daemons.
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub background_jobs: Option<u32>,
270}
271
272/// What a `daemon.control` message asks the daemon to do.
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274#[serde(tag = "action", rename_all = "snake_case")]
275pub enum DaemonCommand {
276    /// Report [`DaemonStatus`].
277    Status,
278    /// Reread channel settings and reconcile the components now.
279    Reload,
280    /// Enable or disable one channel account, optionally changing its
281    /// workspace, remote tool grant, and whose messages it answers.
282    ChannelSet {
283        /// The chat channel, such as `wechat` or `feishu`.
284        channel: String,
285        /// The account name within the channel.
286        account: String,
287        /// Whether the account should run.
288        enabled: bool,
289        /// The account's workspace directory; `None` keeps the saved one.
290        workspace: Option<String>,
291        /// Omitted keeps the saved setting.
292        #[serde(default, skip_serializing_if = "Option::is_none")]
293        remote_tools: Option<RemoteTools>,
294        /// Whose messages the account answers; omitted keeps the saved
295        /// setting.
296        #[serde(default, skip_serializing_if = "Option::is_none")]
297        senders: Option<Senders>,
298        /// What a chat account carries; omitted keeps the saved setting.
299        #[serde(default, skip_serializing_if = "Option::is_none")]
300        purpose: Option<Purpose>,
301    },
302    /// Stop one channel account and remove its credentials and state.
303    ChannelLogout {
304        /// The chat channel, such as `wechat` or `feishu`.
305        channel: String,
306        /// The account name within the channel.
307        account: String,
308    },
309    /// List running delegations; `all` includes orphans awaiting cleanup.
310    Delegations {
311        /// Include orphans awaiting cleanup.
312        #[serde(default)]
313        all: bool,
314    },
315    /// Stop one delegation by handle, or every orphaned one.
316    DelegationKill {
317        /// The delegation to stop.
318        #[serde(default, skip_serializing_if = "Option::is_none")]
319        handle: Option<String>,
320        /// Stop every orphaned delegation instead.
321        #[serde(default)]
322        orphans: bool,
323    },
324    /// Restart into the release installed at the daemon's own path once the
325    /// requesting delegation has finished and its report is stored and no
326    /// owner message is being answered, or at `max_wait_seconds` anyway.
327    RestartWhenIdle {
328        /// The release the caller installed; the daemon checks it.
329        #[serde(default, skip_serializing_if = "Option::is_none")]
330        version: Option<String>,
331        /// The commit it was built from, for the announcement.
332        #[serde(default, skip_serializing_if = "Option::is_none")]
333        commit: Option<String>,
334        /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
335        #[serde(default, skip_serializing_if = "Option::is_none")]
336        parent: Option<String>,
337        /// Longest wait before restarting anyway.
338        #[serde(default, skip_serializing_if = "Option::is_none")]
339        max_wait_seconds: Option<u64>,
340    },
341    /// Ask the owner a yes/no question in chat: in the chat that started the
342    /// work `parent` names, or else the notify target. The reply's `confirm`
343    /// names the question; `confirm_status` then follows it.
344    ConfirmAsk {
345        /// The question, as the owner reads it.
346        question: String,
347        /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
348        /// is asked.
349        #[serde(default, skip_serializing_if = "Option::is_none")]
350        parent: Option<String>,
351        /// How long no answer waits before it counts as no
352        /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
353        #[serde(default, skip_serializing_if = "Option::is_none")]
354        timeout_seconds: Option<u64>,
355    },
356    /// Report where the question `id` stands. Asking also keeps it alive: a
357    /// question nobody asks about for a minute is withdrawn.
358    ConfirmStatus {
359        /// The ID `confirm_ask` returned.
360        id: String,
361    },
362}