Skip to main content

mj_controller/server/api/
types.rs

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