Skip to main content

running_process/broker/
session_codec.rs

1//! Phase 3 SESSION-lane codec (soldr#2365, slice 3a): bridge the proxy pump's
2//! `SessionFrame`s onto the v1 `Frame` envelope and back.
3//!
4//! The pump ([`crate::broker::session_pump`]) speaks in-memory `SessionFrame`
5//! channels; this module is the byte-boundary twin that lets those frames cross
6//! a real transport. Each `SessionFrame` rides exactly one `Frame` on the
7//! [`SESSION_PAYLOAD_PROTOCOL`] lane, encoded with the same
8//! `[u8 framing_version=1][u32 LE len][prost Frame]` wire shape every other
9//! lane uses (via [`encode_framed`] / [`try_decode_framed`]).
10//!
11//! **Direction.** The frame kind is derived from the `SessionFrame` variant:
12//! client→daemon frames (`Stdin`, `StdinEof`) are `REQUEST`, daemon→client
13//! frames (`Stdout`, `Stderr`, `Exit`) are `RESPONSE`. This is a provisional
14//! hint, not request/response correlation — a compile session is one long-lived
15//! bidirectional exchange, so `request_id` carries a session-local **sequence
16//! number** for observability, not a request/response pairing. A later slice
17//! (3b) that multiplexes many sessions over one endpoint will introduce a real
18//! session id; until then one transport carries one session and direction is
19//! implied by which half of the duplex a frame arrives on.
20//!
21//! This module deliberately does **not** touch the broker socket, `FrameClient`,
22//! or any OS handle: it is a pure `SessionFrame <-> bytes` codec so the fidelity
23//! it guarantees (byte-transparency across partial-frame boundaries) can be
24//! proven in isolation and reused by whatever transport slice 3b lands.
25
26use prost::Message;
27
28use crate::broker::protocol::{
29    encode_framed, try_decode_framed, Frame, FrameKind, FramingError, SESSION_PAYLOAD_PROTOCOL,
30};
31use crate::broker::protocol_v2::{session_frame, SessionFrame};
32
33/// The `Frame` kind a `SessionFrame` rides under, derived from its direction.
34///
35/// Inbound (client→daemon) stdin frames are `REQUEST`; outbound
36/// (daemon→client) stdout/stderr/exit frames are `RESPONSE`. See the module
37/// docs for why this is a hint rather than request/response correlation.
38fn frame_kind_for(kind: &session_frame::Kind) -> FrameKind {
39    match kind {
40        // Client → daemon: stdin stream and the opening command carriage.
41        session_frame::Kind::Stdin(_)
42        | session_frame::Kind::StdinEof(_)
43        | session_frame::Kind::Start(_) => FrameKind::Request,
44        // Daemon → client: stdout/stderr stream and the terminal exit.
45        session_frame::Kind::Stdout(_)
46        | session_frame::Kind::Stderr(_)
47        | session_frame::Kind::Exit(_) => FrameKind::Response,
48    }
49}
50
51/// Wrap one `SessionFrame` in a SESSION-lane `Frame`.
52///
53/// The envelope carries the v1 defaults (`envelope_version`,
54/// `payload_encoding = NONE`, no deadline, empty trace context) from
55/// [`Frame::request`], with `payload_protocol` pinned to
56/// [`SESSION_PAYLOAD_PROTOCOL`], `request_id` set to the session-local `seq`,
57/// and `kind` derived from the frame's direction. A `SessionFrame` with no
58/// `kind` set (an empty oneof — never produced by the pump) defaults to
59/// `REQUEST`; it still round-trips, carrying an empty payload.
60pub fn session_frame_to_frame(frame: &SessionFrame, seq: u64) -> Frame {
61    let kind = frame
62        .kind
63        .as_ref()
64        .map_or(FrameKind::Request, frame_kind_for);
65    let mut wrapped =
66        Frame::request(SESSION_PAYLOAD_PROTOCOL, frame.encode_to_vec()).with_request_id(seq);
67    wrapped.kind = kind as i32;
68    wrapped
69}
70
71/// Encode one `SessionFrame` to complete wire bytes
72/// (`[1][u32 len][prost Frame]`), ready to write to a transport.
73///
74/// # Errors
75///
76/// [`FramingError::FrameTooLarge`] when the encoded envelope exceeds the frame
77/// cap — propagated from [`encode_framed`].
78pub fn encode_session_frame(frame: &SessionFrame, seq: u64) -> Result<Vec<u8>, FramingError> {
79    encode_framed(&session_frame_to_frame(frame, seq))
80}
81
82/// One `SessionFrame` decoded from the front of a byte buffer, plus how many
83/// wire bytes it occupied.
84///
85/// `PartialEq` only, not `Eq`: prost stops auto-deriving `Eq` for `SessionFrame`
86/// once its oneof includes `SessionStart` (which carries a repeated message
87/// field), and equality here is only ever used through `assert_eq!` (which needs
88/// `PartialEq`).
89#[derive(Debug, Clone, PartialEq)]
90pub struct DecodedSessionFrame {
91    /// The decoded session frame.
92    pub frame: SessionFrame,
93    /// Total wire bytes consumed (outer header + envelope body). The caller
94    /// advances its read buffer by exactly this many bytes.
95    pub consumed: usize,
96}
97
98/// Incrementally decode one SESSION-lane `SessionFrame` from the front of `buf`.
99///
100/// Returns `Ok(None)` when `buf` does not yet hold a complete frame — the
101/// caller reads more bytes and retries. On `Ok(Some(decoded))` the caller
102/// consumes `decoded.consumed` bytes. This mirrors [`try_decode_framed`] and
103/// adds the SESSION-lane payload decode on top.
104///
105/// # Errors
106///
107/// - [`SessionCodecError::Framing`] for a malformed outer frame (bad framing
108///   version, oversize, or undecodable envelope).
109/// - [`SessionCodecError::WrongProtocol`] when the envelope is well-formed but
110///   is not on the SESSION lane — a caller multiplexing lanes must route by
111///   `payload_protocol` before calling this.
112/// - [`SessionCodecError::Decode`] when the envelope payload is not a valid
113///   `SessionFrame`.
114pub fn try_decode_session_frame(
115    buf: &[u8],
116) -> Result<Option<DecodedSessionFrame>, SessionCodecError> {
117    let Some(decoded) = try_decode_framed(buf).map_err(SessionCodecError::Framing)? else {
118        return Ok(None);
119    };
120    if decoded.frame.payload_protocol != SESSION_PAYLOAD_PROTOCOL {
121        return Err(SessionCodecError::WrongProtocol {
122            got: decoded.frame.payload_protocol,
123        });
124    }
125    let frame = SessionFrame::decode(decoded.frame.payload.as_slice())
126        .map_err(SessionCodecError::Decode)?;
127    Ok(Some(DecodedSessionFrame {
128        frame,
129        consumed: decoded.consumed,
130    }))
131}
132
133/// Errors from [`try_decode_session_frame`].
134#[derive(Debug, thiserror::Error)]
135pub enum SessionCodecError {
136    /// The outer v1 frame was malformed (see [`FramingError`]).
137    #[error(transparent)]
138    Framing(FramingError),
139    /// The frame decoded but was not on the SESSION lane. The value is the
140    /// `payload_protocol` that was seen; the expected lane is `0x5350`.
141    #[error("frame carried payload_protocol {got:#06X}, expected SESSION lane 0x5350")]
142    WrongProtocol {
143        /// The payload protocol the frame actually carried.
144        got: u32,
145    },
146    /// The envelope payload was not a valid `SessionFrame`.
147    #[error("failed to decode SessionFrame payload: {0}")]
148    Decode(prost::DecodeError),
149}
150
151#[cfg(test)]
152mod tests;