frame_conv/error.rs
1//! Typed refusals and failures: the `CONV_*` slug family, envelope refusals,
2//! attach/publish/call errors, and cursor-commit outcomes.
3//!
4//! Every variant answers "what should the caller do now" (F-3a R1). Nothing
5//! here is a silent fallback; every substrate verdict is surfaced typed.
6
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::id::{ConversationSeq, CorrelationId};
12
13/// Closed refusal-slug family for the conversation contract layer (design
14/// §2.2 — golden-vectored in the crate's vector suite).
15pub mod slug {
16 /// Inbound bytes were not a well-formed conversation envelope.
17 pub const CONV_ENVELOPE_MALFORMED: &str = "CONV_ENVELOPE_MALFORMED";
18 /// The envelope named a pattern outside the closed contract set.
19 pub const CONV_PATTERN_UNKNOWN: &str = "CONV_PATTERN_UNKNOWN";
20 /// The envelope's kind is not in the named pattern's closed kind set.
21 pub const CONV_KIND_INVALID: &str = "CONV_KIND_INVALID";
22 /// The envelope's correlation field was not a typed correlation identity.
23 pub const CONV_CORRELATION_INVALID: &str = "CONV_CORRELATION_INVALID";
24 /// A typed payload failed (de)serialization — v1's schema IS the typed
25 /// Rust message and validation IS serde at both ends (F-3a R1).
26 pub const CONV_SCHEMA_INVALID: &str = "CONV_SCHEMA_INVALID";
27}
28
29/// A typed envelope refusal carrying its closed slug and the exact detail.
30#[derive(Debug, Clone, Error)]
31#[error("{slug}: {detail}")]
32pub struct EnvelopeError {
33 /// The closed `CONV_*` refusal slug.
34 pub slug: &'static str,
35 /// Exact refusal detail naming the real source.
36 pub detail: String,
37}
38
39/// Substrate refusal classes surfaced on operations that reached the server.
40///
41/// The class is frame vocabulary over the substrate's closed answer set; the
42/// exact upstream answer is always preserved in the accompanying detail.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum RefusalClass {
45 /// A declared capacity closure refused the operation.
46 Capacity,
47 /// The operation's authority (binding, generation, receipt) was refused.
48 Authority,
49 /// The participant or enrollment identity was refused.
50 Identity,
51 /// The conversation's sequence/order budget refused the operation.
52 Order,
53 /// Any other typed protocol answer, preserved verbatim in the detail.
54 Protocol,
55}
56
57/// Failure opening, joining, or resuming a conversation.
58#[derive(Debug, Error)]
59pub enum AttachError {
60 /// The bus endpoint was rejected or unreachable.
61 #[error("bus endpoint failed: {detail}")]
62 Endpoint {
63 /// Exact transport-layer detail.
64 detail: String,
65 },
66 /// The substrate refused the enrollment or re-attach with a typed answer.
67 #[error("attach refused ({class:?}): {detail}")]
68 Refused {
69 /// Refusal class in frame vocabulary.
70 class: RefusalClass,
71 /// Exact upstream answer.
72 detail: String,
73 },
74 /// The connection was lost before the attach answer arrived.
75 #[error("attach lost the connection: {detail}")]
76 ConnectionLost {
77 /// Exact transport fate.
78 detail: String,
79 },
80 /// The declared answer window elapsed without a correlated attach answer.
81 #[error("attach answer window elapsed after {waited:?}")]
82 AnswerTimeout {
83 /// Total time waited.
84 waited: Duration,
85 },
86 /// The caller-supplied resume state failed decode/validate/restore.
87 #[error("resume state rejected: {detail}")]
88 ResumeRejected {
89 /// Exact typed decode/restore refusal.
90 detail: String,
91 },
92 /// The caller-owned durable store failed a persistence barrier.
93 #[error("resume store failed: {detail}")]
94 Store {
95 /// Exact store failure.
96 detail: String,
97 },
98 /// A protocol-shape violation at the attach seam.
99 #[error("attach protocol violation: {detail}")]
100 Protocol {
101 /// Exact violation detail.
102 detail: String,
103 },
104}
105
106/// Failure publishing a typed message into the conversation.
107#[derive(Debug, Error)]
108pub enum PublishError {
109 /// The typed value failed serialization — refused BEFORE the wire
110 /// (F-3a R1; slug [`slug::CONV_SCHEMA_INVALID`]).
111 #[error("{}: {detail}", slug::CONV_SCHEMA_INVALID)]
112 SchemaInvalid {
113 /// Exact serialization refusal.
114 detail: String,
115 },
116 /// The substrate answered the admission with a typed refusal.
117 #[error("publish refused ({class:?}): {detail}")]
118 Refused {
119 /// Refusal class in frame vocabulary.
120 class: RefusalClass,
121 /// Exact upstream answer.
122 detail: String,
123 },
124 /// The connection was lost. Send outcomes are transport-write testimony
125 /// only (finding G1 — `Sent` is not receipt); the handle drives slot
126 /// recovery through the substrate's transport-loss surface (finding G2)
127 /// and every subsequent operation fails typed until [`resume`] runs.
128 ///
129 /// [`resume`]: crate::ConversationHandle::resume
130 #[error("publish lost the connection: {detail}")]
131 ConnectionLost {
132 /// Exact transport fate, including the operation and reconnect fates
133 /// the substrate recorded.
134 detail: String,
135 },
136 /// The declared answer window elapsed without the admission's correlated
137 /// terminal answer (the G2 wall, bounded loudly instead of spun on).
138 #[error("publish answer window elapsed after {waited:?}")]
139 AnswerTimeout {
140 /// Total time waited.
141 waited: Duration,
142 },
143 /// A protocol-shape violation at the publish seam.
144 #[error("publish protocol violation: {detail}")]
145 Protocol {
146 /// Exact violation detail.
147 detail: String,
148 },
149}
150
151/// Failure leaving a conversation (the F-3b R1 explicit-leave surface,
152/// wrapping the substrate's terminal `LeaveRequest` — brief §Gate fill S1
153/// leave paragraph, probe findings 2026-07-23). The typed already-left
154/// answers are OUTCOMES ([`LeaveOutcome`]), never errors.
155///
156/// [`LeaveOutcome`]: crate::outcome::LeaveOutcome
157#[derive(Debug, Error)]
158pub enum LeaveError {
159 /// The substrate answered the leave with a typed refusal (stale
160 /// authority, generation conflict, identity refusal — the exact
161 /// upstream answer is preserved in the detail).
162 #[error("leave refused ({class:?}): {detail}")]
163 Refused {
164 /// Refusal class in frame vocabulary.
165 class: RefusalClass,
166 /// Exact upstream answer.
167 detail: String,
168 },
169 /// The connection was lost before the leave's correlated answer.
170 #[error("leave lost the connection: {detail}")]
171 ConnectionLost {
172 /// Exact transport fate.
173 detail: String,
174 },
175 /// The declared answer window elapsed without the leave's correlated
176 /// terminal answer.
177 #[error("leave answer window elapsed after {waited:?}")]
178 AnswerTimeout {
179 /// Total time waited.
180 waited: Duration,
181 },
182 /// The bus endpoint was rejected or unreachable (the resumed-leave
183 /// path's connect leg).
184 #[error("bus endpoint failed: {detail}")]
185 Endpoint {
186 /// Exact transport-layer detail.
187 detail: String,
188 },
189 /// The caller-supplied resume state failed decode/validate/restore
190 /// (the resumed-leave path's restore leg).
191 #[error("resume state rejected: {detail}")]
192 ResumeRejected {
193 /// Exact typed decode/restore refusal.
194 detail: String,
195 },
196 /// The caller-owned durable store failed a persistence barrier.
197 #[error("resume store failed: {detail}")]
198 Store {
199 /// Exact store failure.
200 detail: String,
201 },
202 /// A protocol-shape violation at the leave seam.
203 #[error("leave protocol violation: {detail}")]
204 Protocol {
205 /// Exact violation detail.
206 detail: String,
207 },
208}
209
210/// Failure on the receive/wait side of a pattern call.
211#[derive(Debug, Error)]
212pub enum CallError {
213 /// The admission leg of the call failed.
214 #[error(transparent)]
215 Publish(#[from] PublishError),
216 /// The connection died while waiting (a connection fate, distinct from
217 /// every conversation outcome).
218 #[error("connection lost while waiting: {detail}")]
219 ConnectionLost {
220 /// Exact transport fate.
221 detail: String,
222 },
223 /// A correlated reply arrived but failed typed payload validation —
224 /// surfaced typed, never dropped (F-3a R1/R6).
225 #[error(
226 "{}: reply to {correlation} invalid: {detail}",
227 slug::CONV_SCHEMA_INVALID
228 )]
229 ReplySchemaInvalid {
230 /// The exchange whose reply failed validation.
231 correlation: CorrelationId,
232 /// Exact deserialization refusal.
233 detail: String,
234 },
235 /// A protocol-shape violation while waiting.
236 #[error("call protocol violation: {detail}")]
237 Protocol {
238 /// Exact violation detail.
239 detail: String,
240 },
241}
242
243/// Failure from a caller-owned resume store (frame vocabulary over the
244/// caller-owned persistence boundary — F-0c §R4: the caller owns cursor and
245/// resume-state persistence; the substrate owns barrier ordering).
246#[derive(Debug, Clone, Error)]
247#[error("resume store failure: {detail}")]
248pub struct StoreError {
249 /// Exact store failure detail.
250 pub detail: String,
251}
252
253impl StoreError {
254 /// Constructs a store failure with its exact detail.
255 #[must_use]
256 pub fn new(detail: impl Into<String>) -> Self {
257 Self {
258 detail: detail.into(),
259 }
260 }
261}
262
263/// Typed outcome of committing the caller-owned cursor (honest acking is the
264/// consumer's lever on replay volume — F-0c §R4).
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum CursorCommit {
267 /// The cursor advanced to the requested boundary.
268 Committed {
269 /// The committed cumulative boundary.
270 through: ConversationSeq,
271 },
272 /// The requested boundary did not advance the cursor.
273 NoOp {
274 /// The unchanged cursor.
275 current: ConversationSeq,
276 },
277 /// The requested boundary skipped undischarged obligations; the cursor
278 /// is unchanged. A joiner's cursor space begins at its membership —
279 /// history before the join is not ackable (F-0c late-joiner finding).
280 Gap {
281 /// The refused boundary.
282 requested: ConversationSeq,
283 /// The unchanged cursor.
284 current: ConversationSeq,
285 },
286 /// The requested boundary regressed below the committed cursor; the
287 /// cursor is unchanged.
288 Regression {
289 /// The refused boundary.
290 requested: ConversationSeq,
291 },
292}