agentmux 0.8.0

Multi-agent coordination runtime with inter-agent messaging across CLI, MCP, tmux, and ACP.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
//! Transport interface contract for the relay delivery subsystem.
//!
//! The relay delivery worker dispatches every agent delivery operation through
//! the [`Transport`] trait. Concrete transports (ACP, Tmux, UI) each implement
//! the trait in their own module; the relay selects between them via the
//! [`TransportImpl`] enum, which delegates by `match` with no dynamic
//! allocation. Promoting UI (and the forward-declared Pubsub) to first-class
//! transports retires the relay's former `Acp/Tmux/Ui/Pubsub` routing fork.
//!
//! ## Write boundary: non-blocking, future-resolved
//!
//! The write methods ([`Transport::mailw`] for relay-framed envelopes,
//! [`Transport::raww`] for raw input) do not block. Each enqueues the write onto
//! the transport's own internal ordered channel and returns an [`OutcomeFuture`]
//! that resolves when the transport's internal delivery task drives that write
//! to a terminal [`SingleDeliveryOutcome`]. The transport owns that task, its
//! `spawn_blocking`, and the quiescence/coalesce waits; the relay worker
//! concurrently submits new writes and collects resolved futures without
//! blocking on any single one.
//!
//! This retires the earlier "the sync core never crosses `.await`; the worker
//! owns `spawn_blocking`" invariant: ownership of the blocking delivery moves
//! into each transport. The legacy synchronous `deliver`/`prepare_delivery`/
//! `raw_write` seam has been removed now that every relay callsite delivers
//! through the write methods.
//!
//! ## Transport <-> relay interactions
//!
//! There is no generic inbound event channel. Each transport->relay interaction
//! uses its natural primitive:
//!
//! - **Choices** (tool-call permissions, and any future operator decision) are
//!   blocking requests: the relay injects a re-entrant [`Chooser`] via
//!   [`StartupContext`], which the transport invokes inline and blocks on until
//!   the operator decides. No transport->relay back-edge: the transport holds an
//!   opaque `Arc<dyn Fn>` typed here in `transports`.
//! - **Completion** resolves through the [`OutcomeFuture`] returned by
//!   [`Transport::mailw`]/[`Transport::raww`]: the transport's internal delivery
//!   task drives each write to a terminal [`SingleDeliveryOutcome`], and the
//!   worker fans out from the resolved future.
//! - **Output for `look`** is a concurrent read via [`Transport::give_output`],
//!   which hands the relay an [`OutputView`] handle the look request path can
//!   read without borrowing the worker-owned transport.
//!
//! ## Status
//!
//! Complete (`decouple-transport-layer`): the trait, the [`TransportImpl`]
//! dispatch enum, and the shared types live here; the ACP transport lives in
//! `crate::acp` (driven by the `AcpWorkerDriver` lifecycle behind
//! [`TransportImpl::Acp`]) and the tmux transport in `crate::tmux`. The relay
//! delivery worker holds a [`TransportImpl`] per target and dispatches every
//! agent delivery through it.

use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use serde_json::Value;
use tokio::sync::oneshot;

use crate::acp::{AcpDriverServices, AcpWorkerDriver};
use crate::configuration::BundleMember;
use crate::tmux::TmuxTransport;
use crate::transports::ui::{UiTransport, UiTransportServices};
// Re-export the configuration prompt-readiness template into the transport
// contract namespace; tmux quiescence consumes it and Slice 3 wires it through
// the delivery context. It is defined once in `configuration`; re-exporting
// (rather than redefining) keeps the two in lockstep.
pub use crate::configuration::PromptReadinessTemplate;
// Pane-envelope rendering helpers. Canonical home is the transport-safe
// `crate::envelope` module (it imports no relay internals), so coder transports
// can render their own pane text from the structured delivery message.
use crate::envelope::{AddressIdentity, EnvelopeRenderInput, PromptBatchSettings, render_envelope};
// Delivery/look wire vocabulary. Canonical home is the sibling `vocabulary`
// module (below `crate::relay` in dependency order), so the transport contract
// never depends on relay. The relay re-exports these from its own contract.
use crate::transports::vocabulary::{LookSnapshotPayload, SendOutcome};

