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}
60
61/// Who may use tools through a remote bridge account.
62#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum RemoteTools {
65    /// Every remote session is tool-free (the default).
66    #[default]
67    None,
68    /// The account's authenticated owner gets full, auto-approved tools.
69    Owner,
70}
71
72/// Whose messages a remote bridge account answers.
73#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum Senders {
76    /// Only the account's authenticated owner (the default); everyone else's
77    /// messages are dropped unanswered. Without a known owner ID, nobody.
78    #[default]
79    Owner,
80    /// Anyone who can reach the bot; everyone but the owner stays tool-free.
81    Anyone,
82}
83
84/// What `scv status` shows: the daemon, its components, and its delegations.
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86pub struct DaemonStatus {
87    /// The daemon's release.
88    pub version: String,
89    /// The daemon's process ID.
90    pub pid: u32,
91    /// Channel accounts.
92    pub components: Vec<ComponentHealth>,
93    /// Delegated agent runs.
94    #[serde(default)]
95    pub delegations: DelegationSummary,
96    /// A restart the daemon has scheduled, if any.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub restart: Option<RestartInfo>,
99    /// The question a `confirm_ask` or `confirm_status` request is about.
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub confirm: Option<ConfirmInfo>,
102}
103
104/// How long a question to the owner waits for an answer unless the asker
105/// says otherwise.
106pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
107/// The longest a question to the owner may wait: the default ceiling of a
108/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
109/// how a delegated agent asks.
110pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
111
112/// A yes/no question to the owner in chat (`scv confirm`).
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct ConfirmInfo {
115    /// The question's ID, for `confirm_status`.
116    pub id: String,
117    /// Where it stands.
118    pub state: ConfirmState,
119    /// The account whose owner was asked, as `<channel>:<account>`.
120    pub chat: String,
121    /// When no answer counts as no.
122    pub deadline_unix_seconds: u64,
123}
124
125/// Where a question to the owner stands.
126#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "snake_case")]
128pub enum ConfirmState {
129    /// Sent, or being stored for sending, and waiting for an answer.
130    Pending,
131    /// The owner answered yes.
132    Yes,
133    /// The owner answered no.
134    No,
135    /// No answer came before the deadline, which counts as no.
136    Expired,
137    /// The asker stopped asking about it before an answer came.
138    Withdrawn,
139    /// The question could not be handed to the chat, or its answer was lost.
140    Failed,
141    /// A state this client does not know, from a newer daemon.
142    #[serde(other)]
143    Unknown,
144}
145
146/// A restart into a newly installed release, waiting for owner work to end.
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub struct RestartInfo {
149    /// The release it restarts into.
150    pub to_version: String,
151    /// What it still waits for, such as the requesting delegation or an
152    /// owner's message; `None` once it restarts.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub waiting_for: Option<String>,
155    /// The delegation that asked, whose report goes out first.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub requester: Option<String>,
158    /// The chat the announcement goes to, as `<channel>:<account>`.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub origin: Option<String>,
161    /// When it restarts even if work is still running.
162    pub deadline_unix_seconds: u64,
163}
164
165/// Delegated agent runs of the daemon's SCV instance.
166#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
167pub struct DelegationSummary {
168    /// Running delegations, whichever SCV process of the instance started them.
169    /// Live agents waiting between turns count too.
170    pub active: u64,
171    /// How many of `active` are live agents (a nested SCV or an ACP agent)
172    /// waiting between turns, apart from a nested SCV whose own background
173    /// jobs still count; `None` from a daemon that does not tell.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub idle: Option<u64>,
176    /// Orphaned delegations the daemon has stopped since it started.
177    pub reaped: u64,
178    /// Listed delegations, for `delegations` and `delegation_kill`.
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub entries: Vec<DelegationInfo>,
181    /// Handles this request stopped.
182    #[serde(default, skip_serializing_if = "Vec::is_empty")]
183    pub killed: Vec<String>,
184}
185
186/// One running delegated agent.
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
188pub struct DelegationInfo {
189    /// The delegation to stop.
190    pub handle: String,
191    /// The agent, such as `codex`.
192    pub agent: String,
193    /// The SCV session that started it.
194    pub session: String,
195    /// Delegation depth: 1 for an agent SCV started directly.
196    pub depth: u32,
197    /// The agent's process ID (and process group).
198    pub pid: u32,
199    /// The SCV process that started it.
200    pub owner_pid: u32,
201    /// Live processes in its group plus tagged processes outside it.
202    pub processes: u32,
203    /// Its working directory.
204    pub cwd: String,
205    /// When it started.
206    pub started_unix_seconds: u64,
207    /// The owning SCV process is gone; the daemon will stop it.
208    pub orphaned: bool,
209    /// The conversation this run is a turn of, and which turn.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub conversation: Option<String>,
212    /// Which turn of that conversation.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub turn: Option<u32>,
215    /// A live agent (a nested SCV or an ACP agent) with no turn running:
216    /// when its last turn ended. Absent while it works, for a per-turn run,
217    /// and from older daemons.
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub idle_since_unix_seconds: Option<u64>,
220    /// A nested SCV's own background jobs that still run or wait to be
221    /// reported to it; a planned restart waits for them even between turns.
222    /// Absent when there are none, for other agents, and from older daemons.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub background_jobs: Option<u32>,
225}
226
227/// What a `daemon.control` message asks the daemon to do.
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229#[serde(tag = "action", rename_all = "snake_case")]
230pub enum DaemonCommand {
231    /// Report [`DaemonStatus`].
232    Status,
233    /// Reread channel settings and reconcile the components now.
234    Reload,
235    /// Enable or disable one channel account, optionally changing its
236    /// workspace, remote tool grant, and whose messages it answers.
237    ChannelSet {
238        /// The chat channel, such as `wechat` or `feishu`.
239        channel: String,
240        /// The account name within the channel.
241        account: String,
242        /// Whether the account should run.
243        enabled: bool,
244        /// The account's workspace directory; `None` keeps the saved one.
245        workspace: Option<String>,
246        /// Omitted keeps the saved setting.
247        #[serde(default, skip_serializing_if = "Option::is_none")]
248        remote_tools: Option<RemoteTools>,
249        /// Whose messages the account answers; omitted keeps the saved
250        /// setting.
251        #[serde(default, skip_serializing_if = "Option::is_none")]
252        senders: Option<Senders>,
253    },
254    /// Stop one channel account and remove its credentials and state.
255    ChannelLogout {
256        /// The chat channel, such as `wechat` or `feishu`.
257        channel: String,
258        /// The account name within the channel.
259        account: String,
260    },
261    /// List running delegations; `all` includes orphans awaiting cleanup.
262    Delegations {
263        /// Include orphans awaiting cleanup.
264        #[serde(default)]
265        all: bool,
266    },
267    /// Stop one delegation by handle, or every orphaned one.
268    DelegationKill {
269        /// The delegation to stop.
270        #[serde(default, skip_serializing_if = "Option::is_none")]
271        handle: Option<String>,
272        /// Stop every orphaned delegation instead.
273        #[serde(default)]
274        orphans: bool,
275    },
276    /// Restart into the release installed at the daemon's own path once the
277    /// requesting delegation has finished and its report is stored and no
278    /// owner message is being answered, or at `max_wait_seconds` anyway.
279    RestartWhenIdle {
280        /// The release the caller installed; the daemon checks it.
281        #[serde(default, skip_serializing_if = "Option::is_none")]
282        version: Option<String>,
283        /// The commit it was built from, for the announcement.
284        #[serde(default, skip_serializing_if = "Option::is_none")]
285        commit: Option<String>,
286        /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
287        #[serde(default, skip_serializing_if = "Option::is_none")]
288        parent: Option<String>,
289        /// Longest wait before restarting anyway.
290        #[serde(default, skip_serializing_if = "Option::is_none")]
291        max_wait_seconds: Option<u64>,
292    },
293    /// Ask the owner a yes/no question in chat: in the chat that started the
294    /// work `parent` names, or else the notify target. The reply's `confirm`
295    /// names the question; `confirm_status` then follows it.
296    ConfirmAsk {
297        /// The question, as the owner reads it.
298        question: String,
299        /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
300        /// is asked.
301        #[serde(default, skip_serializing_if = "Option::is_none")]
302        parent: Option<String>,
303        /// How long no answer waits before it counts as no
304        /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
305        #[serde(default, skip_serializing_if = "Option::is_none")]
306        timeout_seconds: Option<u64>,
307    },
308    /// Report where the question `id` stands. Asking also keeps it alive: a
309    /// question nobody asks about for a minute is withdrawn.
310    ConfirmStatus {
311        /// The ID `confirm_ask` returned.
312        id: String,
313    },
314}