Skip to main content

mj_controller/server/api/
types.rs

1use super::*;
2
3/// Comparison and representation requested for a session's working-tree diff.
4#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct DiffOptions {
7    pub base: Option<String>,
8    #[serde(default)]
9    pub json: bool,
10}
11
12/// Observed provider-owned background work; absent when no live snapshot is available.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ApiBackgroundWork {
15    pub known: Option<bool>,
16    pub tasks: Vec<mj_core::relay::BackgroundCommand>,
17}
18
19impl From<&mj_core::relay::RelayOperationalState> for ApiBackgroundWork {
20    fn from(state: &mj_core::relay::RelayOperationalState) -> Self {
21        Self {
22            known: state.background_work_known,
23            tasks: state.background_commands.clone(),
24        }
25    }
26}
27
28/// One session as the API presents it. This is a narrower, more stable shape
29/// than the viewer's own session projection, which changes whenever the browser
30/// needs something new.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct ApiSession {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub runtime: Option<mj_core::harness_runtime::RuntimeReceipt>,
35    /// Immutable starting selection; session readiness verifies preparation.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub checkout: Option<mj_core::remote_git::ExactCheckout>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub expected_runtime_identity: Option<String>,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub background_work: Option<ApiBackgroundWork>,
42    pub id: String,
43    pub workspace_id: String,
44    pub title: String,
45    pub harness_kind: String,
46    pub profile_id: String,
47    pub target_id: String,
48    pub bundle_id: String,
49    pub state: String,
50    pub lifecycle: ViewerLifecycleCategory,
51    pub chat_phase: crate::server::ViewerChatPhase,
52    pub is_idle: bool,
53    /// What this session is doing, in more detail than `chat_phase`'s four
54    /// values allow: in particular it can say that the daemon cannot see the
55    /// worker and report what it last knew, rather than claiming idleness it
56    /// cannot prove.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub activity_state: Option<mj_core::activity::ActivityState>,
59    pub has_error: bool,
60    /// Why a launch failed, for a session in the error state. Absent
61    /// otherwise: raw runtime error text is deliberately not published for a
62    /// running session. Why a *turn* failed travels in `last_turn_diagnostic`,
63    /// which a single-session query fills.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub error: Option<String>,
66    pub created_at: String,
67    pub updated_at: String,
68    /// How the last finished prompt ended. Absent unless the caller asked for
69    /// one session by id or waited on it, because the dashboard projection the
70    /// list is built from does not carry turn identity.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub last_turn_outcome: Option<MaterializedTurnOutcome>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub last_turn_diagnostic: Option<mj_core::diagnostic::TurnDiagnostic>,
75    #[serde(default)]
76    pub config_options: Vec<crate::server::ViewerConfigOption>,
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub pending_elicitations: Vec<mj_core::elicitation::ElicitationRequest>,
79}
80
81impl From<&ViewerSession> for ApiSession {
82    fn from(session: &ViewerSession) -> Self {
83        Self {
84            background_work: None,
85            checkout: session.checkout.clone(),
86            expected_runtime_identity: session.expected_runtime_identity.clone(),
87            runtime: None,
88            id: session.id.clone(),
89            workspace_id: session.workspace_id.clone(),
90            title: session.title.clone(),
91            harness_kind: session.harness_kind.clone(),
92            profile_id: session.profile_id.clone(),
93            target_id: session.target_id.clone(),
94            bundle_id: session.bundle_id.clone(),
95            state: session.state.clone(),
96            lifecycle: session.lifecycle,
97            chat_phase: session.chat_phase,
98            is_idle: session.is_idle,
99            activity_state: session.activity_state.clone(),
100            has_error: session.has_error,
101            error: session.launch_error.clone(),
102            created_at: session.created_at.clone(),
103            updated_at: session.updated_at.clone(),
104            last_turn_outcome: None,
105            last_turn_diagnostic: None,
106            config_options: session.config_options.clone(),
107            pending_elicitations: session.pending_elicitations.clone(),
108        }
109    }
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct SessionListResponse {
114    pub sessions: Vec<ApiSession>,
115}
116
117/// The workspaces the daemon holds, newest opening first, exactly as the
118/// terminal's workspace tabs and the viewer's list see them.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct WorkspaceListResponse {
121    pub workspaces: Vec<mj_core::workspace::WorkspaceRecord>,
122}
123
124/// Name the workspace to work in. The name is the identity: it is trimmed, at
125/// most 64 characters, and unique case-insensitively, so naming one that
126/// already exists returns it rather than making a second.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(deny_unknown_fields)]
129pub struct CreateWorkspaceRequest {
130    pub name: String,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct CreateWorkspaceResponse {
135    pub workspace: mj_core::workspace::WorkspaceRecord,
136}
137
138/// Create a session and, optionally, send its first prompt. Served in M2.
139///
140/// `profile_id` and `target_id` may be omitted, and each resolves
141/// independently: a caller may name a profile and take the saved default
142/// target. An omitted identifier comes from the pair the user last saved with
143/// the `mj go` workflow, which the first setup also becomes, so a caller that
144/// has never read `config.toml` can create a session by naming neither. The
145/// controller still receives two explicit identifiers, because a session whose
146/// profile was implicit would be a session nobody can explain later.
147#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
148#[serde(deny_unknown_fields)]
149pub struct StartSessionRequest {
150    #[serde(default)]
151    pub create_managed_worktree: Option<bool>,
152    /// Diff baseline; also the starting revision for a raw managed worktree.
153    #[serde(default)]
154    pub launch_base: Option<String>,
155    /// Branch to check out in a new isolated workspace.
156    #[serde(default)]
157    pub launch_branch: Option<String>,
158    /// Exact starting selection for one bundle repository, verified before readiness.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub checkout: Option<mj_core::remote_git::ExactCheckout>,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub expected_runtime_identity: Option<String>,
163    /// None means native sub-agents, the same as `Some(false)`.
164    #[serde(default)]
165    pub mjolnir_subagents: Option<bool>,
166    #[serde(default)]
167    pub workspace_id: Option<String>,
168    /// Omitted follows the saved default. See the type's own documentation.
169    #[serde(default)]
170    pub profile_id: Option<String>,
171    /// Omitted follows the saved default. See the type's own documentation.
172    #[serde(default)]
173    pub target_id: Option<String>,
174    #[serde(default)]
175    pub bundle_id: Option<String>,
176    #[serde(default)]
177    pub project_directory: Option<PathBuf>,
178    #[serde(default)]
179    pub title: Option<String>,
180    #[serde(default)]
181    pub model: Option<String>,
182    #[serde(default)]
183    pub effort: Option<String>,
184    #[serde(default)]
185    pub prompt: Option<String>,
186}
187
188/// Resume a stopped, lost, or failed session. Every field is optional: the
189/// session's own record supplies what the caller does not name, which is what
190/// makes `POST .../resume` with no body the scriptable "continue this session"
191/// call.
192#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct ResumeSessionRequest {
195    /// Profile to resume on. Defaults to the one the session last ran.
196    #[serde(default)]
197    pub profile_id: Option<String>,
198    /// Target template to provision. Defaults to the session's own.
199    #[serde(default)]
200    pub target_id: Option<String>,
201    /// Workspace the resumed session belongs to. Defaults to its own.
202    #[serde(default)]
203    pub workspace_id: Option<String>,
204    /// Whether prompts queued when the session stopped are started or
205    /// discarded. Defaults to `start`, which is what the terminal's own resume
206    /// wizard defaults to.
207    #[serde(default)]
208    pub queue: Option<mj_core::state::ResumeQueueDisposition>,
209}
210
211/// What a resume was accepted as: the settings it will actually use, resolved
212/// from the request and the session's record.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct ResumeSessionResponse {
215    pub session_id: String,
216    pub workspace_id: String,
217    pub profile_id: String,
218    pub target_id: String,
219}
220
221/// What a suspend was accepted as. A suspend stops the session's active
222/// Mjolnir sub-agents without a checkpoint and suspends the session alone.
223#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SuspendSessionResponse {
225    #[serde(default)]
226    pub session_id: String,
227    /// Active sub-agents the suspend stops.
228    #[serde(default)]
229    pub stopped_subagents: usize,
230    /// How many of those have not handed back their report.
231    #[serde(default)]
232    pub subagents_not_handed_back: usize,
233    /// "N sub-agents have not handed back; suspending stops them", when any
234    /// have not.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub warning: Option<String>,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct StartSessionResponse {
241    pub session_id: String,
242    /// The turn the follow-up prompt was accepted as, once it has been
243    /// submitted. Creation answers before that, so it is usually absent.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub turn_id: Option<u64>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(deny_unknown_fields)]
250pub struct SubagentSourceRange {
251    pub file: PathBuf,
252    pub start: u64,
253    pub end: u64,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct SpawnSubagentRequest {
259    pub task_name: String,
260    pub instructions: String,
261    #[serde(default)]
262    pub profile_id: Option<String>,
263    #[serde(default)]
264    pub model: Option<String>,
265    #[serde(default)]
266    pub effort: Option<String>,
267    #[serde(default)]
268    pub working_directory: Option<PathBuf>,
269    #[serde(default)]
270    pub context: Option<String>,
271    #[serde(default)]
272    pub files: Vec<SubagentSourceRange>,
273}
274
275/// One profile a parent may start a sub-agent on, with what it offers and how
276/// much of its quota is left.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct SubagentCandidate {
279    pub profile_id: String,
280    pub harness: mj_core::config::HarnessKind,
281    pub choices: mj_core::worker_launch::ProfileConfig,
282    /// The lower of the profile's quota windows (the 5-hour and weekly ones
283    /// for Codex and Claude), 100 for a pay-per-use profile, and `None` when
284    /// no usable report exists.
285    pub remaining_percent: Option<u8>,
286}
287
288/// The profiles a parent may delegate to, split into those whose choices are
289/// known and those whose discovery failed, with the reason.
290#[derive(Debug, Clone, Default, PartialEq, Eq)]
291pub struct SubagentCandidates {
292    pub offered: Vec<SubagentCandidate>,
293    pub unavailable: Vec<(String, String)>,
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct SubagentView {
298    pub parent_session_id: String,
299    pub task_name: String,
300    pub session: ApiSession,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304pub struct SubagentListResponse {
305    pub subagents: Vec<SubagentView>,
306}
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct PromptRequest {
311    pub text: String,
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct PromptResponse {
316    /// The relay acceptance ordinal for this prompt, which is what `wait`
317    /// takes as `turn_id`.
318    pub turn_id: u64,
319}
320
321#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
322#[serde(deny_unknown_fields)]
323pub struct WaitRequest {
324    /// Return when the harness presents a structured input request.
325    #[serde(default)]
326    pub return_on_input: bool,
327    /// Wait for this specific prompt. Absent means "wait until the session is
328    /// idle with nothing queued", which is what a caller that lost its turn id
329    /// wants.
330    #[serde(default)]
331    pub turn_id: Option<u64>,
332    #[serde(default)]
333    pub timeout_secs: Option<u64>,
334}
335
336/// How a wait ended.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum WaitOutcome {
340    /// A structured elicitation needs an answer; only returned by opt-in waits.
341    InputRequired,
342    /// The turn completed normally.
343    Finished,
344    /// The turn failed, was rejected, or the session reported an error.
345    Error,
346    /// The turn was cancelled or interrupted.
347    Cancelled,
348    /// The model was at capacity and no retry is armed.
349    QuotaLimit,
350    /// The wait's deadline passed with the turn still running.
351    Timeout,
352    /// The session stopped or is stopping, so no turn can finish on it.
353    Stopped,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub struct WaitCapacityRetry {
358    pub attempt: u32,
359    pub retry_at_ms: i64,
360}
361
362impl From<&CapacityRetry> for WaitCapacityRetry {
363    fn from(retry: &CapacityRetry) -> Self {
364        Self {
365            attempt: retry.attempt,
366            retry_at_ms: retry.retry_at_ms,
367        }
368    }
369}
370
371/// How the daemon's live view of a session's relay is doing.
372///
373/// This reports; it never decides an outcome. A caller that gets `timeout`
374/// needs to tell "the turn is still working" from "the daemon cannot see the
375/// worker at all", and those look identical without it.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "snake_case")]
378pub enum RelayState {
379    /// The daemon is attached to the worker and following its events.
380    Connected,
381    /// Not attached, with no error recorded yet: attaching, or between tries.
382    Disconnected,
383    /// The worker could not be reached.
384    Unreachable,
385    /// The session's target is gone.
386    TargetMissing,
387    /// The event stream did not line up with what the daemon had projected.
388    ProjectionIntegrity,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct RelayHealth {
393    pub state: RelayState,
394    /// The view's own description of the problem, when it recorded one.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub detail: Option<String>,
397}
398
399impl From<&mj_client::session::ManagedSessionView> for RelayHealth {
400    fn from(view: &mj_client::session::ManagedSessionView) -> Self {
401        use mj_client::session::ViewError;
402        // A recorded error outranks `connected`: it is the specific thing
403        // standing between the caller and a finished turn.
404        match &view.error {
405            Some(error) => Self {
406                state: match error {
407                    ViewError::Unreachable(_) => RelayState::Unreachable,
408                    ViewError::TargetMissing(_) => RelayState::TargetMissing,
409                    ViewError::ProjectionIntegrity(_) => RelayState::ProjectionIntegrity,
410                },
411                detail: Some(error.detail().to_owned()),
412            },
413            None if view.connected => Self {
414                state: RelayState::Connected,
415                detail: None,
416            },
417            None => Self {
418                state: RelayState::Disconnected,
419                detail: None,
420            },
421        }
422    }
423}
424
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426pub struct WaitResponse {
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub diagnostic: Option<mj_core::diagnostic::TurnDiagnostic>,
429
430    #[serde(default, skip_serializing_if = "Vec::is_empty")]
431    pub pending_elicitations: Vec<mj_core::elicitation::ElicitationRequest>,
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub usage: Option<mj_core::usage::TokenUsage>,
434    pub outcome: WaitOutcome,
435    /// The harness's own stop reason, when the turn reached one.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub stop_reason: Option<String>,
438    /// Why the wait ended this way, when there is something to say.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub message: Option<String>,
441    /// The agent's last message of the turn, flattened to text. For a
442    /// sub-agent child it is the report the child handed back, when it did.
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub final_message: Option<String>,
445    /// For a sub-agent child: `handback` when `final_message` is the report
446    /// the child handed back, `last_message` when it is the turn's last
447    /// message. Absent for every other session.
448    #[serde(default, skip_serializing_if = "Option::is_none")]
449    pub report_source: Option<String>,
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub turn_id: Option<u64>,
452    /// One-based position of this turn in the conversation.
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub turn_number: Option<u64>,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub elapsed_ms: Option<i64>,
457    /// Legacy alias for a worker-owned server retry, retained for older clients.
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub capacity_retry: Option<WaitCapacityRetry>,
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub server_retry: Option<WaitCapacityRetry>,
462    #[serde(default)]
463    pub retry_assessment_pending: bool,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub quota_recovery: Option<mj_core::continuation::QuotaRecovery>,
466    /// The health of the daemon's live view of this session. Absent when no
467    /// live actor holds the session, because there is then no view to report
468    /// on and inventing one would be worse than saying nothing.
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub relay: Option<RelayHealth>,
471    pub session: ApiSession,
472}
473
474/// How many transcript items a page carries when the caller names no limit,
475/// and the most it may ask for. A caller that asks for more gets the ceiling
476/// rather than an error: paging is the point, and refusing a large limit would
477/// only make the caller retry with a smaller one.
478pub const DEFAULT_TRANSCRIPT_LIMIT: usize = 200;
479pub const MAX_TRANSCRIPT_LIMIT: usize = 1_000;
480
481#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
482pub struct TranscriptQuery {
483    #[serde(default)]
484    pub role: Option<mj_core::transcript::TranscriptRole>,
485    /// Resume from the highest sequence the caller has already seen.
486    #[serde(default)]
487    pub after_seq: Option<u64>,
488    #[serde(default)]
489    pub limit: Option<usize>,
490}
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493pub struct TranscriptItemView {
494    pub stable_id: String,
495    pub position: u64,
496    /// What to pass as the next `after_seq`. It is the position for everything
497    /// but an agent message, which carries the ordinal of its latest content.
498    pub seq: u64,
499    pub role: String,
500    /// The item flattened to text, which is what a reading caller wants.
501    pub text: String,
502    pub created_at_ms: i64,
503    pub last_changed_at_ms: i64,
504    /// The stored body, for a caller that needs the structure behind the text.
505    pub body: mj_core::transcript::TranscriptBody,
506}
507
508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
509pub struct TranscriptResponse {
510    #[serde(default)]
511    pub next_after_seq: u64,
512    pub session_id: String,
513    /// The newest sequence in the whole transcript. A page whose last item
514    /// reaches this is up to date.
515    pub latest_seq: u64,
516    pub execution: MaterializedExecutionState,
517    pub items: Vec<TranscriptItemView>,
518}
519
520// ---------------------------------------------------------------------------
521// Backend
522// ---------------------------------------------------------------------------
523
524/// Where a session stands turn by turn, read from the durable projection when
525/// no live actor holds the session.
526#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct TurnState {
528    pub execution: MaterializedExecutionState,
529    pub active_turn: Option<MaterializedTurn>,
530    pub last_turn_outcome: Option<MaterializedTurnOutcome>,
531}
532
533pub use crate::database::TurnSummary;
534
535/// Configuration and a first prompt to apply once a newly created session's
536/// harness is ready. Served in M2.
537#[derive(Debug, Clone, Default, PartialEq, Eq)]
538pub struct StartFollowup {
539    pub model: Option<String>,
540    pub effort: Option<String>,
541    pub prompt: Option<String>,
542    pub fast_mode: bool,
543}
544
545/// How far a created session's follow-up has got. Served in M2.
546#[derive(Debug, Clone, PartialEq, Eq)]
547pub enum StartStatus {
548    /// The session is still provisioning, or its harness is not ready.
549    Pending,
550    /// The follow-up prompt was submitted and accepted as this turn.
551    Submitted { turn_id: u64 },
552    /// The session could not be started, or the follow-up could not be applied.
553    Failed { message: String },
554}
555
556/// A page of transcript items, read from the durable projection.
557pub use crate::database::TranscriptPage;
558
559/// A branch the daemon pushed on the caller's behalf.
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub struct PushedBranch {
562    pub branch: String,
563    pub remote: String,
564}
565
566/// Which file of the session's workspace to read.
567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568pub struct FileQuery {
569    /// Path relative to the session's workspace root.
570    pub path: String,
571}
572
573/// What form the caller wants the session's work in.
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(rename_all = "snake_case")]
576pub enum ExportKind {
577    /// A unified diff, as `GET /diff` returns.
578    Patch,
579    /// A branch pushed to the repository's push remote.
580    Branch,
581    /// The git bundle of the session's committed work.
582    Bundle,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586pub struct ExportRequest {
587    pub kind: ExportKind,
588    /// The branch to push. Required when `kind` is `branch`.
589    #[serde(default, skip_serializing_if = "Option::is_none")]
590    pub branch: Option<String>,
591}
592
593/// A git bundle of the session's work.
594#[derive(Debug, Clone)]
595pub struct BundleExport {
596    pub repository: String,
597    pub bytes: Vec<u8>,
598}
599
600/// Why an export could not be produced. Served in M4.
601#[derive(Debug)]
602pub enum ExportError {
603    /// The session is in a state where this export is not possible. The caller
604    /// can act on it, so it answers 409.
605    Refused(String),
606    /// The export was attempted and failed.
607    Failed(anyhow::Error),
608}
609
610impl From<ExportError> for ApiFailure {
611    fn from(error: ExportError) -> Self {
612        match error {
613            ExportError::Refused(message) => Self::conflict(message),
614            ExportError::Failed(error) => Self::from(error),
615        }
616    }
617}
618
619// ---------------------------------------------------------------------------
620// SessionWiki
621// ---------------------------------------------------------------------------
622
623#[derive(Debug, Default, Deserialize)]
624#[serde(deny_unknown_fields)]
625pub struct WikiSearchQuery {
626    #[serde(default)]
627    pub q: Option<String>,
628    #[serde(default)]
629    pub limit: Option<usize>,
630}
631
632#[derive(Debug, Default, Deserialize)]
633#[serde(deny_unknown_fields)]
634pub struct WikiBriefQuery {
635    #[serde(default)]
636    pub max_chars: Option<usize>,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640pub struct WikiBriefResponse {
641    pub markdown: String,
642}
643
644/// The query for the matching passages of one indexed session.
645#[derive(Debug, Default, Deserialize)]
646#[serde(deny_unknown_fields)]
647pub struct WikiHitsQuery {
648    pub q: String,
649    #[serde(default)]
650    pub context_messages: Option<usize>,
651    #[serde(default)]
652    pub per_message_chars: Option<usize>,
653}
654
655/// The fields of a start request a restore needs. The archived session decides
656/// the rest: its title, and the project it ran in when the caller names none.
657#[derive(Debug, Clone, Default, Serialize, Deserialize)]
658#[serde(deny_unknown_fields)]
659pub struct WikiRestoreBody {
660    #[serde(default)]
661    pub workspace_id: Option<String>,
662    pub profile_id: String,
663    pub target_id: String,
664    #[serde(default)]
665    pub project_directory: Option<PathBuf>,
666    #[serde(default)]
667    pub model: Option<String>,
668    #[serde(default)]
669    pub effort: Option<String>,
670}
671
672// ---------------------------------------------------------------------------
673// Launch options
674// ---------------------------------------------------------------------------
675
676/// What a caller may choose when it starts a session, and which pair to use
677/// when it chooses nothing.
678///
679/// Served by `GET /api/v1/options` so a caller that has never read
680/// `config.toml` can enumerate the profiles and targets this daemon knows,
681/// learn whether each host answered its last check, and read the remembered
682/// default. It is the public projection in `server/viewer_types.rs`, narrowed
683/// to what a launch decision needs: no harness home, no SSH host or key, no
684/// container environment, no AWS detail, and no controller-side path.
685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
686#[serde(deny_unknown_fields)]
687pub struct LaunchOptions {
688    /// The projection revision these lists came from. Two reads carrying the
689    /// same revision describe the same configuration, so a caller can build a
690    /// form without mixing halves of two different worlds.
691    pub revision: u64,
692    pub profiles: Vec<LaunchProfile>,
693    pub targets: Vec<LaunchTarget>,
694    pub bundles: Vec<LaunchBundle>,
695    /// One entry per host that has published a capacity reading. The label is
696    /// how a person names the host; it is never a locator or an address.
697    #[serde(default, skip_serializing_if = "Vec::is_empty")]
698    pub hosts: Vec<LaunchHost>,
699    /// The pair the user last saved as their default, which the first setup
700    /// also becomes. Absent when nothing has ever been saved, and never an
701    /// error: a caller that cannot read a preference still has the lists.
702    #[serde(default, skip_serializing_if = "Option::is_none")]
703    pub default: Option<LaunchDefault>,
704}
705
706/// One account this daemon can run work under.
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(deny_unknown_fields)]
709pub struct LaunchProfile {
710    pub id: String,
711    /// The harness kind, such as `codex` or `claude`. Which account it is, and
712    /// where its credentials live, stay on the controller.
713    pub harness: String,
714}
715
716/// One runtime template a session can run on.
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718#[serde(deny_unknown_fields)]
719pub struct LaunchTarget {
720    pub id: String,
721    /// The runtime kind, such as `local-bare` or `ssh-podman`.
722    pub kind: String,
723    /// Whether this target needs an existing Git directory on its machine
724    /// instead of provisioning repositories into a workspace.
725    pub requires_project_directory: bool,
726    pub availability: LaunchAvailability,
727    /// What to tell a person when the host did not answer. This repository
728    /// composes the sentence: a probe's own message names hosts and commands
729    /// and stays on the controller.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub unavailable_reason: Option<String>,
732    /// How a person names the host, when a reading covers this target.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub host: Option<String>,
735}
736
737/// How much this daemon knows about a target's host.
738///
739/// A reading arrives from a background poll, so it can be absent, old, or
740/// failed. Only `Unavailable` is a statement that the target cannot be used;
741/// `Unknown` means nobody has checked yet, which is the ordinary state
742/// immediately after startup.
743#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
744#[serde(rename_all = "lowercase")]
745pub enum LaunchAvailability {
746    /// A reading covers this target and its last check succeeded.
747    Ready,
748    /// A reading covers this target but is marked stale.
749    Stale,
750    /// A reading covers this target and its last check failed.
751    Unavailable,
752    /// No reading covers this target yet.
753    Unknown,
754}
755
756/// One repository set a managed target can provision.
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(deny_unknown_fields)]
759pub struct LaunchBundle {
760    pub id: String,
761    pub primary_repository: String,
762    pub repositories: Vec<LaunchRepository>,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
766#[serde(deny_unknown_fields)]
767pub struct LaunchRepository {
768    pub id: String,
769    /// The GitHub source, when this repository has one. A repository sourced
770    /// from a local directory publishes nothing here, because that source is a
771    /// path on the controller.
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub github: Option<String>,
774    pub destination: String,
775}
776
777/// One host or fleet, as much as a caller needs to explain an unavailable
778/// target. The probe's own error text is deliberately absent.
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
780#[serde(deny_unknown_fields)]
781pub struct LaunchHost {
782    pub id: String,
783    pub label: String,
784    /// The targets this reading covers.
785    pub targets: Vec<String>,
786    pub stale: bool,
787    pub refreshing: bool,
788    pub has_error: bool,
789}
790
791/// The pair a caller may leave unnamed when it starts a session.
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(deny_unknown_fields)]
794pub struct LaunchDefault {
795    pub profile_id: String,
796    pub target_id: String,
797}