/// A pending delivery outcome handed back by the non-blocking write methods
/// ([`Transport::mailw`], [`Transport::raww`]). It resolves to the terminal
/// [`SingleDeliveryOutcome`] once the transport's internal delivery task settles
/// the write; the sender half lives inside the transport's ordered channel item.
///
/// Carries the transport-side [`SingleDeliveryOutcome`], not the relay
/// `SendResult`: the transport contract never depends on `crate::relay`, so the
/// relay worker maps the resolved outcome onto its own `SendResult` at the
/// collect site.
pub type OutcomeFuture = oneshot::Receiver<SingleDeliveryOutcome>;

/// Delivery contract implemented by each concrete transport.
///
/// The non-blocking write methods ([`mailw`](Transport::mailw),
/// [`raww`](Transport::raww)) return an [`OutcomeFuture`]; each transport owns
/// its own internal delivery task and `spawn_blocking`. They are the relay's
/// only delivery seam — the legacy synchronous `deliver`/`prepare_delivery`/
/// `raw_write` methods have been removed.
pub trait Transport {
    /// Establishes (or re-establishes, on respawn) the transport runtime for a
    /// target. On respawn the transport may publish a fresh [`OutputView`]; the
    /// worker re-calls [`give_output`] afterward to pick up the new handle.
    ///
    /// [`give_output`]: Transport::give_output
    fn startup(&mut self, context: StartupContext) -> Result<TransportStatus, TransportError>;

    /// Submits one relay-framed envelope for delivery WITHOUT blocking, returning
    /// an [`OutcomeFuture`] that resolves when the transport's internal delivery
    /// task drives this envelope to a terminal [`SingleDeliveryOutcome`]. The
    /// transport buffers the envelope on its own ordered channel, coalesces it
    /// with contiguous envelopes during its quiescence wait, and resolves the
    /// future once the combined turn settles.
    ///
    /// The relay's sole envelope-delivery seam; see the module-level "Write
    /// boundary" note. Default body is an additions-only stub; each transport
    /// overrides it with its internal delivery task.
    fn mailw(&mut self, envelope: DeliveryEnvelope) -> OutcomeFuture {
        let _ = envelope;
        unimplemented!("mailw lands with the per-transport internal delivery task")
    }

    /// Submits raw input (no envelope framing) for `raww` WITHOUT blocking,
    /// returning an [`OutcomeFuture`] that resolves when the write settles. FIFO
    /// with [`mailw`](Transport::mailw) on the transport's internal channel: a raw
    /// item flushes any buffered envelope group first, then delivers as its own
    /// write, acting as a batch barrier.
    ///
    /// The relay's sole raw-input delivery seam. Default body is an
    /// additions-only stub overridden when the internal delivery task lands.
    fn raww(&mut self, content: String, append_enter: bool) -> OutcomeFuture {
        let _ = (content, append_enter);
        unimplemented!("raww lands with the per-transport internal delivery task")
    }

    /// Reports whether the transport is ready to accept delivery.
    fn is_ready(&self) -> bool;

    /// Tears down the transport runtime, releasing its resources.
    fn shutdown(&mut self);

    /// Hands the relay a concurrently-readable [`OutputView`] handle for the
    /// `look` request path, or `None` for transports with no observable output.
    ///
    /// The look request runs concurrently with the worker that owns the
    /// transport, so it cannot call [`Transport`] methods directly; the handle
    /// is the shared seam it reads instead. The worker re-fetches the handle
    /// after every [`startup`] (ACP respawn allocates a fresh replay buffer).
    ///
    /// [`startup`]: Transport::startup
    fn give_output(&self) -> Option<Arc<dyn OutputView>>;
}

/// A concurrently-readable view of a transport's output for the `look` request
/// path, published by [`Transport::give_output`].
///
/// The relay stores the handle per-target and reads it from the look request
/// thread, which runs concurrently with the worker that owns the transport. The
/// handle owns the bounded prime-wait: [`look`] reads the transport's shared
/// readiness signal, waits up to [`LookMode::prime_timeout`] for a still-
/// initializing target to populate its first snapshot, then returns the entries
/// plus freshness metadata. The relay supplies only the timeout value (its
/// look-surface policy) and remains transport-generic.
///
/// [`look`]: OutputView::look
pub trait OutputView: Send + Sync {
    /// Captures a snapshot of the target's current output.
    fn look(&self, mode: LookMode) -> Result<LookSnapshotPayload, TransportError>;
}

