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
6use crate::workbench::{RunLogPage, SendMode, WorkbenchSnapshot, WorktreeInfo};
7
8/// Where a daemon component (a channel account) is in its lifecycle.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum ComponentState {
12    /// Turned off in configuration.
13    Disabled,
14    /// Starting up.
15    Starting,
16    /// Running and connected to its platform.
17    Connected,
18    /// Running but not connected.
19    Disconnected,
20    /// Waiting before a restart after a failure.
21    Backoff,
22    /// Shutting down.
23    Stopping,
24    /// Not running.
25    Stopped,
26    /// Stopped after an error it cannot recover from, such as expired credentials.
27    Failed,
28}
29
30/// The health of one channel account.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub struct ComponentHealth {
33    /// Component ID, `<channel>:<account>`.
34    pub id: String,
35    /// The chat channel this account belongs to, such as `wechat`.
36    #[serde(default)]
37    pub channel: String,
38    /// The account name within the channel.
39    pub account: String,
40    /// The platform's ID of the bot account, when known.
41    pub bot_id: Option<String>,
42    /// The platform's ID of the account's owner, when known.
43    pub user_id: Option<String>,
44    /// Whether the account should run.
45    pub enabled: bool,
46    /// Lifecycle state.
47    pub state: ComponentState,
48    /// When the platform last answered successfully.
49    pub last_success_unix_seconds: Option<u64>,
50    /// The latest error, if any.
51    pub error: Option<String>,
52    /// Restarts since the daemon started.
53    pub restarts: u64,
54    /// Effective remote tool authority; `owner` only when the owner ID is known.
55    #[serde(default)]
56    pub remote_tools: RemoteTools,
57    /// Who the account answers, as set; `owner` with no known owner ID
58    /// answers nobody. `None` from daemons before 0.3.0, which answer anyone.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub senders: Option<Senders>,
61    /// What a chat account carries, when it is not an ordinary chat:
62    /// `mail` for a mail chat. Absent for ordinary chats, email accounts,
63    /// and daemons before mail.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub purpose: Option<Purpose>,
66    /// An email account's counts: never addresses, subjects, or any other
67    /// mail text. Absent for chat accounts.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub mail: Option<MailCounts>,
70}
71
72/// What a chat account carries.
73#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum Purpose {
76    /// Conversations with a model (the default).
77    #[default]
78    Chat,
79    /// Only the email accounts' reports to the owner: no model ever answers
80    /// in it, it has no tools, and SCV never logs it.
81    Mail,
82}
83
84/// An email account's activity, as counts only.
85#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(default)]
87pub struct MailCounts {
88    /// Mail claimed and not yet decided.
89    pub claimed: u64,
90    /// Report items waiting to be sent to the mail chat.
91    pub queued: u64,
92    /// Mail decided today (the account's local day).
93    pub seen_today: u64,
94    /// Of those, mail a model triaged.
95    pub triaged_today: u64,
96    /// Of those, mail reported to the owner.
97    pub reported_today: u64,
98    /// Model tokens spent today.
99    pub tokens_today: u64,
100    /// The daily token budget.
101    pub token_budget: u64,
102    /// Digest messages sent to the mail chat in the last 24 hours.
103    pub messages_24h: u64,
104    /// When the mailbox was last checked, in Unix seconds.
105    pub last_check_unix_seconds: Option<u64>,
106    /// The provider: `imap`, `gmail`, or `graph`. Absent from daemons
107    /// before mail actions.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub provider: Option<String>,
110    /// The kinds of mail action the account may propose, as the provider
111    /// and the settings allow them (`draft`, `send`, `archive`,
112    /// `mark_read`, `trash`, `spam`); empty when it only reads.
113    #[serde(skip_serializing_if = "Vec::is_empty")]
114    pub actions: Vec<String>,
115    /// Mail actions waiting for the owner or for their turn to run.
116    #[serde(skip_serializing_if = "is_zero")]
117    pub actions_open: u64,
118    /// Mail actions being carried out now.
119    #[serde(skip_serializing_if = "is_zero")]
120    pub actions_executing: u64,
121    /// Mail actions SCV lost track of while carrying them out, kept for a
122    /// check of the mailbox.
123    #[serde(skip_serializing_if = "is_zero")]
124    pub actions_unknown: u64,
125    /// Mail actions carried out in the last 24 hours.
126    #[serde(skip_serializing_if = "is_zero")]
127    pub actions_done_24h: u64,
128}
129
130#[allow(
131    clippy::trivially_copy_pass_by_ref,
132    reason = "serde's skip_serializing_if passes a reference"
133)]
134fn is_zero(value: &u64) -> bool {
135    *value == 0
136}
137
138/// One mail action, as `scv mail status` lists it: IDs, kinds, states, and
139/// times only, never a code, a handle, an address, or mail text.
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct MailAction {
142    /// The email account, `email:<account>`.
143    pub account: String,
144    /// The action's ID, `a` and 32 hex digits.
145    pub id: String,
146    /// `draft`, `send`, `archive`, `mark_read`, `trash`, or `spam`.
147    pub kind: String,
148    /// `proposed`, `previewing`, `open`, `approved`, `executing`, or
149    /// `unknown`.
150    pub state: String,
151    /// When it was proposed.
152    pub created_unix_seconds: u64,
153    /// When its approval stops counting, while it waits for one.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub expires_unix_seconds: Option<u64>,
156}
157
158/// Who may use tools through a remote bridge account.
159#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
160#[serde(rename_all = "snake_case")]
161pub enum RemoteTools {
162    /// Every remote session is tool-free (the default).
163    #[default]
164    None,
165    /// The account's authenticated owner gets full, auto-approved tools.
166    Owner,
167}
168
169/// Whose messages a remote bridge account answers.
170#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172pub enum Senders {
173    /// Only the account's authenticated owner (the default); everyone else's
174    /// messages are dropped unanswered. Without a known owner ID, nobody.
175    #[default]
176    Owner,
177    /// Anyone who can reach the bot; everyone but the owner stays tool-free.
178    Anyone,
179}
180
181/// What `scv status` shows: the daemon, its components, and its delegations.
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183pub struct DaemonStatus {
184    /// The daemon's release.
185    pub version: String,
186    /// The daemon's process ID.
187    pub pid: u32,
188    /// Channel accounts.
189    pub components: Vec<ComponentHealth>,
190    /// Delegated agent runs.
191    #[serde(default)]
192    pub delegations: DelegationSummary,
193    /// A restart the daemon has scheduled, if any.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub restart: Option<RestartInfo>,
196    /// The question a `confirm_ask` or `confirm_status` request is about.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub confirm: Option<ConfirmInfo>,
199    /// The response to a project ledger command, when one was requested.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub project: Option<ProjectResponse>,
202    /// The mail actions a `mail_status` or `mail_cancel` request is about.
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    pub mail_actions: Vec<MailAction>,
205    /// What a `mail_cancel` request did, in SCV's words.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub mail_note: Option<String>,
208    /// The workbench, for a `workbench` request and after each other
209    /// `workbench_*` action.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub workbench: Option<WorkbenchSnapshot>,
212    /// Part of a run's activity log, for a `run_log` request.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub run_log: Option<RunLogPage>,
215    /// What a `workbench_*` or `worktree_remove` action did, in SCV's words.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub workbench_note: Option<String>,
218    /// The job a `workbench_start`, `workbench_send`, or `workbench_resume`
219    /// started, as `<session>/<job>`; absent when the prompt steered a
220    /// running turn instead.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub workbench_job: Option<String>,
223    /// The worktrees SCV created, for a `worktrees` request.
224    #[serde(default, skip_serializing_if = "Vec::is_empty")]
225    pub worktrees: Vec<WorktreeInfo>,
226}
227
228/// The lifecycle phase observed for a durable project.
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "snake_case")]
231pub enum ProjectPhase {
232    /// The project has been created and is being understood.
233    Discovery,
234    /// Tasks and dependencies are being prepared.
235    Planning,
236    /// One or more implementation tasks are running.
237    Implementation,
238    /// Work is being checked or has failed a run.
239    Verification,
240    /// Work is ready for staging.
241    Staging,
242    /// Work has been released to production.
243    Production,
244    /// The project is being reported or closed.
245    Reporting,
246}
247
248/// The reducer's current project status.
249#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(rename_all = "snake_case")]
251pub enum ProjectStatus {
252    /// The orchestrator can make progress.
253    Active,
254    /// No task can currently make progress.
255    Blocked,
256    /// An owner decision is required.
257    WaitingApproval,
258    /// Every task has completed successfully.
259    Completed,
260    /// A task exhausted its retry budget or a run failed permanently.
261    Failed,
262    /// The project was intentionally retired.
263    Archived,
264}
265
266/// A task's observed state.
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "snake_case")]
269pub enum ProjectTaskStatus {
270    /// Waiting for dependencies.
271    Pending,
272    /// Eligible to start.
273    Ready,
274    /// Owned by a currently running agent.
275    Running,
276    /// Waiting on an external condition.
277    Blocked,
278    /// Finished successfully.
279    Done,
280    /// Failed permanently.
281    Failed,
282    /// Its owner stopped reporting heartbeats.
283    Stale,
284}
285
286/// A delegated run's observed state.
287#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
288#[serde(rename_all = "snake_case")]
289pub enum ProjectRunStatus {
290    /// The delegated run is still executing.
291    Running,
292    /// The run finished successfully.
293    Succeeded,
294    /// The run finished with an error.
295    Failed,
296    /// The run was intentionally cancelled.
297    Cancelled,
298    /// The run stopped reporting heartbeats.
299    Stale,
300}
301
302/// A compact project record suitable for status output.
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304pub struct ProjectSummary {
305    /// Stable project identifier.
306    pub id: String,
307    /// Owner-visible project name.
308    pub name: String,
309    /// Absolute workspace directory.
310    pub workspace: String,
311    /// Current lifecycle phase.
312    pub phase: ProjectPhase,
313    /// Current reducer status.
314    pub status: ProjectStatus,
315    /// Creation time in Unix seconds.
316    pub created_unix_seconds: u64,
317    /// Last reducer update in Unix seconds.
318    pub updated_unix_seconds: u64,
319    /// Number of tasks in the project.
320    pub task_count: u64,
321    /// Number of tasks in the done state.
322    pub completed_tasks: u64,
323    /// Last task or run heartbeat, when one exists.
324    pub last_heartbeat_unix_seconds: Option<u64>,
325}
326
327/// A project task and its dependency/progress evidence.
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
329pub struct ProjectTask {
330    /// Stable task identifier.
331    pub id: String,
332    /// Owner-visible task title.
333    pub title: String,
334    /// Current task state.
335    pub status: ProjectTaskStatus,
336    /// Task identifiers that must complete first.
337    #[serde(default)]
338    pub depends_on: Vec<String>,
339    /// Number of failed attempts.
340    pub retries: u32,
341    /// Maximum failed attempts before the task is permanently failed.
342    pub max_retries: u32,
343    /// The current run, when the task is running.
344    pub run_id: Option<String>,
345    /// Latest observed progress text.
346    pub progress: Option<String>,
347    /// Last heartbeat in Unix seconds.
348    pub last_heartbeat_unix_seconds: Option<u64>,
349}
350
351/// A run tracked by the project ledger.
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
353pub struct ProjectRun {
354    /// Stable run identifier.
355    pub id: String,
356    /// Task owned by this run.
357    pub task_id: String,
358    /// Agent adapter that owns the run.
359    pub agent: String,
360    /// Current run state.
361    pub status: ProjectRunStatus,
362    /// One-based attempt number.
363    pub attempt: u32,
364    /// Start time in Unix seconds.
365    pub started_unix_seconds: u64,
366    /// Last observed update in Unix seconds.
367    pub updated_unix_seconds: u64,
368    /// Latest observed progress text.
369    pub progress: Option<String>,
370}
371
372/// One durable event, exposed for audit and recovery inspection.
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
374pub struct ProjectEvent {
375    /// Monotonic event sequence within the instance ledger.
376    pub sequence: u64,
377    /// Project that owns the event.
378    pub project_id: String,
379    /// Reducer event kind.
380    pub kind: String,
381    /// Event time in Unix seconds.
382    pub timestamp_unix_seconds: u64,
383    /// Event-specific evidence.
384    pub data: serde_json::Value,
385}
386
387/// A report generated from observed ledger state.
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389pub struct ProjectReport {
390    /// Current project summary.
391    pub project: ProjectSummary,
392    /// Current task records.
393    pub tasks: Vec<ProjectTask>,
394    /// Current run records.
395    pub runs: Vec<ProjectRun>,
396    /// Report generation time in Unix seconds.
397    pub generated_unix_seconds: u64,
398    /// Human-readable evidence notes.
399    pub evidence: Vec<String>,
400}
401
402/// Payload returned by a project command.
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
404#[serde(tag = "kind", rename_all = "snake_case")]
405pub enum ProjectResponse {
406    /// A project was created.
407    Created {
408        /// Created project summary.
409        project: ProjectSummary,
410    },
411    /// A project status was requested.
412    Status {
413        /// Current project summary.
414        project: ProjectSummary,
415    },
416    /// Project events were requested.
417    Events {
418        /// Project identifier.
419        project_id: String,
420        /// Highest sequence included in a compacted snapshot, when older
421        /// events are no longer retained in the live tail.
422        #[serde(default, skip_serializing_if = "Option::is_none")]
423        compacted_before: Option<u64>,
424        /// Matching events.
425        events: Vec<ProjectEvent>,
426    },
427    /// Project tasks were requested.
428    Tasks {
429        /// Project identifier.
430        project_id: String,
431        /// Current tasks.
432        tasks: Vec<ProjectTask>,
433    },
434    /// A report was generated.
435    Report {
436        /// Generated report.
437        report: ProjectReport,
438    },
439    /// A project changed.
440    Updated {
441        /// Current project summary.
442        project: ProjectSummary,
443    },
444    /// A task was created.
445    TaskAdded {
446        /// Current project summary.
447        project: ProjectSummary,
448        /// New task identifier.
449        task_id: String,
450    },
451    /// A run was started.
452    RunStarted {
453        /// Current project summary.
454        project: ProjectSummary,
455        /// New run identifier.
456        run_id: String,
457    },
458}
459
460/// How long a question to the owner waits for an answer unless the asker
461/// says otherwise.
462pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
463/// The longest a question to the owner may wait: the default ceiling of a
464/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
465/// how a delegated agent asks.
466pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
467
468/// A yes/no question to the owner in chat (`scv confirm`).
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
470pub struct ConfirmInfo {
471    /// The question's ID, for `confirm_status`.
472    pub id: String,
473    /// Where it stands.
474    pub state: ConfirmState,
475    /// The account whose owner was asked, as `<channel>:<account>`.
476    pub chat: String,
477    /// When no answer counts as no.
478    pub deadline_unix_seconds: u64,
479}
480
481/// Where a question to the owner stands.
482#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
483#[serde(rename_all = "snake_case")]
484pub enum ConfirmState {
485    /// Sent, or being stored for sending, and waiting for an answer.
486    Pending,
487    /// The owner answered yes.
488    Yes,
489    /// The owner answered no.
490    No,
491    /// No answer came before the deadline, which counts as no.
492    Expired,
493    /// The asker stopped asking about it before an answer came.
494    Withdrawn,
495    /// The question could not be handed to the chat, or its answer was lost.
496    Failed,
497    /// A state this client does not know, from a newer daemon.
498    #[serde(other)]
499    Unknown,
500}
501
502/// A restart into a newly installed release, waiting for owner work to end.
503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
504pub struct RestartInfo {
505    /// The release it restarts into.
506    pub to_version: String,
507    /// What it still waits for, such as the requesting delegation or an
508    /// owner's message; `None` once it restarts.
509    #[serde(default, skip_serializing_if = "Option::is_none")]
510    pub waiting_for: Option<String>,
511    /// The delegation that asked, whose report goes out first.
512    #[serde(default, skip_serializing_if = "Option::is_none")]
513    pub requester: Option<String>,
514    /// The chat the announcement goes to, as `<channel>:<account>`.
515    #[serde(default, skip_serializing_if = "Option::is_none")]
516    pub origin: Option<String>,
517    /// When it restarts even if work is still running.
518    pub deadline_unix_seconds: u64,
519}
520
521/// Delegated agent runs of the daemon's SCV instance.
522#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
523pub struct DelegationSummary {
524    /// Running delegations, whichever SCV process of the instance started them.
525    /// Live agents waiting between turns count too.
526    pub active: u64,
527    /// How many of `active` are live agents (a nested SCV or an ACP agent)
528    /// waiting between turns, apart from a nested SCV whose own background
529    /// jobs still count; `None` from a daemon that does not tell.
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub idle: Option<u64>,
532    /// Orphaned delegations the daemon has stopped since it started.
533    pub reaped: u64,
534    /// Listed delegations, for `delegations` and `delegation_kill`.
535    #[serde(default, skip_serializing_if = "Vec::is_empty")]
536    pub entries: Vec<DelegationInfo>,
537    /// Handles this request stopped.
538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
539    pub killed: Vec<String>,
540}
541
542/// One running delegated agent.
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
544pub struct DelegationInfo {
545    /// The delegation to stop.
546    pub handle: String,
547    /// The agent, such as `codex`.
548    pub agent: String,
549    /// The SCV session that started it.
550    pub session: String,
551    /// Delegation depth: 1 for an agent SCV started directly.
552    pub depth: u32,
553    /// The agent's process ID (and process group).
554    pub pid: u32,
555    /// The SCV process that started it.
556    pub owner_pid: u32,
557    /// Live processes in its group plus tagged processes outside it.
558    pub processes: u32,
559    /// Its working directory.
560    pub cwd: String,
561    /// When it started.
562    pub started_unix_seconds: u64,
563    /// The owning SCV process is gone; the daemon will stop it.
564    pub orphaned: bool,
565    /// The conversation this run is a turn of, and which turn.
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub conversation: Option<String>,
568    /// Which turn of that conversation.
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub turn: Option<u32>,
571    /// A live agent (a nested SCV or an ACP agent) with no turn running:
572    /// when its last turn ended. Absent while it works, for a per-turn run,
573    /// and from older daemons.
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub idle_since_unix_seconds: Option<u64>,
576    /// A nested SCV's own background jobs that still run or wait to be
577    /// reported to it; a planned restart waits for them even between turns.
578    /// Absent when there are none, for other agents, and from older daemons.
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub background_jobs: Option<u32>,
581}
582
583/// What a `daemon.control` message asks the daemon to do.
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
585#[serde(tag = "action", rename_all = "snake_case")]
586pub enum DaemonCommand {
587    /// Report [`DaemonStatus`].
588    Status,
589    /// Reread channel settings and reconcile the components now.
590    Reload,
591    /// Enable or disable one channel account, optionally changing its
592    /// workspace, remote tool grant, and whose messages it answers.
593    ChannelSet {
594        /// The chat channel, such as `wechat` or `feishu`.
595        channel: String,
596        /// The account name within the channel.
597        account: String,
598        /// Whether the account should run.
599        enabled: bool,
600        /// The account's workspace directory; `None` keeps the saved one.
601        workspace: Option<String>,
602        /// Omitted keeps the saved setting.
603        #[serde(default, skip_serializing_if = "Option::is_none")]
604        remote_tools: Option<RemoteTools>,
605        /// Whose messages the account answers; omitted keeps the saved
606        /// setting.
607        #[serde(default, skip_serializing_if = "Option::is_none")]
608        senders: Option<Senders>,
609        /// What a chat account carries; omitted keeps the saved setting.
610        #[serde(default, skip_serializing_if = "Option::is_none")]
611        purpose: Option<Purpose>,
612    },
613    /// Stop one channel account and remove its credentials and state.
614    ChannelLogout {
615        /// The chat channel, such as `wechat` or `feishu`.
616        channel: String,
617        /// The account name within the channel.
618        account: String,
619    },
620    /// List running delegations; `all` includes orphans awaiting cleanup.
621    Delegations {
622        /// Include orphans awaiting cleanup.
623        #[serde(default)]
624        all: bool,
625    },
626    /// Stop one delegation by handle, or every orphaned one.
627    DelegationKill {
628        /// The delegation to stop.
629        #[serde(default, skip_serializing_if = "Option::is_none")]
630        handle: Option<String>,
631        /// Stop every orphaned delegation instead.
632        #[serde(default)]
633        orphans: bool,
634    },
635    /// Restart into the release installed at the daemon's own path once the
636    /// requesting delegation has finished and its report is stored and no
637    /// owner message is being answered, or at `max_wait_seconds` anyway.
638    RestartWhenIdle {
639        /// The release the caller installed; the daemon checks it.
640        #[serde(default, skip_serializing_if = "Option::is_none")]
641        version: Option<String>,
642        /// The commit it was built from, for the announcement.
643        #[serde(default, skip_serializing_if = "Option::is_none")]
644        commit: Option<String>,
645        /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
646        #[serde(default, skip_serializing_if = "Option::is_none")]
647        parent: Option<String>,
648        /// Longest wait before restarting anyway.
649        #[serde(default, skip_serializing_if = "Option::is_none")]
650        max_wait_seconds: Option<u64>,
651    },
652    /// Ask the owner a yes/no question in chat: in the chat that started the
653    /// work `parent` names, or else the notify target. The reply's `confirm`
654    /// names the question; `confirm_status` then follows it.
655    ConfirmAsk {
656        /// The question, as the owner reads it.
657        question: String,
658        /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
659        /// is asked.
660        #[serde(default, skip_serializing_if = "Option::is_none")]
661        parent: Option<String>,
662        /// How long no answer waits before it counts as no
663        /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
664        #[serde(default, skip_serializing_if = "Option::is_none")]
665        timeout_seconds: Option<u64>,
666    },
667    /// Report where the question `id` stands. Asking also keeps it alive: a
668    /// question nobody asks about for a minute is withdrawn.
669    ConfirmStatus {
670        /// The ID `confirm_ask` returned.
671        id: String,
672    },
673    /// Explicitly create an owner-authorized durable project.
674    ProjectCreate {
675        /// Owner-visible project name.
676        name: String,
677        /// Absolute workspace directory.
678        workspace: String,
679    },
680    /// Read a project's reducer state without contacting an agent.
681    ProjectStatus {
682        /// Project name or identifier.
683        project: String,
684    },
685    /// Read the append-only event history after an optional sequence.
686    ProjectEvents {
687        /// Project name or identifier.
688        project: String,
689        /// Return events after this sequence.
690        after: Option<u64>,
691    },
692    /// Read current tasks and observed progress.
693    ProjectTasks {
694        /// Project name or identifier.
695        project: String,
696    },
697    /// Build a report from ledger evidence.
698    ProjectReport {
699        /// Project name or identifier.
700        project: String,
701    },
702    /// Add a task to an explicitly created project.
703    ProjectTaskAdd {
704        /// Project name or identifier.
705        project: String,
706        /// Owner-visible task title.
707        title: String,
708        /// Task identifiers that must be done first.
709        depends_on: Vec<String>,
710        /// Maximum failed attempts before permanent failure.
711        max_retries: u32,
712    },
713    /// Record an observed task state/progress update.
714    ProjectTaskUpdate {
715        /// Project name or identifier.
716        project: String,
717        /// Task identifier.
718        task: String,
719        /// New task state.
720        status: ProjectTaskStatus,
721        /// Optional progress text.
722        progress: Option<String>,
723    },
724    /// Record a supervised agent run starting.
725    ProjectRunStart {
726        /// Project name or identifier.
727        project: String,
728        /// Task identifier.
729        task: String,
730        /// Agent adapter name.
731        agent: String,
732    },
733    /// Record observed progress from a run heartbeat.
734    ProjectRunProgress {
735        /// Project name or identifier.
736        project: String,
737        /// Run identifier.
738        run: String,
739        /// Progress text.
740        progress: String,
741    },
742    /// Record an observed terminal run state.
743    ProjectRunFinish {
744        /// Project name or identifier.
745        project: String,
746        /// Run identifier.
747        run: String,
748        /// Terminal run state.
749        status: ProjectRunStatus,
750    },
751    /// Record a heartbeat for a project task/run.
752    ProjectHeartbeat {
753        /// Project name or identifier.
754        project: String,
755        /// Optional task identifier.
756        task: Option<String>,
757        /// Optional run identifier.
758        run: Option<String>,
759    },
760    /// List the mail actions of every running email account, or of one;
761    /// the reply's `mail_actions` holds them.
762    MailStatus {
763        /// The email account name; omitted, every account.
764        #[serde(default, skip_serializing_if = "Option::is_none")]
765        account: Option<String>,
766    },
767    /// Withdraw one mail action by its ID, or every action of an account
768    /// that has not started. It can never approve anything.
769    MailCancel {
770        /// The email account name.
771        account: String,
772        /// The action's ID; omitted with `all`.
773        #[serde(default, skip_serializing_if = "Option::is_none")]
774        id: Option<String>,
775        /// Withdraw every action that has not started.
776        #[serde(default)]
777        all: bool,
778    },
779    /// Report the workbench: sessions, background jobs, delegated runs,
780    /// approval requests waiting for the owner, and restorable sessions.
781    Workbench {
782        /// The owner is watching (a dashboard that keeps asking): for the
783        /// next 15 seconds, a background job's approval request that the
784        /// session would deny unattended waits up to two minutes for the
785        /// owner's answer instead.
786        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
787        watch: bool,
788    },
789    /// Read a delegated run's activity log after position `after`.
790    RunLog {
791        /// The run's delegation handle.
792        handle: String,
793        /// Return events after this position; 0 reads from the start.
794        #[serde(default)]
795        after: u64,
796        /// At most this many events (default and ceiling 500).
797        #[serde(default, skip_serializing_if = "Option::is_none")]
798        limit: Option<u32>,
799    },
800    /// Start a background job for the owner, in the workbench's own session
801    /// for `workspace`: no model is involved, and the job is not reported to
802    /// any model.
803    WorkbenchStart {
804        /// The absolute workspace directory the job belongs to.
805        workspace: String,
806        /// The agent to run, such as `codex`.
807        agent: String,
808        /// The prompt, as the agent reads it.
809        prompt: String,
810        /// A directory inside the workspace to run in.
811        #[serde(default, skip_serializing_if = "Option::is_none")]
812        cwd: Option<String>,
813        /// The model for the agent.
814        #[serde(default, skip_serializing_if = "Option::is_none")]
815        model: Option<String>,
816        /// The reasoning effort for the agent.
817        #[serde(default, skip_serializing_if = "Option::is_none")]
818        effort: Option<String>,
819        /// Run in a new git worktree of the repository holding `cwd`.
820        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
821        worktree: bool,
822    },
823    /// Send the owner's prompt to a delegated conversation of a live session
824    /// (attach and take over): a new turn of it, queued behind a running
825    /// turn, or added to the running turn (`mode`).
826    WorkbenchSend {
827        /// The session that holds the conversation.
828        session: String,
829        /// The conversation, such as `codex-2`.
830        conversation: String,
831        /// The owner's prompt.
832        prompt: String,
833        /// What to do while the conversation runs a turn.
834        #[serde(default)]
835        mode: SendMode,
836    },
837    /// Stop a background job of a live session.
838    WorkbenchCancel {
839        /// The session that owns it.
840        session: String,
841        /// The job, such as `job-3`.
842        job: String,
843    },
844    /// Answer an approval request a background job relayed.
845    WorkbenchApprove {
846        /// The request, as the workbench listed it.
847        id: String,
848        /// Whether the request may go ahead.
849        approved: bool,
850    },
851    /// Run an interrupted job again in the workbench's session for its
852    /// workspace, continuing its agent's own conversation when that agent
853    /// can.
854    WorkbenchResume {
855        /// The session that owned it.
856        session: String,
857        /// The job.
858        job: String,
859    },
860    /// Forget a finished or interrupted job's record.
861    WorkbenchDismiss {
862        /// The session that owned it.
863        session: String,
864        /// The job.
865        job: String,
866    },
867    /// List the git worktrees SCV created for jobs.
868    Worktrees,
869    /// Remove a worktree SCV created; one with uncommitted changes only
870    /// with `force`. Its branch is kept.
871    WorktreeRemove {
872        /// The worktree's directory, as `worktrees` lists it.
873        path: String,
874        /// Remove it even with uncommitted changes.
875        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
876        force: bool,
877    },
878}