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}