/// Static dispatch over the fixed transport set.
///
/// Enum dispatch (not `Box<dyn Transport>`) is deliberate: the transport set is
/// fixed and small, sync RPITIT-free methods would still make the trait
/// non-object-safe if it ever went async, and enum dispatch carries zero heap
/// overhead per call.
#[allow(clippy::large_enum_variant)]
pub enum TransportImpl {
    /// ACP delivery transport with its worker lifecycle driver (Slice 2 / 4A-2).
    /// The driver owns the `AcpTransport` plus its bootstrap/respawn lifecycle;
    /// delivery methods delegate to the inner transport. Boxed: the driver is far
    /// larger than the other variants, and the worker moves the `TransportImpl`
    /// in and out of `spawn_blocking` each delivery, so the indirection keeps that
    /// move cheap.
    Acp(Box<AcpWorkerDriver>),
    /// Tmux pane delivery transport (implemented in Slice 3).
    Tmux(TmuxTransport),
    /// UI stream-broadcast transport. Delivers via `mailw` (a single broadcast
    /// with a bounded reconnect wait); not lookable, not raw-writable, not
    /// batchable. Promoting UI to a first-class transport retires the relay's
    /// `Acp/Tmux/Ui/Pubsub` routing fork.
    Ui(UiTransport),
    /// Forward-declared pub/sub fan-out transport. The capability row answers now
    /// (mirrors UI: not lookable/writable/streamable/batchable); delivery methods
    /// are unimplemented until the Pubsub transport lands. When it does, this
    /// becomes `Pubsub(PubsubTransport)`.
    Pubsub,
    /// Forward-declared PTY transport. The capability row answers now
    /// (look/write/stream all true); delivery methods are unimplemented until
    /// the PTY transport lands as the long-term replacement for Tmux. When it
    /// does, this becomes `Pty(PtyTransport)`.
    Pty,
}

impl TransportImpl {
    /// Builds an ACP transport with its worker lifecycle driver for one target.
    /// The relay constructs `services` closing over its own registries; the
    /// driver imports nothing from `crate::relay`.
    #[must_use]
    pub fn acp(
        target_member: BundleMember,
        runtime_directory: PathBuf,
        namespace: String,
        services: AcpDriverServices,
        batch_settings: PromptBatchSettings,
    ) -> Self {
        Self::Acp(Box::new(AcpWorkerDriver::new(
            target_member,
            runtime_directory,
            namespace,
            services,
            batch_settings,
        )))
    }

    /// Builds a tmux delivery transport carrying the prompt-batch settings (token
    /// budget and tokenizer profile) the internal delivery task consumes when
    /// combining a coalesced envelope group.
    #[must_use]
    pub fn tmux(batch_settings: PromptBatchSettings) -> Self {
        Self::Tmux(TmuxTransport::new(batch_settings))
    }

    /// Builds a UI stream-broadcast transport for one target. The relay
    /// constructs `services` closing over its own stream registry; the transport
    /// imports nothing from `crate::relay`.
    #[must_use]
    pub fn ui(services: UiTransportServices) -> Self {
        Self::Ui(UiTransport::new(services))
    }

    /// The target can be captured by `look`.
    #[must_use]
    pub fn can_be_looked(&self) -> bool {
        match self {
            Self::Acp(_) | Self::Tmux(_) | Self::Pty => true,
            Self::Ui(_) | Self::Pubsub => false,
        }
    }

    /// The target can be written by `raww`.
    #[must_use]
    pub fn can_be_written(&self) -> bool {
        match self {
            Self::Acp(_) | Self::Tmux(_) | Self::Pty => true,
            Self::Ui(_) | Self::Pubsub => false,
        }
    }

    /// The target's transport natively produces live output chunks.
    #[must_use]
    pub fn can_stream_output(&self) -> bool {
        match self {
            Self::Acp(_) | Self::Pty => true,
            Self::Tmux(_) | Self::Ui(_) | Self::Pubsub => false,
        }
    }

    /// The target's transport can surface choice requests (ACP only).
    #[must_use]
    pub fn can_give_choices(&self) -> bool {
        match self {
            Self::Acp(_) => true,
            Self::Tmux(_) | Self::Ui(_) | Self::Pubsub | Self::Pty => false,
        }
    }

