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}