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