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