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    /// The provider: `imap`, `gmail`, or `graph`. Absent from daemons
105    /// before mail actions.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub provider: Option<String>,
108    /// The kinds of mail action the account may propose, as the provider
109    /// and the settings allow them (`draft`, `send`, `archive`,
110    /// `mark_read`, `trash`, `spam`); empty when it only reads.
111    #[serde(skip_serializing_if = "Vec::is_empty")]
112    pub actions: Vec<String>,
113    /// Mail actions waiting for the owner or for their turn to run.
114    #[serde(skip_serializing_if = "is_zero")]
115    pub actions_open: u64,
116    /// Mail actions being carried out now.
117    #[serde(skip_serializing_if = "is_zero")]
118    pub actions_executing: u64,
119    /// Mail actions SCV lost track of while carrying them out, kept for a
120    /// check of the mailbox.
121    #[serde(skip_serializing_if = "is_zero")]
122    pub actions_unknown: u64,
123    /// Mail actions carried out in the last 24 hours.
124    #[serde(skip_serializing_if = "is_zero")]
125    pub actions_done_24h: u64,
126}
127
128#[allow(
129    clippy::trivially_copy_pass_by_ref,
130    reason = "serde's skip_serializing_if passes a reference"
131)]
132fn is_zero(value: &u64) -> bool {
133    *value == 0
134}
135
136/// One mail action, as `scv mail status` lists it: IDs, kinds, states, and
137/// times only, never a code, a handle, an address, or mail text.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139pub struct MailAction {
140    /// The email account, `email:<account>`.
141    pub account: String,
142    /// The action's ID, `a` and 32 hex digits.
143    pub id: String,
144    /// `draft`, `send`, `archive`, `mark_read`, `trash`, or `spam`.
145    pub kind: String,
146    /// `proposed`, `previewing`, `open`, `approved`, `executing`, or
147    /// `unknown`.
148    pub state: String,
149    /// When it was proposed.
150    pub created_unix_seconds: u64,
151    /// When its approval stops counting, while it waits for one.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub expires_unix_seconds: Option<u64>,
154}
155
156/// Who may use tools through a remote bridge account.
157#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
158#[serde(rename_all = "snake_case")]
159pub enum RemoteTools {
160    /// Every remote session is tool-free (the default).
161    #[default]
162    None,
163    /// The account's authenticated owner gets full, auto-approved tools.
164    Owner,
165}
166
167/// Whose messages a remote bridge account answers.
168#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
169#[serde(rename_all = "snake_case")]
170pub enum Senders {
171    /// Only the account's authenticated owner (the default); everyone else's
172    /// messages are dropped unanswered. Without a known owner ID, nobody.
173    #[default]
174    Owner,
175    /// Anyone who can reach the bot; everyone but the owner stays tool-free.
176    Anyone,
177}
178
179/// What `scv status` shows: the daemon, its components, and its delegations.
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct DaemonStatus {
182    /// The daemon's release.
183    pub version: String,
184    /// The daemon's process ID.
185    pub pid: u32,
186    /// Channel accounts.
187    pub components: Vec<ComponentHealth>,
188    /// Delegated agent runs.
189    #[serde(default)]
190    pub delegations: DelegationSummary,
191    /// A restart the daemon has scheduled, if any.
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub restart: Option<RestartInfo>,
194    /// The question a `confirm_ask` or `confirm_status` request is about.
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub confirm: Option<ConfirmInfo>,
197    /// The response to a project ledger command, when one was requested.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub project: Option<ProjectResponse>,
200    /// The mail actions a `mail_status` or `mail_cancel` request is about.
201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
202    pub mail_actions: Vec<MailAction>,
203    /// What a `mail_cancel` request did, in SCV's words.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub mail_note: Option<String>,
206}
207
208/// The lifecycle phase observed for a durable project.
209#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
210#[serde(rename_all = "snake_case")]
211pub enum ProjectPhase {
212    /// The project has been created and is being understood.
213    Discovery,
214    /// Tasks and dependencies are being prepared.
215    Planning,
216    /// One or more implementation tasks are running.
217    Implementation,
218    /// Work is being checked or has failed a run.
219    Verification,
220    /// Work is ready for staging.
221    Staging,
222    /// Work has been released to production.
223    Production,
224    /// The project is being reported or closed.
225    Reporting,
226}
227
228/// The reducer's current project status.
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "snake_case")]
231pub enum ProjectStatus {
232    /// The orchestrator can make progress.
233    Active,
234    /// No task can currently make progress.
235    Blocked,
236    /// An owner decision is required.
237    WaitingApproval,
238    /// Every task has completed successfully.
239    Completed,
240    /// A task exhausted its retry budget or a run failed permanently.
241    Failed,
242    /// The project was intentionally retired.
243    Archived,
244}
245
246/// A task's observed state.
247#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "snake_case")]
249pub enum ProjectTaskStatus {
250    /// Waiting for dependencies.
251    Pending,
252    /// Eligible to start.
253    Ready,
254    /// Owned by a currently running agent.
255    Running,
256    /// Waiting on an external condition.
257    Blocked,
258    /// Finished successfully.
259    Done,
260    /// Failed permanently.
261    Failed,
262    /// Its owner stopped reporting heartbeats.
263    Stale,
264}
265
266/// A delegated run's observed state.
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "snake_case")]
269pub enum ProjectRunStatus {
270    /// The delegated run is still executing.
271    Running,
272    /// The run finished successfully.
273    Succeeded,
274    /// The run finished with an error.
275    Failed,
276    /// The run was intentionally cancelled.
277    Cancelled,
278    /// The run stopped reporting heartbeats.
279    Stale,
280}
281
282/// A compact project record suitable for status output.
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284pub struct ProjectSummary {
285    /// Stable project identifier.
286    pub id: String,
287    /// Owner-visible project name.
288    pub name: String,
289    /// Absolute workspace directory.
290    pub workspace: String,
291    /// Current lifecycle phase.
292    pub phase: ProjectPhase,
293    /// Current reducer status.
294    pub status: ProjectStatus,
295    /// Creation time in Unix seconds.
296    pub created_unix_seconds: u64,
297    /// Last reducer update in Unix seconds.
298    pub updated_unix_seconds: u64,
299    /// Number of tasks in the project.
300    pub task_count: u64,
301    /// Number of tasks in the done state.
302    pub completed_tasks: u64,
303    /// Last task or run heartbeat, when one exists.
304    pub last_heartbeat_unix_seconds: Option<u64>,
305}
306
307/// A project task and its dependency/progress evidence.
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309pub struct ProjectTask {
310    /// Stable task identifier.
311    pub id: String,
312    /// Owner-visible task title.
313    pub title: String,
314    /// Current task state.
315    pub status: ProjectTaskStatus,
316    /// Task identifiers that must complete first.
317    #[serde(default)]
318    pub depends_on: Vec<String>,
319    /// Number of failed attempts.
320    pub retries: u32,
321    /// Maximum failed attempts before the task is permanently failed.
322    pub max_retries: u32,
323    /// The current run, when the task is running.
324    pub run_id: Option<String>,
325    /// Latest observed progress text.
326    pub progress: Option<String>,
327    /// Last heartbeat in Unix seconds.
328    pub last_heartbeat_unix_seconds: Option<u64>,
329}
330
331/// A run tracked by the project ledger.
332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
333pub struct ProjectRun {
334    /// Stable run identifier.
335    pub id: String,
336    /// Task owned by this run.
337    pub task_id: String,
338    /// Agent adapter that owns the run.
339    pub agent: String,
340    /// Current run state.
341    pub status: ProjectRunStatus,
342    /// One-based attempt number.
343    pub attempt: u32,
344    /// Start time in Unix seconds.
345    pub started_unix_seconds: u64,
346    /// Last observed update in Unix seconds.
347    pub updated_unix_seconds: u64,
348    /// Latest observed progress text.
349    pub progress: Option<String>,
350}
351
352/// One durable event, exposed for audit and recovery inspection.
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
354pub struct ProjectEvent {
355    /// Monotonic event sequence within the instance ledger.
356    pub sequence: u64,
357    /// Project that owns the event.
358    pub project_id: String,
359    /// Reducer event kind.
360    pub kind: String,
361    /// Event time in Unix seconds.
362    pub timestamp_unix_seconds: u64,
363    /// Event-specific evidence.
364    pub data: serde_json::Value,
365}
366
367/// A report generated from observed ledger state.
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
369pub struct ProjectReport {
370    /// Current project summary.
371    pub project: ProjectSummary,
372    /// Current task records.
373    pub tasks: Vec<ProjectTask>,
374    /// Current run records.
375    pub runs: Vec<ProjectRun>,
376    /// Report generation time in Unix seconds.
377    pub generated_unix_seconds: u64,
378    /// Human-readable evidence notes.
379    pub evidence: Vec<String>,
380}
381
382/// Payload returned by a project command.
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(tag = "kind", rename_all = "snake_case")]
385pub enum ProjectResponse {
386    /// A project was created.
387    Created {
388        /// Created project summary.
389        project: ProjectSummary,
390    },
391    /// A project status was requested.
392    Status {
393        /// Current project summary.
394        project: ProjectSummary,
395    },
396    /// Project events were requested.
397    Events {
398        /// Project identifier.
399        project_id: String,
400        /// Highest sequence included in a compacted snapshot, when older
401        /// events are no longer retained in the live tail.
402        #[serde(default, skip_serializing_if = "Option::is_none")]
403        compacted_before: Option<u64>,
404        /// Matching events.
405        events: Vec<ProjectEvent>,
406    },
407    /// Project tasks were requested.
408    Tasks {
409        /// Project identifier.
410        project_id: String,
411        /// Current tasks.
412        tasks: Vec<ProjectTask>,
413    },
414    /// A report was generated.
415    Report {
416        /// Generated report.
417        report: ProjectReport,
418    },
419    /// A project changed.
420    Updated {
421        /// Current project summary.
422        project: ProjectSummary,
423    },
424    /// A task was created.
425    TaskAdded {
426        /// Current project summary.
427        project: ProjectSummary,
428        /// New task identifier.
429        task_id: String,
430    },
431    /// A run was started.
432    RunStarted {
433        /// Current project summary.
434        project: ProjectSummary,
435        /// New run identifier.
436        run_id: String,
437    },
438}
439
440/// How long a question to the owner waits for an answer unless the asker
441/// says otherwise.
442pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
443/// The longest a question to the owner may wait: the default ceiling of a
444/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
445/// how a delegated agent asks.
446pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
447
448/// A yes/no question to the owner in chat (`scv confirm`).
449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
450pub struct ConfirmInfo {
451    /// The question's ID, for `confirm_status`.
452    pub id: String,
453    /// Where it stands.
454    pub state: ConfirmState,
455    /// The account whose owner was asked, as `<channel>:<account>`.
456    pub chat: String,
457    /// When no answer counts as no.
458    pub deadline_unix_seconds: u64,
459}
460
461/// Where a question to the owner stands.
462#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
463#[serde(rename_all = "snake_case")]
464pub enum ConfirmState {
465    /// Sent, or being stored for sending, and waiting for an answer.
466    Pending,
467    /// The owner answered yes.
468    Yes,
469    /// The owner answered no.
470    No,
471    /// No answer came before the deadline, which counts as no.
472    Expired,
473    /// The asker stopped asking about it before an answer came.
474    Withdrawn,
475    /// The question could not be handed to the chat, or its answer was lost.
476    Failed,
477    /// A state this client does not know, from a newer daemon.
478    #[serde(other)]
479    Unknown,
480}
481
482/// A restart into a newly installed release, waiting for owner work to end.
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
484pub struct RestartInfo {
485    /// The release it restarts into.
486    pub to_version: String,
487    /// What it still waits for, such as the requesting delegation or an
488    /// owner's message; `None` once it restarts.
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub waiting_for: Option<String>,
491    /// The delegation that asked, whose report goes out first.
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub requester: Option<String>,
494    /// The chat the announcement goes to, as `<channel>:<account>`.
495    #[serde(default, skip_serializing_if = "Option::is_none")]
496    pub origin: Option<String>,
497    /// When it restarts even if work is still running.
498    pub deadline_unix_seconds: u64,
499}
500
501/// Delegated agent runs of the daemon's SCV instance.
502#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
503pub struct DelegationSummary {
504    /// Running delegations, whichever SCV process of the instance started them.
505    /// Live agents waiting between turns count too.
506    pub active: u64,
507    /// How many of `active` are live agents (a nested SCV or an ACP agent)
508    /// waiting between turns, apart from a nested SCV whose own background
509    /// jobs still count; `None` from a daemon that does not tell.
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub idle: Option<u64>,
512    /// Orphaned delegations the daemon has stopped since it started.
513    pub reaped: u64,
514    /// Listed delegations, for `delegations` and `delegation_kill`.
515    #[serde(default, skip_serializing_if = "Vec::is_empty")]
516    pub entries: Vec<DelegationInfo>,
517    /// Handles this request stopped.
518    #[serde(default, skip_serializing_if = "Vec::is_empty")]
519    pub killed: Vec<String>,
520}
521
522/// One running delegated agent.
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524pub struct DelegationInfo {
525    /// The delegation to stop.
526    pub handle: String,
527    /// The agent, such as `codex`.
528    pub agent: String,
529    /// The SCV session that started it.
530    pub session: String,
531    /// Delegation depth: 1 for an agent SCV started directly.
532    pub depth: u32,
533    /// The agent's process ID (and process group).
534    pub pid: u32,
535    /// The SCV process that started it.
536    pub owner_pid: u32,
537    /// Live processes in its group plus tagged processes outside it.
538    pub processes: u32,
539    /// Its working directory.
540    pub cwd: String,
541    /// When it started.
542    pub started_unix_seconds: u64,
543    /// The owning SCV process is gone; the daemon will stop it.
544    pub orphaned: bool,
545    /// The conversation this run is a turn of, and which turn.
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub conversation: Option<String>,
548    /// Which turn of that conversation.
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub turn: Option<u32>,
551    /// A live agent (a nested SCV or an ACP agent) with no turn running:
552    /// when its last turn ended. Absent while it works, for a per-turn run,
553    /// and from older daemons.
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    pub idle_since_unix_seconds: Option<u64>,
556    /// A nested SCV's own background jobs that still run or wait to be
557    /// reported to it; a planned restart waits for them even between turns.
558    /// Absent when there are none, for other agents, and from older daemons.
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub background_jobs: Option<u32>,
561}
562
563/// What a `daemon.control` message asks the daemon to do.
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
565#[serde(tag = "action", rename_all = "snake_case")]
566pub enum DaemonCommand {
567    /// Report [`DaemonStatus`].
568    Status,
569    /// Reread channel settings and reconcile the components now.
570    Reload,
571    /// Enable or disable one channel account, optionally changing its
572    /// workspace, remote tool grant, and whose messages it answers.
573    ChannelSet {
574        /// The chat channel, such as `wechat` or `feishu`.
575        channel: String,
576        /// The account name within the channel.
577        account: String,
578        /// Whether the account should run.
579        enabled: bool,
580        /// The account's workspace directory; `None` keeps the saved one.
581        workspace: Option<String>,
582        /// Omitted keeps the saved setting.
583        #[serde(default, skip_serializing_if = "Option::is_none")]
584        remote_tools: Option<RemoteTools>,
585        /// Whose messages the account answers; omitted keeps the saved
586        /// setting.
587        #[serde(default, skip_serializing_if = "Option::is_none")]
588        senders: Option<Senders>,
589        /// What a chat account carries; omitted keeps the saved setting.
590        #[serde(default, skip_serializing_if = "Option::is_none")]
591        purpose: Option<Purpose>,
592    },
593    /// Stop one channel account and remove its credentials and state.
594    ChannelLogout {
595        /// The chat channel, such as `wechat` or `feishu`.
596        channel: String,
597        /// The account name within the channel.
598        account: String,
599    },
600    /// List running delegations; `all` includes orphans awaiting cleanup.
601    Delegations {
602        /// Include orphans awaiting cleanup.
603        #[serde(default)]
604        all: bool,
605    },
606    /// Stop one delegation by handle, or every orphaned one.
607    DelegationKill {
608        /// The delegation to stop.
609        #[serde(default, skip_serializing_if = "Option::is_none")]
610        handle: Option<String>,
611        /// Stop every orphaned delegation instead.
612        #[serde(default)]
613        orphans: bool,
614    },
615    /// Restart into the release installed at the daemon's own path once the
616    /// requesting delegation has finished and its report is stored and no
617    /// owner message is being answered, or at `max_wait_seconds` anyway.
618    RestartWhenIdle {
619        /// The release the caller installed; the daemon checks it.
620        #[serde(default, skip_serializing_if = "Option::is_none")]
621        version: Option<String>,
622        /// The commit it was built from, for the announcement.
623        #[serde(default, skip_serializing_if = "Option::is_none")]
624        commit: Option<String>,
625        /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
626        #[serde(default, skip_serializing_if = "Option::is_none")]
627        parent: Option<String>,
628        /// Longest wait before restarting anyway.
629        #[serde(default, skip_serializing_if = "Option::is_none")]
630        max_wait_seconds: Option<u64>,
631    },
632    /// Ask the owner a yes/no question in chat: in the chat that started the
633    /// work `parent` names, or else the notify target. The reply's `confirm`
634    /// names the question; `confirm_status` then follows it.
635    ConfirmAsk {
636        /// The question, as the owner reads it.
637        question: String,
638        /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
639        /// is asked.
640        #[serde(default, skip_serializing_if = "Option::is_none")]
641        parent: Option<String>,
642        /// How long no answer waits before it counts as no
643        /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
644        #[serde(default, skip_serializing_if = "Option::is_none")]
645        timeout_seconds: Option<u64>,
646    },
647    /// Report where the question `id` stands. Asking also keeps it alive: a
648    /// question nobody asks about for a minute is withdrawn.
649    ConfirmStatus {
650        /// The ID `confirm_ask` returned.
651        id: String,
652    },
653    /// Explicitly create an owner-authorized durable project.
654    ProjectCreate {
655        /// Owner-visible project name.
656        name: String,
657        /// Absolute workspace directory.
658        workspace: String,
659    },
660    /// Read a project's reducer state without contacting an agent.
661    ProjectStatus {
662        /// Project name or identifier.
663        project: String,
664    },
665    /// Read the append-only event history after an optional sequence.
666    ProjectEvents {
667        /// Project name or identifier.
668        project: String,
669        /// Return events after this sequence.
670        after: Option<u64>,
671    },
672    /// Read current tasks and observed progress.
673    ProjectTasks {
674        /// Project name or identifier.
675        project: String,
676    },
677    /// Build a report from ledger evidence.
678    ProjectReport {
679        /// Project name or identifier.
680        project: String,
681    },
682    /// Add a task to an explicitly created project.
683    ProjectTaskAdd {
684        /// Project name or identifier.
685        project: String,
686        /// Owner-visible task title.
687        title: String,
688        /// Task identifiers that must be done first.
689        depends_on: Vec<String>,
690        /// Maximum failed attempts before permanent failure.
691        max_retries: u32,
692    },
693    /// Record an observed task state/progress update.
694    ProjectTaskUpdate {
695        /// Project name or identifier.
696        project: String,
697        /// Task identifier.
698        task: String,
699        /// New task state.
700        status: ProjectTaskStatus,
701        /// Optional progress text.
702        progress: Option<String>,
703    },
704    /// Record a supervised agent run starting.
705    ProjectRunStart {
706        /// Project name or identifier.
707        project: String,
708        /// Task identifier.
709        task: String,
710        /// Agent adapter name.
711        agent: String,
712    },
713    /// Record observed progress from a run heartbeat.
714    ProjectRunProgress {
715        /// Project name or identifier.
716        project: String,
717        /// Run identifier.
718        run: String,
719        /// Progress text.
720        progress: String,
721    },
722    /// Record an observed terminal run state.
723    ProjectRunFinish {
724        /// Project name or identifier.
725        project: String,
726        /// Run identifier.
727        run: String,
728        /// Terminal run state.
729        status: ProjectRunStatus,
730    },
731    /// Record a heartbeat for a project task/run.
732    ProjectHeartbeat {
733        /// Project name or identifier.
734        project: String,
735        /// Optional task identifier.
736        task: Option<String>,
737        /// Optional run identifier.
738        run: Option<String>,
739    },
740    /// List the mail actions of every running email account, or of one;
741    /// the reply's `mail_actions` holds them.
742    MailStatus {
743        /// The email account name; omitted, every account.
744        #[serde(default, skip_serializing_if = "Option::is_none")]
745        account: Option<String>,
746    },
747    /// Withdraw one mail action by its ID, or every action of an account
748    /// that has not started. It can never approve anything.
749    MailCancel {
750        /// The email account name.
751        account: String,
752        /// The action's ID; omitted with `all`.
753        #[serde(default, skip_serializing_if = "Option::is_none")]
754        id: Option<String>,
755        /// Withdraw every action that has not started.
756        #[serde(default)]
757        all: bool,
758    },
759}