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