    /// Establishes the transport runtime; see [`Transport::startup`].
    pub fn startup(&mut self, context: StartupContext) -> Result<TransportStatus, TransportError> {
        match self {
            Self::Acp(transport) => transport.startup(context),
            Self::Tmux(transport) => transport.startup(context),
            Self::Ui(transport) => transport.startup(context),
            Self::Pubsub => unimplemented!("Pubsub transport not yet implemented"),
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }

    /// Submits one envelope via the non-blocking write seam; see
    /// [`Transport::mailw`].
    pub fn mailw(&mut self, envelope: DeliveryEnvelope) -> OutcomeFuture {
        match self {
            Self::Acp(transport) => transport.mailw(envelope),
            Self::Tmux(transport) => transport.mailw(envelope),
            Self::Ui(transport) => transport.mailw(envelope),
            Self::Pubsub => unimplemented!("Pubsub transport not yet implemented"),
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }

    /// Submits raw input via the non-blocking write seam; see
    /// [`Transport::raww`].
    pub fn raww(&mut self, content: String, append_enter: bool) -> OutcomeFuture {
        match self {
            Self::Acp(transport) => transport.raww(content, append_enter),
            Self::Tmux(transport) => transport.raww(content, append_enter),
            Self::Ui(transport) => transport.raww(content, append_enter),
            Self::Pubsub => unimplemented!("Pubsub transport not yet implemented"),
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }

    /// Reports delivery readiness; see [`Transport::is_ready`].
    #[must_use]
    pub fn is_ready(&self) -> bool {
        match self {
            Self::Acp(transport) => transport.is_ready(),
            Self::Tmux(transport) => transport.is_ready(),
            Self::Ui(transport) => transport.is_ready(),
            // The delivery worker latches a `Pubsub` stub for a configured Pubsub
            // target (delivery is guarded and answered with a not-implemented
            // outcome), so its query/lifecycle delegates must not panic. It is
            // never ready to deliver.
            Self::Pubsub => false,
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }

    /// Tears down the transport; see [`Transport::shutdown`].
    pub fn shutdown(&mut self) {
        match self {
            Self::Acp(transport) => transport.shutdown(),
            Self::Tmux(transport) => transport.shutdown(),
            Self::Ui(transport) => transport.shutdown(),
            // The `Pubsub` stub owns no runtime, so teardown is a no-op — and it
            // MUST NOT panic: the delivery worker latches this stub for a
            // configured Pubsub target and calls `shutdown()` on it during relay
            // shutdown (`shutdown_drain`). `Pty` is not yet constructible, so it
            // can never be a latched shutdown target and stays loudly unimplemented.
            Self::Pubsub => {}
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }

    /// Publishes the look output handle; see [`Transport::give_output`].
    pub fn give_output(&self) -> Option<Arc<dyn OutputView>> {
        match self {
            Self::Acp(transport) => transport.give_output(),
            Self::Tmux(transport) => transport.give_output(),
            Self::Ui(transport) => transport.give_output(),
            // The latched `Pubsub` stub is not lookable; it publishes no handle.
            Self::Pubsub => None,
            Self::Pty => unimplemented!("PTY transport not yet implemented"),
        }
    }
}

// The concrete `TmuxTransport` lives in `crate::tmux::transport` (Slice 3),
// mirroring `AcpTransport` in `crate::acp`. `TransportImpl::Tmux` delegates to
// it; the relay re-exports it via `transports::mod`.

/// Relay-provided, synchronous resolver for operator choices (tool-call
/// permissions today; any operator decision later).
///
/// Injected once at [`startup`](Transport::startup), so the transport depends
/// only downward on `transports`, never on `crate::relay`: the transport holds
/// an opaque `Arc<dyn Fn>`; the relay constructs it closing over its choice
/// queue. The transport invokes it on its own thread and BLOCKS until the
/// operator decides, preserving "the agent turn does not progress past a pending
/// choice."
///
/// RE-ENTRANT: the relay implementation keys per-request state by a generated
/// choice id and guards the shared queue with a mutex plus a per-request
/// condvar, so concurrent invocations (multiple permission requests in one turn)
/// each manage a distinct choice safely. INVARIANT: it MUST unblock and return
/// [`ChoiceMade::Cancelled`] on relay shutdown or respawn invalidation.
pub type Chooser = Arc<dyn Fn(ChoiceToMake) -> ChoiceMade + Send + Sync>;

/// A pending choice handed to the [`Chooser`]. The per-delivery correlation
/// fields (`message_id`, `target_session`, `decider_sessions`) are sourced from
/// the [`DeliveryEnvelope`] the transport's internal delivery task is submitting
/// when it raises a choice, since the startup-time chooser cannot close over
/// them. The queue bound (`choices_pending_max`) is a per-bundle constant the
/// chooser captures at construction, so it is not carried here.
#[derive(Clone, Debug)]
pub struct ChoiceToMake {
    /// Transport-native request id used to correlate the operator's response.
    pub request_id: u64,
    /// The originating send's message id (choice event correlation).
    pub message_id: String,
    /// The target session the choice belongs to.
    pub target_session: String,
    /// Sessions authorized to decide this choice.
    pub decider_sessions: Vec<String>,
    /// Human-facing title for the choice (for example, a tool-call title).
    pub title: String,
    /// The category of choice (for example, the requested permission kind).
    pub species: String,
    /// Transport-native detail payload for the choice.
    pub details: Value,
    /// The options the operator may choose among.
    pub options: Vec<ThingToChoose>,
}

/// One selectable option within a [`ChoiceToMake`].
#[derive(Clone, Debug)]
pub struct ThingToChoose {
    pub option_id: String,
    pub name: String,
    pub species: String,
}

/// The resolution of a [`ChoiceToMake`], returned by the [`Chooser`]. Mirrors
/// the relay's choice-resolution taxonomy so the transport's internal delivery
/// task can build the same terminal outcome.
#[derive(Clone, Debug)]
pub enum ChoiceMade {
    /// An option was chosen; carries the option id and who decided.
    Chosen {
        option_id: String,
        decided_by: String,
    },
    /// The choice was cancelled; carries the cancellation taxonomy (queue full,
    /// queue unavailable, user cancelled, shutdown, respawn invalidation).
    Cancelled {
        decided_by: String,
        reason_code: String,
        reason: Option<String>,
    },
}

/// Inputs required to establish a transport runtime for one target.
#[derive(Clone)]
pub struct StartupContext {
    pub namespace: String,
    pub runtime_directory: PathBuf,
    pub target_member: BundleMember,
    /// Relay-injected, re-entrant resolver for operator choices. See [`Chooser`].
    pub choose: Chooser,
}

impl std::fmt::Debug for StartupContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("StartupContext")
            .field("namespace", &self.namespace)
            .field("runtime_directory", &self.runtime_directory)
            .field("target_member", &self.target_member)
            .field("choose", &"<Chooser>")
            .finish()
    }
}

/// One structured message to deliver to a target, plus the per-write control
/// hints the transport's internal delivery task needs.
///
/// The relay populates [`message`](Self::message) with relay-authored attribution
/// after routing and authorization; the transport renders its own representation
/// from those fields (coder transports render pane-envelope text, UI builds a
/// stream event) and never infers or mutates attribution. The remaining fields
/// are per-write transport control, not message content.
#[derive(Clone, Debug)]
pub struct DeliveryEnvelope {
    /// Correlation id echoed back in the [`SingleDeliveryOutcome`].
    pub message_id: String,
    /// Structured, transport-neutral message data. The receiving transport
    /// renders the representation it owns from these fields.
    pub message: DeliveryMessage,
    /// Whether to submit (append Enter) after writing the rendered text.
    pub append_enter: bool,
    /// Sessions authorized to decide choices raised during this envelope's
    /// delivery, threaded to [`ChoiceToMake::decider_sessions`].
    pub choice_decider_sessions: Vec<String>,
    /// Quiescence poll window for the transport's internal delivery task.
    /// The transport uses this as the quiet period before declaring the target
    /// ready to receive a flush group. Ignored by transports with no
    /// quiescence wait (ACP).
    pub quiet_window: Duration,
    /// Deadline for the quiescence wait; `None` means unbounded (bounded only
    /// by relay shutdown). Ignored by transports with no quiescence wait. The UI
    /// transport reuses this as the cap on its reconnect wait.
    pub quiescence_timeout: Option<Duration>,
}

/// One party (sender, target, or co-recipient) of a structured delivery message.
/// Transport-neutral so the UI transport does not depend on pane-envelope
/// addressing vocabulary; coder transports convert it into an
/// [`AddressIdentity`] when rendering.
#[derive(Clone, Debug)]
pub struct DeliveryParty {
    /// Canonical `session@namespace` id.
    pub session: String,
    /// Configured display name, when known.
    pub display_name: Option<String>,
}

/// Structured, transport-neutral message data sufficient for any transport to
/// render its own representation without importing `crate::relay` or parsing
/// already-rendered text. The relay authors every field; transports treat them
/// as read-only input.
#[derive(Clone, Debug)]
pub struct DeliveryMessage {
    /// The message body text.
    pub body: String,
    /// RFC 3339 creation timestamp, rendered into the `Date` header.
    pub created_at: String,
    /// The routing namespace qualifying canonical `session@namespace` ids
    /// (a session bundle, or a relay-wide namespace such as `GLOBAL`).
    pub namespace: String,
    /// Canonical sender identity.
    pub sender: DeliveryParty,
    /// Canonical target identity.
    pub target: DeliveryParty,
    /// Canonical co-recipient identities (the full target set minus this
    /// envelope's own recipient), including co-recipients in other namespaces.
    pub cc: Vec<DeliveryParty>,
    /// The sender's verified `principal_id`, when present; `None` for
    /// socket-trust senders.
    pub authenticated_identity: Option<String>,
}

impl DeliveryParty {
    fn to_address(&self) -> AddressIdentity {
        AddressIdentity {
            session_name: self.session.clone(),
            display_name: self.display_name.clone(),
        }
    }
}

impl DeliveryMessage {
    /// Renders this message as RFC 822/MIME pane-envelope text. Coder transports
    /// (Tmux/ACP) call this before writing to the harness; UI does not render
    /// pane text. `message_id` is the owning envelope's correlation id, which
    /// seeds the MIME boundary and `Message-Id` header.
    #[must_use]
    pub fn render_pane_envelope(&self, message_id: &str) -> String {
        render_envelope(&EnvelopeRenderInput {
            message_id: message_id.to_string(),
            created_at: self.created_at.clone(),
            from: self.sender.to_address(),
            to: vec![self.target.to_address()],
            cc: self.cc.iter().map(DeliveryParty::to_address).collect(),
            subject: None,
            body: self.body.clone(),
        })
    }
}

/// A quiescence-barrier failure surfaced by the tmux transport's internal
/// delivery task when it waits for its target pane to fall quiet before a flush
/// group. The task maps it to a [`SingleDeliveryOutcome`] for the buffered
/// group. Its canonical home is the transport contract, so the tmux loop that
/// raises it forms no transport<->relay back-edge.
#[derive(Debug)]
pub enum DeliveryWaitError {
    Timeout {
        timeout: Duration,
        readiness_mismatch: bool,
        mismatch_reason: Option<String>,
    },
    Failed {
        reason: String,
    },
    Shutdown,
}

/// The transport-level outcome for one delivered envelope. Structurally mirrors
/// the relay `SendResult`; kept distinct so the transport vocabulary can evolve
/// independently of the relay wire contract.
#[derive(Clone, Debug)]
pub struct SingleDeliveryOutcome {
    pub target_session: String,
    pub message_id: String,
    pub outcome: SendOutcome,
    pub reason_code: Option<String>,
    pub reason: Option<String>,
    pub details: Option<Value>,
}

/// The result of a [`Transport::startup`] call.
#[derive(Clone, Debug)]
pub struct TransportStatus {
    pub readiness: TransportReadiness,
}

/// Readiness of a transport runtime after startup.
#[derive(Clone, Debug)]
pub enum TransportReadiness {
    /// Ready to accept delivery immediately.
    Ready,
    /// Established but not yet ready (for example, awaiting first prompt).
    Pending,
    /// Could not be established; carries the failure taxonomy.
    Unavailable { code: String, reason: String },
}

/// A structured transport failure surfaced to the relay worker.
#[derive(Clone, Debug)]
pub struct TransportError {
    pub code: String,
    pub reason: String,
    pub details: Option<Value>,
}

/// Windowing parameters for an [`OutputView::look`] snapshot.
#[derive(Clone, Copy, Debug, Default)]
pub struct LookMode {
    /// Window size (tmux pane lines or ACP replay entries).
    pub lines: Option<u64>,
    /// Entries to skip from the newest end before the tail window (ACP only).
    pub offset: Option<u64>,
    /// How long the handle may wait for a still-initializing target to populate
    /// its first snapshot before returning a stale-tagged result. The relay
    /// supplies this as its look-surface policy; a zero duration means no wait.
    pub prime_timeout: Duration,
}