Skip to main content

brain_db_sdk/wire/
types.rs

1//! Wire payload structs for the handshake + the v1 verbs + the
2//! typed-graph ops the conformance corpus exercises.
3//!
4//! Field names, field order, and serde attributes match the server's
5//! payload structs exactly, because CBOR struct encoding is field-order
6//! sensitive and the corpus pins it byte-for-byte. The seam is the
7//! `(struct, Option<vector>)` shape: only `EncodeVectorDirectRequest`
8//! carries a trailing raw-vector section; every other payload is pure
9//! CBOR.
10
11// The types below mirror the server's wire structs one-for-one: same names,
12// same field names, same order. Their documentation is the protocol itself —
13// `conformance/protocol.json` carries every field's type and serde attributes,
14// and the corpus pins the bytes. A doc comment on each of ~2000 fields would
15// restate the field name and nothing more, and the ones that DO carry meaning
16// beyond their name (written below) would be lost in the noise.
17#![allow(missing_docs)]
18
19use serde::{Deserialize, Serialize};
20
21/// 16-byte UUID-shaped identifier (space id, request id, txn id, …).
22pub type WireUuid = [u8; 16];
23/// `SessionId` on the wire — a `u64`.
24pub type WireSessionId = u64;
25/// Packed `MemoryId` — a `u128`.
26pub type WireMemoryId = u128;
27
28// ===========================================================================
29// Shared enums.
30// ===========================================================================
31
32/// Durable memory kind. Integer discriminant on the wire.
33#[derive(
34    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
35)]
36#[repr(u8)]
37pub enum MemoryKindWire {
38    Episodic = 0,
39    Semantic = 1,
40    Consolidated = 2,
41}
42
43/// Edge kind for memory-to-memory links. Integer discriminant on the wire.
44#[derive(
45    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
46)]
47#[repr(u8)]
48pub enum EdgeKindWire {
49    Caused = 0,
50    FollowedBy = 1,
51    DerivedFrom = 2,
52    SimilarTo = 3,
53    Contradicts = 4,
54    Supports = 5,
55    References = 6,
56    PartOf = 7,
57}
58
59/// Soft tombstone vs. hard erase. Integer discriminant on the wire.
60#[derive(
61    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
62)]
63#[repr(u8)]
64pub enum ForgetMode {
65    Soft = 0,
66    Hard = 1,
67}
68
69/// Background stage a write queues. Integer discriminant on the wire.
70#[derive(
71    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
72)]
73#[repr(u8)]
74pub enum StageKind {
75    AutoEdge = 0,
76    TemporalEdge = 1,
77    Extractor = 2,
78    Hype = 3,
79}
80
81// ===========================================================================
82// Handshake.
83// ===========================================================================
84
85/// Feature flags exchanged during the handshake.
86#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
87pub struct HelloCapabilities {
88    pub streaming: bool,
89    pub compression_zstd: bool,
90    pub server_push: bool,
91}
92
93/// Authentication method. Integer discriminant on the wire.
94#[derive(
95    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
96)]
97#[repr(u8)]
98pub enum AuthMethod {
99    Token = 0,
100    Mtls = 1,
101}
102
103/// Credentials carried in an AUTH frame. Externally-tagged enum (CBOR
104/// map keyed by the variant name), matching the server.
105#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
106pub enum AuthCredentials {
107    Token(Vec<u8>),
108    Mtls(MtlsClaim),
109}
110
111/// mTLS claim accompanying an mTLS auth.
112#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
113pub struct MtlsClaim {
114    #[serde(with = "serde_bytes")]
115    pub cert_fingerprint: [u8; 32],
116    pub asserted_subject: String,
117}
118
119/// Agent permissions confirmed in AUTH_OK.
120#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
121pub struct SpacePermissions {
122    pub can_encode: bool,
123    pub can_recall: bool,
124    pub can_plan: bool,
125    pub can_reason: bool,
126    pub can_forget: bool,
127    pub can_admin: bool,
128    /// Authorizes the connection to run an op *on behalf of another identity*
129    /// via the per-request `act_as` field. Held only by a trusted service
130    /// principal (an edge / gateway); a normal agent's key never carries it,
131    /// and it does not widen what the connection's own agent may do.
132    pub can_act_as: bool,
133}
134
135/// Server-declared parameters carried in WELCOME.
136#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
137pub struct ServerFeatures {
138    pub max_payload_size: u32,
139    pub max_concurrent_streams: u32,
140    pub idle_timeout_seconds: u32,
141    pub auth_methods: Vec<AuthMethod>,
142}
143
144/// HELLO (`0x0001`) — first client frame after connect.
145#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
146pub struct HelloPayload {
147    pub client_id: String,
148    pub supported_versions: Vec<u8>,
149    pub capabilities: HelloCapabilities,
150    /// Reserved for connection resumption; `None` encodes as CBOR null.
151    #[serde(with = "serde_bytes")]
152    pub client_connection_token: Option<[u8; 32]>,
153}
154
155/// WELCOME (`0x0081`) — server reply to HELLO.
156#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
157pub struct WelcomePayload {
158    pub server_id: String,
159    pub chosen_version: u8,
160    /// 16 cryptographically-random bytes; per-connection identifier.
161    #[serde(with = "serde_bytes")]
162    pub connection_id: [u8; 16],
163    pub capabilities: HelloCapabilities,
164    pub server_features: ServerFeatures,
165}
166
167/// AUTH (`0x0002`) — client credentials. The client never claims an identity:
168/// the server resolves `(namespace, space, permissions)` from the credential
169/// alone and echoes the assignment in AUTH_OK.
170#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
171pub struct AuthPayload {
172    pub method: AuthMethod,
173    pub credentials: AuthCredentials,
174}
175
176/// AUTH_OK (`0x0082`) — server acknowledgment.
177#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
178pub struct AuthOkPayload {
179    #[serde(with = "serde_bytes")]
180    pub space_id: WireUuid,
181    pub bound_shard_id: u16,
182    pub permissions: SpacePermissions,
183    /// Owning tenant the connection resolved to (server-derived from auth).
184    /// Empty when the connection resolves to the reserved `brain` system
185    /// namespace. The client only surfaces this — it never sends a namespace.
186    pub namespace: String,
187    pub server_time_unix_nanos: u64,
188}
189
190// ===========================================================================
191// Per-request effective identity (`act_as`).
192// ===========================================================================
193
194/// Per-request effective-identity selector carried on data-plane op requests.
195/// When present, the op runs as this `(namespace, space_id)` on behalf of the
196/// authenticated connection principal; when the field is absent (the common
197/// case, omitted on the wire) the op runs as the connection's own key-bound
198/// identity.
199///
200/// Honored server-side only when the connection principal holds `can_act_as`
201/// and `namespace` lies within its granted allowlist — otherwise the op is
202/// rejected with [`ErrorCodeWire::ActAsDenied`]. This is the wire form only;
203/// the trust model is enforced by the server, not this codec.
204#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
205pub struct ActAs {
206    /// Effective namespace. Must be within the principal's allowlist.
207    pub namespace: String,
208    /// Structured, opaque effective-space selector (e.g.
209    /// `"support-bot:user123"`) — a CBOR text string on the wire. The server
210    /// derives the 16-byte storage space id from it at ingress. An empty
211    /// string selects the connection's key-bound space.
212    pub space_id: String,
213    /// Extra capability bits granted to the effective caller for this op,
214    /// on top of the standard space mask. Only `SCHEMA_UPLOAD` (`1 << 4`)
215    /// and `ADMIN` (`1 << 5`) are grantable, and only when the connection
216    /// principal holds them itself. `0` is omitted on the wire, so a
217    /// grant-less selector encodes byte-identically to the pre-grant form.
218    #[serde(default, skip_serializing_if = "is_zero_u32")]
219    pub grant: u32,
220}
221
222/// Grantable [`ActAs::grant`] bits.
223pub mod act_as_grant {
224    /// May upload / validate-and-apply schema documents.
225    pub const SCHEMA_UPLOAD: u32 = 1 << 4;
226    /// May run destructive admin ops (`SCHEMA_REPLACE`, `SCHEMA_DROP`).
227    pub const ADMIN: u32 = 1 << 5;
228}
229
230#[allow(clippy::trivially_copy_pass_by_ref)] // serde's skip_serializing_if passes &T
231fn is_zero_u32(v: &u32) -> bool {
232    *v == 0
233}
234
235// ===========================================================================
236// ENCODE / ENCODE_VECTOR_DIRECT.
237// ===========================================================================
238
239/// Edge attached to an ENCODE request.
240#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
241pub struct EdgeRequest {
242    pub target: WireMemoryId,
243    pub kind: EdgeKindWire,
244    pub weight: f32,
245}
246
247/// Write-completion mode — how long a write op blocks before it returns.
248///
249/// API convention: writes take `wait` (this enum); reads take `trace: bool`.
250/// They are never both on one op. On a write, waiting and the trace payload are
251/// one decision — the only reason to wait for async derivation is to observe it —
252/// so a single `wait` knob controls both. On a read there is no async to wait
253/// for, so `trace` is a pure observability toggle with no timing effect.
254#[derive(
255    Clone,
256    Copy,
257    Debug,
258    Default,
259    Eq,
260    PartialEq,
261    serde_repr::Serialize_repr,
262    serde_repr::Deserialize_repr,
263)]
264#[repr(u8)]
265pub enum WaitMode {
266    /// Return after the durable sync ack; async derivation runs in the
267    /// background. The fast default (omitted from the wire map).
268    #[default]
269    Ack = 0,
270    /// Block until async derivation completes, then return the full trace.
271    Derived = 1,
272}
273
274impl WaitMode {
275    /// `true` for the default [`WaitMode::Ack`] — used to omit the field from
276    /// the wire map in the common case.
277    #[must_use]
278    pub fn is_ack(&self) -> bool {
279        matches!(self, WaitMode::Ack)
280    }
281}
282
283/// ENCODE (`0x0020`).
284#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
285pub struct EncodeRequest {
286    pub text: String,
287    pub session_id: WireSessionId,
288    #[serde(with = "serde_bytes")]
289    pub request_id: WireUuid,
290    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
291    pub txn_id: Option<WireUuid>,
292    pub occurred_at_unix_nanos: Option<u64>,
293    /// Effective identity this encode runs as. `None` (omitted on the wire)
294    /// means the op runs as the connection's own key-bound identity.
295    #[serde(skip_serializing_if = "Option::is_none", default)]
296    pub act_as: Option<ActAs>,
297    /// How long the write blocks before replying. This is the single completion
298    /// knob for writes (reads use `trace` instead — see [`WaitMode`]).
299    ///
300    /// - [`WaitMode::Ack`] (the default, omitted on the wire): return as soon as
301    ///   the WAL record is durable; async derivation runs in the background and
302    ///   the response's `trace` is `None`.
303    /// - [`WaitMode::Derived`]: block until async derivation completes, then
304    ///   return a populated `trace: EncodeTrace`.
305    #[serde(default, skip_serializing_if = "WaitMode::is_ack")]
306    pub wait: WaitMode,
307    /// Opt out of content dedup and force a distinct memory. Default `false`:
308    /// Brain dedupes text ENCODE on `(space_id, session_id, BLAKE3(text))` — a
309    /// repeat of byte-identical text returns the existing MemoryId
310    /// (`was_deduplicated = true`) and writes nothing new. Set `true` when the
311    /// same text is a genuinely distinct observation that must coexist (e.g. the
312    /// same fact re-stated at a different `occurred_at`). Omitted on the wire in
313    /// the default (dedup-on) case.
314    #[serde(default, skip_serializing_if = "is_false")]
315    pub allow_duplicates: bool,
316}
317
318/// `skip_serializing_if` predicate — omit `false` from the CBOR map so the
319/// default (dedup-on) encode stays byte-minimal and wire-compatible.
320#[allow(clippy::trivially_copy_pass_by_ref)]
321fn is_false(b: &bool) -> bool {
322    !*b
323}
324
325/// ENCODE_VECTOR_DIRECT (`0x002A`). The embedding rides the trailing raw
326/// little-endian f32 section, never the CBOR map, so `vector` is skipped
327/// from CBOR and reattached at the payload codec seam.
328#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
329pub struct EncodeVectorDirectRequest {
330    pub text: String,
331    /// Carried in the trailing raw section, not the CBOR map.
332    #[serde(skip, default)]
333    pub vector: Vec<f32>,
334    #[serde(with = "serde_bytes")]
335    pub model_fingerprint: [u8; 16],
336    pub session_id: WireSessionId,
337    pub kind: MemoryKindWire,
338    pub salience_hint: f32,
339    pub edges: Vec<EdgeRequest>,
340    #[serde(with = "serde_bytes")]
341    pub request_id: WireUuid,
342    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
343    pub txn_id: Option<WireUuid>,
344    pub deduplicate: bool,
345}
346
347/// ENCODE_RESP (`0x00A0`).
348#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
349pub struct EncodeResponse {
350    pub memory_id: WireMemoryId,
351    pub was_deduplicated: bool,
352    pub salience: f32,
353    pub auto_edges_added: u32,
354    pub lsn: u64,
355    #[serde(with = "serde_bytes")]
356    pub space_id: WireUuid,
357    pub session_id: WireSessionId,
358    pub kind: MemoryKindWire,
359    pub created_at_unix_nanos: u64,
360    pub edges_out_count: u32,
361    #[serde(with = "serde_bytes")]
362    pub embedding_model_fp: [u8; 16],
363    pub pending_stages: Vec<StageKind>,
364    pub has_active_schema: bool,
365    /// Full synchronous write-analysis trace, present only when the request
366    /// set `trace = true`; `None` otherwise (and absent from the wire map).
367    /// The same async stages are also observable on SUBSCRIBE keyed by `lsn`
368    /// + `pending_stages`; this field is the one-response alternative.
369    #[serde(skip_serializing_if = "Option::is_none", default)]
370    pub trace: Option<EncodeTrace>,
371}
372
373/// Full synchronous write-analysis trace for one ENCODE, present when the
374/// request opted in with `trace = true`. The ENCODE analog of `RecallTrace`:
375/// `stages` is the per-phase timeline (synchronous phases plus async
376/// derivation stages), and `artifacts` is what the write produced.
377#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
378pub struct EncodeTrace {
379    pub stages: Vec<EncodeTraceStage>,
380    pub artifacts: EncodeTraceArtifacts,
381    pub total_latency_us: u64,
382}
383
384/// One phase in an `EncodeTrace` timeline.
385#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
386pub struct EncodeTraceStage {
387    pub name: String,
388    pub status: EncodeTraceStageStatus,
389    pub latency_us: u64,
390    pub detail: String,
391    /// The concrete data this stage produced (embed → vector, persist →
392    /// record, extractor → graph, …). Present only when the caller set
393    /// `trace = true` and the stage produced inspectable output; absent from
394    /// the wire map otherwise.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub artifact: Option<EncodeStageArtifact>,
397}
398
399/// Terminal status of an `EncodeTrace` phase.
400#[derive(
401    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
402)]
403#[repr(u8)]
404pub enum EncodeTraceStageStatus {
405    Ok = 0,
406    Skipped = 1,
407    Failed = 2,
408    Timeout = 3,
409}
410
411/// What an ENCODE produced, resolved after the async stages drained.
412#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
413pub struct EncodeTraceArtifacts {
414    pub entities: Vec<EncodeTraceEntity>,
415    pub statements: Vec<EncodeTraceStatement>,
416    pub relations: Vec<EncodeTraceRelation>,
417    pub indexes: Vec<EncodeTraceIndex>,
418    pub dedup: EncodeTraceDedup,
419}
420
421/// One entity artifact in an `EncodeTrace`.
422#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
423pub struct EncodeTraceEntity {
424    #[serde(with = "serde_bytes")]
425    pub id: [u8; 16],
426    pub name: String,
427    pub type_qname: String,
428}
429
430/// One statement artifact in an `EncodeTrace`.
431#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
432pub struct EncodeTraceStatement {
433    #[serde(with = "serde_bytes")]
434    pub id: [u8; 16],
435    pub subject_name: String,
436    pub predicate: String,
437    pub object_name: String,
438    pub confidence: f32,
439    /// When the statement's EVENT happened, in unix nanos — the reified
440    /// Time slot of an Event-kind statement. Absent from the wire map for
441    /// a statement with no event time.
442    #[serde(skip_serializing_if = "Option::is_none", default)]
443    pub event_at_unix_nanos: Option<u64>,
444}
445
446/// One relation artifact in an `EncodeTrace`.
447#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
448pub struct EncodeTraceRelation {
449    pub source_name: String,
450    pub predicate: String,
451    pub target_name: String,
452}
453
454/// One index the write landed in.
455#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
456pub struct EncodeTraceIndex {
457    pub name: String,
458    pub status: EncodeTraceStageStatus,
459}
460
461/// The dedup verdict carried on an `EncodeTrace`.
462#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
463pub struct EncodeTraceDedup {
464    pub was_deduplicated: bool,
465    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
466    pub matched_memory_id: Option<[u8; 16]>,
467}
468
469// ===========================================================================
470// Per-stage write artifacts — the concrete data each ENCODE stage produced.
471// Shared shape for the live `trace = true` per-stage `artifact` AND the
472// durable `MEMORY_INSPECT` bundle. Every field is optional and omitted from
473// the wire map when empty/absent (matches the server's `skip_serializing_if`).
474// ===========================================================================
475
476/// The concrete output one ENCODE stage produced (embed → vector, persist →
477/// record, HyPE → questions, text-index → keyword terms, extractor → graph).
478#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
479pub struct EncodeStageArtifact {
480    #[serde(default, skip_serializing_if = "Vec::is_empty")]
481    pub vector: Vec<f32>,
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub record: Option<EncodeStageRecord>,
484    #[serde(default, skip_serializing_if = "Vec::is_empty")]
485    pub hype_questions: Vec<String>,
486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
487    pub keyword_fields: Vec<EncodeStageKeywordField>,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub graph: Option<EncodeStageGraph>,
490}
491
492/// The durable metadata row a `persist` stage wrote.
493#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
494pub struct EncodeStageRecord {
495    #[serde(with = "serde_bytes")]
496    pub memory_id: [u8; 16],
497    pub kind: u8,
498    pub salience: f32,
499    pub created_at_unix_nanos: u64,
500    pub occurred_at_unix_nanos: u64,
501    pub vector_dim: u32,
502    pub text_len: u32,
503    pub lsn: u64,
504}
505
506/// One text-index field and the analyzed terms the write produced for it.
507#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
508pub struct EncodeStageKeywordField {
509    pub field: String,
510    pub terms: Vec<String>,
511}
512
513/// A node in the knowledge graph an ENCODE produced.
514#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
515pub struct EncodeGraphNode {
516    #[serde(with = "serde_bytes")]
517    pub id: [u8; 16],
518    pub name: String,
519    pub kind: String,
520    pub type_qname: String,
521}
522
523/// A directed edge in the knowledge graph an ENCODE produced.
524#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
525pub struct EncodeGraphEdge {
526    #[serde(with = "serde_bytes")]
527    pub source: [u8; 16],
528    #[serde(with = "serde_bytes")]
529    pub target: [u8; 16],
530    pub predicate: String,
531    pub kind: String,
532    pub confidence: f32,
533    /// When the EVENT this edge records happened, in unix nanos —
534    /// denormalised from the backing statement. Absent from the wire map
535    /// for an undated statement and for every non-statement edge kind.
536    #[serde(skip_serializing_if = "Option::is_none", default)]
537    pub event_at_unix_nanos: Option<u64>,
538}
539
540/// The knowledge graph an ENCODE produced — nodes + directed edges.
541#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
542pub struct EncodeStageGraph {
543    pub nodes: Vec<EncodeGraphNode>,
544    pub edges: Vec<EncodeGraphEdge>,
545}
546
547// ===========================================================================
548// MEMORY_INSPECT — one memory's durable write-artifact bundle.
549// ===========================================================================
550
551/// `MEMORY_INSPECT_REQ` — fetch the durable write-artifact bundle for one
552/// memory. Single-shot (not paginated).
553#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
554pub struct MemoryInspectRequest {
555    #[serde(with = "serde_bytes")]
556    pub memory_id: [u8; 16],
557    #[serde(skip_serializing_if = "Option::is_none", default)]
558    pub act_as: Option<ActAs>,
559}
560
561/// `MEMORY_INSPECT_RESP` — the durable per-memory artifact bundle plus text.
562/// `found = false` (with an empty `artifact`) when no memory / no bundle
563/// exists for the id under the caller's scope.
564#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
565pub struct MemoryInspectResponse {
566    pub found: bool,
567    #[serde(with = "serde_bytes")]
568    pub memory_id: [u8; 16],
569    pub text: String,
570    pub artifact: EncodeStageArtifact,
571    /// Memory↔memory edges anchored at this memory, both directions.
572    ///
573    /// Not the same as `artifact.graph`, which is the extractor's write-time
574    /// view and never contains an edge asserted later with `LINK`. Before
575    /// this field existed, `LINK`/`UNLINK` were write-only: a client could
576    /// assert an edge and no operation could read it back.
577    ///
578    /// `#[serde(default)]` so a server that does not send the field decodes
579    /// as "no edges" instead of failing the whole response.
580    #[serde(default)]
581    pub edges: Vec<MemoryEdgeWire>,
582}
583
584/// One memory↔memory edge from `MEMORY_INSPECT`.
585///
586/// `direction` is relative to the inspected memory: `Outgoing` means the
587/// inspected memory is the edge's source. `peer` alone cannot express that.
588#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
589pub struct MemoryEdgeWire {
590    pub kind: EdgeKindWire,
591    pub direction: EdgeDirectionWire,
592    /// The memory at the other end.
593    pub peer: WireMemoryId,
594    pub weight: f32,
595    pub created_at_unix_nanos: u64,
596    #[serde(default)]
597    pub annotation: Option<String>,
598}
599
600/// Which way an edge points relative to the memory being inspected.
601#[derive(
602    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
603)]
604#[repr(u8)]
605pub enum EdgeDirectionWire {
606    Outgoing = 0,
607    Incoming = 1,
608}
609
610// ===========================================================================
611// RECALL.
612// ===========================================================================
613
614/// What kind of answer a RECALL produced. Serializes as the variant name
615/// string, matching the server.
616#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
617pub enum AnswerKindWire {
618    Single,
619    Many,
620    None,
621}
622
623/// RECALL scope selector. `Space` (default) serves the caller's single
624/// `(namespace, space)` on its shard; `Namespace` spans every space in the
625/// caller's namespace via the server's cross-shard fan-out + global merge, and
626/// never crosses namespaces. Mirrors the server's `RecallScopeWire`.
627///
628/// Encoded as the variant name string (`"Space"` / `"Namespace"`), matching the
629/// server. The [`RecallRequest::scope`] field is always present on the wire,
630/// exactly as the server serializes it — a plain recall carries `"Space"`.
631#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
632pub enum RecallScopeWire {
633    /// The caller's single space (default).
634    #[default]
635    Space,
636    /// Every space in the caller's namespace (cross-shard fan-out + merge).
637    Namespace,
638}
639
640/// RECALL (`0x0021`).
641#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
642pub struct RecallRequest {
643    pub cue_text: String,
644    pub subject_name: String,
645    pub max_results: u32,
646    pub confidence_threshold: f32,
647    pub session_filter: Option<Vec<WireSessionId>>,
648    pub age_bound_unix_nanos: Option<u64>,
649    pub as_of_record_time_unix_nanos: Option<u64>,
650    pub kind_filter: Option<Vec<MemoryKindWire>>,
651    pub salience_floor: f32,
652    pub include_edges: bool,
653    pub include_graph: bool,
654    pub include_text: bool,
655    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
656    pub request_id: Option<WireUuid>,
657    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
658    pub txn_id: Option<WireUuid>,
659    /// Opt-in per-stage observability. When `true`, the final response frame
660    /// carries a populated `trace: RecallTrace`; when `false` (the default)
661    /// the field is absent and the read pays nothing.
662    #[serde(default)]
663    pub trace: bool,
664    /// Effective identity this recall runs as. `None` (omitted on the wire)
665    /// means the op runs as the connection's own key-bound identity.
666    #[serde(skip_serializing_if = "Option::is_none", default)]
667    pub act_as: Option<ActAs>,
668    /// Recall scope: the caller's single space (default) or every space in its
669    /// namespace. Always serialized, exactly as the server does (`#[serde(default)]`,
670    /// no skip) — a plain recall carries `scope: "Space"`.
671    #[serde(default)]
672    pub scope: RecallScopeWire,
673}
674
675/// One streaming RECALL_RESP frame (`0x00A1`).
676#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
677pub struct RecallResponseFrame {
678    pub answer_kind: AnswerKindWire,
679    pub memories: Vec<MemoryResult>,
680    pub is_final: bool,
681    pub cumulative_count: u32,
682    pub estimated_remaining: Option<u32>,
683    /// Per-stage read-pipeline trace, present only on the final frame and only
684    /// when the request set `trace = true`; `None` otherwise.
685    #[serde(skip_serializing_if = "Option::is_none", default)]
686    pub trace: Option<RecallTrace>,
687}
688
689/// Per-stage observability for one RECALL, surfaced on the final frame when
690/// the request opted in with `trace = true`.
691#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
692pub struct RecallTrace {
693    pub retrievers: Vec<RecallTraceRetriever>,
694    pub filter_chain: RecallTraceFilterChain,
695    pub rerank: Option<RecallTraceRerank>,
696    pub total_latency_ms: f64,
697    /// Full-detail mode only: per-fused-item score breakdown by
698    /// contributing lane. `None` when trace detail wasn't requested or
699    /// fusion produced nothing.
700    pub fusion: Option<RecallTraceFusion>,
701}
702
703/// What one retriever lane did during a traced RECALL.
704#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
705pub struct RecallTraceRetriever {
706    pub name: RetrieverNameWire,
707    pub status: RecallTraceRetrieverStatus,
708    /// Skip reason for `Skipped`, error message for `Failure`; empty otherwise.
709    pub status_detail: String,
710    pub latency_ms: f64,
711    pub candidate_count: u32,
712    /// Only populated in full-detail mode: the raw candidates this lane
713    /// contributed before fusion, in the lane's own rank order.
714    pub candidates: Vec<RecallTraceCandidate>,
715}
716
717/// One retriever-lane candidate surfaced in full-detail trace mode.
718#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
719pub struct RecallTraceCandidate {
720    /// Raw id of the surfaced item — a memory, statement, entity, or relation
721    /// id depending on `kind`. All are `u128` on the wire.
722    pub item_id: WireMemoryId,
723    /// How to interpret `item_id`.
724    pub kind: RecallCandidateKind,
725    /// A human-readable label — memory text, entity name, or a rendered
726    /// statement/relation. Full-detail mode only; truncated server-side.
727    pub text: String,
728    /// This lane's raw score for this item.
729    pub score: f32,
730}
731
732/// The kind of typed-graph item a retriever lane surfaced. The graph lane emits
733/// entities and relations (not memories), so a candidate is not always a memory
734/// — `kind` tells the client how to interpret `RecallTraceCandidate::item_id`.
735#[derive(
736    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
737)]
738#[repr(u8)]
739pub enum RecallCandidateKind {
740    Memory = 0,
741    Statement = 1,
742    Entity = 2,
743    Relation = 3,
744}
745
746/// Terminal status of a retriever lane in a RECALL trace.
747#[derive(
748    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
749)]
750#[repr(u8)]
751pub enum RecallTraceRetrieverStatus {
752    Success = 0,
753    Skipped = 1,
754    Timeout = 2,
755    Failure = 3,
756}
757
758/// Kind of a [`RecallTraceDroppedId`], disambiguating which id-space the
759/// paired `id` belongs to. Mirrors the pipeline-internal `RankedItemId`,
760/// which the supersession/as-of filter steps and the final limit truncation
761/// can drop items from — unlike the type/temporal/confidence/tombstone
762/// steps, supersession, as-of, and limit can drop `Statement` and
763/// `Relation` items, not just `Memory` ones, so a plain (untagged)
764/// `WireMemoryId` can't represent what those three steps removed.
765#[derive(
766    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
767)]
768#[repr(u8)]
769pub enum RankedItemKindWire {
770    Memory = 0,
771    Statement = 1,
772    Entity = 2,
773    Relation = 3,
774}
775
776/// One id a filter-chain step dropped, tagged with which id-space it came
777/// from. Used where a step can drop non-`Memory` items (supersession,
778/// as-of, and the final limit truncation all operate on the fused
779/// `RankedItemId` set, which includes `Statement`/`Relation`/`Entity`
780/// alongside `Memory`).
781#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
782pub struct RecallTraceDroppedId {
783    pub kind: RankedItemKindWire,
784    pub id: u128,
785}
786
787/// Filter-chain survivor counts after each step.
788#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
789pub struct RecallTraceFilterChain {
790    pub before: u32,
791    pub after_type: u32,
792    pub after_temporal: u32,
793    pub after_confidence: u32,
794    pub after_tombstone: u32,
795    pub after_supersession: u32,
796    pub after_as_of: u32,
797    pub after_limit: u32,
798    /// Only populated in full-detail mode: which memory ids were removed
799    /// by this specific filter step (empty = nothing dropped here).
800    pub dropped_by_type: Vec<WireMemoryId>,
801    /// Only populated in full-detail mode: which memory ids were removed
802    /// by this specific filter step (empty = nothing dropped here).
803    pub dropped_by_temporal: Vec<WireMemoryId>,
804    /// Only populated in full-detail mode: which memory ids were removed
805    /// by this specific filter step (empty = nothing dropped here).
806    pub dropped_by_confidence: Vec<WireMemoryId>,
807    /// Only populated in full-detail mode: which memory ids were removed
808    /// by this specific filter step (empty = nothing dropped here).
809    pub dropped_by_tombstone: Vec<WireMemoryId>,
810    /// Only populated in full-detail mode: which ids were removed by
811    /// supersession, kind-tagged since this step drops `Statement` and
812    /// `Relation` items, not just `Memory` ones (empty = nothing dropped
813    /// here).
814    pub dropped_by_supersession: Vec<RecallTraceDroppedId>,
815    /// Only populated in full-detail mode: which ids the bi-temporal
816    /// as-of filter removed, kind-tagged since this step drops `Statement`
817    /// items (empty = nothing dropped here).
818    pub dropped_by_as_of: Vec<RecallTraceDroppedId>,
819    /// Only populated in full-detail mode: which ids the final `limit`
820    /// truncation removed, kind-tagged since the truncated tail can
821    /// contain any item kind (empty = nothing dropped here).
822    pub dropped_by_limit: Vec<RecallTraceDroppedId>,
823}
824
825/// Outcome of the cross-encoder rerank stage in a RECALL trace.
826#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
827pub struct RecallTraceRerank {
828    pub applied: bool,
829    pub candidates: u32,
830    pub latency_ms: f64,
831    /// Full-detail mode only: the fused (pre-rerank) order, so the client
832    /// can show exactly what the cross-encoder moved.
833    pub before_order: Vec<WireMemoryId>,
834    /// Full-detail mode only: the post-rerank order, so the client can show
835    /// exactly what the cross-encoder moved.
836    pub after_order: Vec<WireMemoryId>,
837}
838
839/// Per-fused-item RRF score plus the per-lane component scores that
840/// contributed to it, surfaced on a `RecallTrace` when the request opted
841/// into tracing.
842#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
843pub struct RecallTraceFusion {
844    pub items: Vec<RecallTraceFusionItem>,
845}
846
847/// One fused item's RRF score plus the per-lane component scores that
848/// contributed to it.
849#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
850pub struct RecallTraceFusionItem {
851    pub memory_id: WireMemoryId,
852    pub rrf_score: f32,
853    pub lane_scores: Vec<(RetrieverNameWire, f32)>,
854}
855
856/// A single recalled memory.
857#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
858pub struct MemoryResult {
859    pub memory_id: WireMemoryId,
860    pub text: String,
861    pub similarity_score: f32,
862    pub confidence: f32,
863    pub salience: f32,
864    pub kind: MemoryKindWire,
865    #[serde(with = "serde_bytes")]
866    pub space_id: WireUuid,
867    pub session_id: WireSessionId,
868    pub created_at_unix_nanos: u64,
869    pub last_accessed_at_unix_nanos: u64,
870    pub edges: Option<Vec<EdgeView>>,
871    pub contributing_retrievers: Vec<RetrieverNameWire>,
872    pub fused_score: f32,
873    pub rerank_score: Option<f32>,
874    pub salience_initial: f32,
875    pub access_count: u32,
876    pub lsn: u64,
877    pub flags: u32,
878    pub consolidated_at_unix_nanos: Option<u64>,
879    pub occurred_at_unix_nanos: Option<u64>,
880    pub edges_out_count: u32,
881    pub edges_in_count: u32,
882    pub graph: Option<GraphEnrichment>,
883}
884
885/// Retriever family that surfaced a memory. Integer discriminant.
886#[derive(
887    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
888)]
889#[repr(u8)]
890pub enum RetrieverNameWire {
891    Semantic = 0,
892    Lexical = 1,
893    Graph = 2,
894}
895
896/// Outgoing edge view on a recalled memory.
897#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
898pub struct EdgeView {
899    pub target: WireMemoryId,
900    pub kind: EdgeKindWire,
901    pub weight: f32,
902}
903
904/// Per-hit typed-graph enrichment.
905#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
906pub struct GraphEnrichment {
907    pub entities: Vec<EnrichedEntity>,
908    pub statements: Vec<EnrichedStatement>,
909    pub relations: Vec<EnrichedRelation>,
910}
911
912/// A typed entity attached to a recall hit by [`GraphEnrichment`]: the resolved
913/// entity id, its display name, and its schema type qname.
914#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
915pub struct EnrichedEntity {
916    #[serde(with = "serde_bytes")]
917    pub id: [u8; 16],
918    pub name: String,
919    pub type_qname: String,
920}
921
922/// A typed statement attached to a recall hit by [`GraphEnrichment`], flattened
923/// to display strings (subject name, predicate, object label) plus its
924/// confidence.
925#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
926pub struct EnrichedStatement {
927    #[serde(with = "serde_bytes")]
928    pub id: [u8; 16],
929    pub subject_name: String,
930    pub predicate: String,
931    pub object_label: String,
932    /// The object's entity id when the object IS an entity; `None` for a
933    /// literal value.
934    ///
935    /// `object_label` alone cannot answer "is this an entity?" — it
936    /// stringifies both cases — so a consumer had to guess by matching the
937    /// label against known entity names, and that guess is wrong exactly when
938    /// a literal's text equals an entity's name. Optional on the wire, so a
939    /// peer that omits it still decodes; absent means "no entity object".
940    #[serde(default, with = "crate::wire::cbor::opt_byte_array16")]
941    pub object_entity_id: Option<[u8; 16]>,
942    pub confidence: f32,
943    /// When the statement's EVENT happened, in unix nanos — the reified
944    /// Time slot of an Event-kind statement. Absent from the wire map for
945    /// a statement with no event time.
946    #[serde(skip_serializing_if = "Option::is_none", default)]
947    pub event_at_unix_nanos: Option<u64>,
948}
949
950/// A typed relation attached to a recall hit by [`GraphEnrichment`], flattened
951/// to the two endpoint names and the connecting predicate.
952#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
953pub struct EnrichedRelation {
954    pub from_name: String,
955    pub predicate: String,
956    pub to_name: String,
957}
958
959// ===========================================================================
960// FORGET.
961// ===========================================================================
962
963/// FORGET (`0x0024`).
964#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
965pub struct ForgetRequest {
966    pub memory_id: WireMemoryId,
967    pub mode: ForgetMode,
968    #[serde(with = "serde_bytes")]
969    pub request_id: WireUuid,
970    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
971    pub txn_id: Option<WireUuid>,
972    /// Effective identity this forget runs as. `None` (omitted on the wire)
973    /// means the op runs as the connection's own key-bound identity.
974    #[serde(skip_serializing_if = "Option::is_none", default)]
975    pub act_as: Option<ActAs>,
976}
977
978/// FORGET_RESP (`0x00A4`).
979#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
980pub struct ForgetResponse {
981    pub memory_id: WireMemoryId,
982    pub was_already_forgotten: bool,
983    pub edges_removed: u32,
984}
985
986// ===========================================================================
987// LINK / UNLINK.
988// ===========================================================================
989
990/// LINK (`0x0025`). Creates (or reweights) an edge between two memories.
991#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
992pub struct LinkRequest {
993    pub source: WireMemoryId,
994    pub target: WireMemoryId,
995    pub kind: EdgeKindWire,
996    /// `[0, 1]` for most kinds; `[-1, 1]` for `Contradicts`.
997    pub weight: f32,
998    #[serde(with = "serde_bytes")]
999    pub request_id: WireUuid,
1000    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1001    pub txn_id: Option<WireUuid>,
1002    /// Effective identity this link runs as. `None` (omitted on the wire)
1003    /// means the op runs as the connection's own key-bound identity.
1004    #[serde(skip_serializing_if = "Option::is_none", default)]
1005    pub act_as: Option<ActAs>,
1006}
1007
1008/// LINK_RESP (`0x00A5`).
1009#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1010pub struct LinkResponse {
1011    pub source: WireMemoryId,
1012    pub target: WireMemoryId,
1013    pub kind: EdgeKindWire,
1014    pub weight: f32,
1015    pub created_at_unix_nanos: u64,
1016    /// `true` if the edge already existed (LINK overwrote its weight);
1017    /// `false` if newly created.
1018    pub already_existed: bool,
1019}
1020
1021/// UNLINK (`0x0026`). Removes the edge identified by the
1022/// `(source, kind, target)` triple.
1023#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1024pub struct UnlinkRequest {
1025    pub source: WireMemoryId,
1026    pub target: WireMemoryId,
1027    pub kind: EdgeKindWire,
1028    #[serde(with = "serde_bytes")]
1029    pub request_id: WireUuid,
1030    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1031    pub txn_id: Option<WireUuid>,
1032    /// Effective identity this unlink runs as. `None` (omitted on the wire)
1033    /// means the op runs as the connection's own key-bound identity.
1034    #[serde(skip_serializing_if = "Option::is_none", default)]
1035    pub act_as: Option<ActAs>,
1036}
1037
1038/// UNLINK_RESP (`0x00A6`).
1039#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1040pub struct UnlinkResponse {
1041    pub source: WireMemoryId,
1042    pub target: WireMemoryId,
1043    pub kind: EdgeKindWire,
1044    /// `true` if the edge existed and was removed; `false` if it didn't
1045    /// exist (UNLINK is idempotent — non-existent = no-op).
1046    pub removed: bool,
1047}
1048
1049// ===========================================================================
1050// MEMORY_LIST.
1051// ===========================================================================
1052
1053/// Sort axis for MEMORY_LIST. Integer discriminant on the wire. v1 supports
1054/// only `Created` end-to-end; the others are reserved wire values.
1055#[derive(
1056    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1057)]
1058#[repr(u8)]
1059pub enum MemoryListSortWire {
1060    Created = 0,
1061    Salience = 1,
1062    Occurred = 2,
1063    LastAccessed = 3,
1064}
1065
1066/// Sort direction for MEMORY_LIST. Integer discriminant on the wire.
1067#[derive(
1068    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1069)]
1070#[repr(u8)]
1071pub enum MemoryListDirWire {
1072    Asc = 0,
1073    Desc = 1,
1074}
1075
1076/// Which time field a MEMORY_LIST `from`/`to` range filters on. Integer
1077/// discriminant on the wire. v1 supports only `Created`.
1078#[derive(
1079    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1080)]
1081#[repr(u8)]
1082pub enum MemoryListTimeAxisWire {
1083    Created = 0,
1084    Occurred = 1,
1085}
1086
1087/// MEMORY_LIST (`0x0027`) — a paginated enumeration of the caller's
1088/// `(namespace, space)` memories. This is not RECALL: no query, no ranking.
1089/// It walks the tenant timeline in a stable order and returns a page plus an
1090/// opaque keyset cursor. Empty/zero fields mean "no filter". v1 supports
1091/// `sort = Created` (Asc/Desc); other sorts, the text filter, and the
1092/// occurred axis are rejected with InvalidRequest.
1093#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1094pub struct MemoryListRequest {
1095    pub sort: MemoryListSortWire,
1096    pub dir: MemoryListDirWire,
1097    /// Page size, validated server-side to `1..=100`.
1098    pub limit: u32,
1099    /// Empty on first page; opaque continuation token otherwise.
1100    pub cursor: Vec<u8>,
1101    /// Empty = all kinds; otherwise only these memory kinds are returned.
1102    pub kinds: Vec<MemoryKindWire>,
1103    /// When false, tombstoned memories are excluded; when true, both active
1104    /// and tombstoned rows are enumerated.
1105    pub include_tombstoned: bool,
1106    /// Which time field the `from`/`to` bounds apply to.
1107    pub time_axis: MemoryListTimeAxisWire,
1108    /// Inclusive lower time bound in unix-nanos; `0` = no lower bound.
1109    pub from_unix_nanos: u64,
1110    /// Inclusive upper time bound in unix-nanos; `0` = no upper bound.
1111    pub to_unix_nanos: u64,
1112    /// Inclusive salience floor in `[0, 1]`.
1113    pub salience_min: f32,
1114    /// Inclusive salience ceiling in `[0, 1]`.
1115    pub salience_max: f32,
1116    /// Substring/token filter over memory text; empty = no filter.
1117    pub text_contains: String,
1118    /// Effective identity this list runs as. `None` (omitted on the wire) means
1119    /// the op runs as the connection's own key-bound identity. Honored only when
1120    /// the connection principal holds the `ACT_AS` grant and the target
1121    /// namespace is in its `may_act` allowlist.
1122    #[serde(skip_serializing_if = "Option::is_none", default)]
1123    pub act_as: Option<ActAs>,
1124}
1125
1126/// One memory in a MEMORY_LIST response batch. Carries the
1127/// enumeration-relevant fields plus relationship-handle counts.
1128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1129pub struct MemoryListItem {
1130    #[serde(with = "serde_bytes")]
1131    pub memory_id: [u8; 16],
1132    /// Owner space (16-byte storage key) the row belongs to.
1133    #[serde(with = "serde_bytes")]
1134    pub space_id: [u8; 16],
1135    /// Session (conversation) the memory was encoded under; `0` = default.
1136    pub session_id: WireSessionId,
1137    pub text: String,
1138    /// Raw memory-kind byte (0 = Episodic, 1 = Semantic, 2 = Consolidated).
1139    pub kind: u8,
1140    /// Lifecycle state byte (0 = active, 1 = tombstoned).
1141    pub state: u8,
1142    pub created_at_unix_nanos: u64,
1143    /// Client-supplied event time; `0` when the memory has none.
1144    pub occurred_at_unix_nanos: u64,
1145    pub last_accessed_at_unix_nanos: u64,
1146    /// Point-in-time salience snapshot (it decays).
1147    pub salience: f32,
1148    pub access_count: u32,
1149    #[serde(with = "serde_bytes")]
1150    pub source_request_id: [u8; 16],
1151    pub statement_count: u32,
1152    pub entity_count: u32,
1153    pub relation_count: u32,
1154}
1155
1156/// One streaming MEMORY_LIST_RESP frame (`0x00A7`). The last frame carries
1157/// `is_final = true`. Empty `next_cursor` on the final frame means "exhausted";
1158/// non-empty means "more pages available, resume with this".
1159#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1160pub struct MemoryListResponseFrame {
1161    pub items: Vec<MemoryListItem>,
1162    pub next_cursor: Vec<u8>,
1163    pub cumulative_count: u32,
1164    pub is_final: bool,
1165}
1166
1167// ===========================================================================
1168// GRAPH_FETCH — full-space typed-graph export.
1169// ===========================================================================
1170
1171/// One graph node. `id` is the 16-byte entity / statement / memory id; `kind`
1172/// says which id-space it is (0 = Entity, 1 = Statement, 2 = Memory).
1173#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1174pub struct GraphNode {
1175    #[serde(with = "serde_bytes")]
1176    pub id: [u8; 16],
1177    pub kind: u8,
1178    pub label: String,
1179    /// Entity type qname (e.g. `brain:Person`); empty for non-entity nodes.
1180    pub type_qname: String,
1181}
1182
1183/// One graph edge. `kind`: 0 = Relation, 1 = Fact, 2 = HasStatement,
1184/// 3 = Mentions, 4..=11 = the memory↔memory builtin kinds in `EdgeKind` order
1185/// (`4 + EdgeKind as u8`), i.e. 4 = Caused, 5 = FollowedBy, 6 = DerivedFrom,
1186/// 7 = SimilarTo, 8 = Contradicts, 9 = Supports, 10 = References, 11 = PartOf.
1187/// The kind byte alone identifies the link — there is no companion field.
1188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1189pub struct GraphEdge {
1190    #[serde(with = "serde_bytes")]
1191    pub from_id: [u8; 16],
1192    #[serde(with = "serde_bytes")]
1193    pub to_id: [u8; 16],
1194    pub kind: u8,
1195    /// Predicate / relation-type label; empty for `Mentions`.
1196    pub label: String,
1197}
1198
1199/// GRAPH_FETCH (`0x0163`) — a paginated export of the caller's whole
1200/// `(namespace, space)` typed graph as a node/edge set. Default layer is the
1201/// concept map (entity nodes + Relation/Fact edges); `include_statements` adds
1202/// value-object statement nodes, `include_memories` adds source-memory nodes,
1203/// `include_memory_edges` adds the stored memory↔memory links between them.
1204/// The cursor is opaque, signed over the layer toggles.
1205#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1206pub struct GraphFetchRequest {
1207    /// Page size, validated server-side to `1..=500`.
1208    pub limit: u32,
1209    /// Empty on first page; opaque continuation token otherwise.
1210    pub cursor: Vec<u8>,
1211    /// Emit value-object statement nodes + their `HasStatement` edges.
1212    pub include_statements: bool,
1213    /// Emit source memory nodes + their `Mentions` edges.
1214    pub include_memories: bool,
1215    /// Emit the stored memory↔memory edges (`SimilarTo`, `FollowedBy`, …)
1216    /// incident to the page's memory nodes. Requires `include_memories` —
1217    /// setting it alone is rejected, because the edges would have no rendered
1218    /// endpoints. A far endpoint outside the page is emitted as a memory node
1219    /// so no edge dangles.
1220    pub include_memory_edges: bool,
1221    /// Include tombstoned statements/relations. Default false.
1222    pub include_tombstoned: bool,
1223    /// Effective identity this export runs as. `None` (omitted on the wire)
1224    /// runs as the connection's own key-bound identity.
1225    #[serde(skip_serializing_if = "Option::is_none", default)]
1226    pub act_as: Option<ActAs>,
1227}
1228
1229/// One streaming GRAPH_FETCH_RESP frame (`0x01E3`). Nodes/edges may repeat
1230/// across pages (completeness, not disjointness) — dedup by id. Empty
1231/// `next_cursor` means exhausted.
1232#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1233pub struct GraphFetchResponseFrame {
1234    pub nodes: Vec<GraphNode>,
1235    pub edges: Vec<GraphEdge>,
1236    pub next_cursor: Vec<u8>,
1237    pub is_final: bool,
1238}
1239
1240// ===========================================================================
1241// PLAN.
1242// ===========================================================================
1243
1244/// Plan-strategy hint. Integer discriminant on the wire.
1245#[derive(
1246    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1247)]
1248#[repr(u8)]
1249pub enum PlanStrategy {
1250    Auto = 0,
1251    AStar = 1,
1252    Mcts = 2,
1253    AttractorRollout = 3,
1254}
1255
1256/// Plan endpoint specification. Externally-tagged enum (CBOR map keyed by
1257/// the variant name), matching the server.
1258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1259pub enum PlanState {
1260    ByMemoryId(WireMemoryId),
1261    ByText(String),
1262    ByVector { offset: u32, dim: u16 },
1263}
1264
1265/// Plan budget.
1266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1267pub struct PlanBudget {
1268    pub max_steps: u32,
1269    pub max_wall_time_ms: u32,
1270    pub max_branches_explored: u32,
1271}
1272
1273/// PLAN (`0x0022`).
1274#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1275pub struct PlanRequest {
1276    pub start: PlanState,
1277    pub goal: PlanState,
1278    pub budget: PlanBudget,
1279    pub strategy_hint: Option<PlanStrategy>,
1280    pub session_filter: Option<Vec<WireSessionId>>,
1281    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1282    pub request_id: Option<WireUuid>,
1283    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1284    pub txn_id: Option<WireUuid>,
1285    /// Opt-in per-stage observability. When `true`, the final response frame
1286    /// carries a populated `trace: PlanTrace` describing every node the
1287    /// bidirectional BFS visited (in both directions) and every meeting
1288    /// point found, including the ones dropped by the `max_paths` cap. When
1289    /// `false` (the default) the field is absent and the plan pays nothing.
1290    /// Mirrors `RecallRequest.trace`.
1291    #[serde(default)]
1292    pub trace: bool,
1293    /// Effective identity this plan runs as. `None` (omitted on the wire)
1294    /// means the op runs as the connection's own key-bound identity.
1295    #[serde(skip_serializing_if = "Option::is_none", default)]
1296    pub act_as: Option<ActAs>,
1297}
1298
1299/// Plan terminal status, set on the final frame only. Integer discriminant.
1300#[derive(
1301    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1302)]
1303#[repr(u8)]
1304pub enum PlanStatus {
1305    GoalReached = 0,
1306    BudgetExhausted = 1,
1307    NoPathFound = 2,
1308    Cancelled = 3,
1309}
1310
1311/// Transition kind between plan steps. Externally-tagged enum, matching the
1312/// server (`Other` carries a free-form string).
1313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1314pub enum TransitionKind {
1315    Initial,
1316    Causal,
1317    Temporal,
1318    Similarity,
1319    Other(String),
1320}
1321
1322/// One step in a plan.
1323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1324pub struct PlanStep {
1325    pub step_index: u32,
1326    pub memory_id: WireMemoryId,
1327    pub text: String,
1328    pub transition_kind: TransitionKind,
1329    pub confidence: f32,
1330    pub estimated_distance_to_goal: f32,
1331}
1332
1333/// One streaming PLAN_RESP frame (`0x00A2`). The last frame carries
1334/// `is_final = true` and a populated `plan_status`.
1335#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1336pub struct PlanResponseFrame {
1337    pub steps: Vec<PlanStep>,
1338    pub is_final: bool,
1339    pub plan_status: Option<PlanStatus>,
1340    /// Per-stage bidirectional-search trace. Populated only on the final
1341    /// frame and only when the request set `trace = true`; `None` otherwise
1342    /// (and omitted from the wire map so `trace = false` plans pay
1343    /// nothing). Mirrors `RecallResponseFrame.trace`.
1344    #[serde(skip_serializing_if = "Option::is_none", default)]
1345    pub trace: Option<PlanTrace>,
1346}
1347
1348/// Per-stage observability for one PLAN, surfaced on the final frame when
1349/// the request opted in with `trace = true`. `trace = true` means full
1350/// detail — there is no separate knob — so every field below is populated
1351/// whenever this struct is present at all. Surfaces the full bidirectional
1352/// BFS visited-map contents from both directions (not just a scalar
1353/// explored-node count) and every meeting point found, flagging which
1354/// survived the `max_paths` cap.
1355#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1356pub struct PlanTrace {
1357    /// Every node the bidirectional BFS visited, in both directions.
1358    pub explored: Vec<PlanTraceNode>,
1359    /// Every meeting point the BFS found, flagging which ones survived the
1360    /// `max_paths` cap and made it into the returned path(s).
1361    pub meeting_points: Vec<PlanTraceMeetingPoint>,
1362}
1363
1364/// Which direction of the bidirectional BFS a `PlanTraceNode` was visited
1365/// from. Integer discriminant on the wire.
1366#[derive(
1367    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1368)]
1369#[repr(u8)]
1370pub enum PlanTraceDirection {
1371    /// Visited by the forward search, rooted at `start`.
1372    Forward = 0,
1373    /// Visited by the backward search, rooted at `goal`.
1374    Backward = 1,
1375}
1376
1377/// One node the bidirectional BFS visited, from either direction.
1378#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1379pub struct PlanTraceNode {
1380    pub memory_id: WireMemoryId,
1381    pub text: String,
1382    pub direction: PlanTraceDirection,
1383    pub depth: u32,
1384    /// The edge this node was reached through. `None` for the root
1385    /// (`start` in the forward direction, `goal` in the backward
1386    /// direction).
1387    pub parent_edge: Option<WireMemoryId>,
1388    /// The goal-proximity alignment score computed for this node, when one
1389    /// was computed.
1390    pub alignment_score: Option<f32>,
1391}
1392
1393/// One meeting point the bidirectional BFS found where the forward and
1394/// backward frontiers connected.
1395#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1396pub struct PlanTraceMeetingPoint {
1397    pub memory_id: WireMemoryId,
1398    pub text: String,
1399    /// `true` when this meeting point survived the `max_paths` cap and
1400    /// contributed a path to the response; `false` when it was found but
1401    /// dropped by the cap.
1402    pub included_in_result: bool,
1403}
1404
1405// ===========================================================================
1406// REASON.
1407// ===========================================================================
1408
1409/// What to reason about. Externally-tagged enum, matching the server.
1410#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1411pub enum ObservationInput {
1412    ByMemoryId(WireMemoryId),
1413    ByText(String),
1414}
1415
1416/// REASON (`0x0023`).
1417#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1418pub struct ReasonRequest {
1419    pub observation: ObservationInput,
1420    pub depth: u32,
1421    pub confidence_threshold: f32,
1422    pub session_filter: Option<Vec<WireSessionId>>,
1423    pub max_inferences: u32,
1424    pub budget_wall_time_ms: u32,
1425    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1426    pub request_id: Option<WireUuid>,
1427    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
1428    pub txn_id: Option<WireUuid>,
1429    /// Opt-in per-stage observability. When `true`, the final response frame
1430    /// carries a populated `trace: ReasonTrace` describing the full
1431    /// base-candidate set, every edge the outward walk considered (and why
1432    /// each was pruned), the un-collapsed per-item score components, and
1433    /// whether topic-alignment centroid computation ran. When `false` (the
1434    /// default) the field is absent and the reason pays nothing. Mirrors
1435    /// `RecallRequest.trace`.
1436    #[serde(default)]
1437    pub trace: bool,
1438    /// Effective identity this reason runs as. `None` (omitted on the wire)
1439    /// means the op runs as the connection's own key-bound identity.
1440    #[serde(skip_serializing_if = "Option::is_none", default)]
1441    pub act_as: Option<ActAs>,
1442}
1443
1444/// Reason terminal status, set on the final frame only. Integer discriminant.
1445#[derive(
1446    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1447)]
1448#[repr(u8)]
1449pub enum ReasonStatus {
1450    Complete = 0,
1451    BudgetExhausted = 1,
1452    DepthLimitReached = 2,
1453    Cancelled = 3,
1454}
1455
1456/// Inference kind. Externally-tagged enum, matching the server (`Other`
1457/// carries a free-form string).
1458#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1459pub enum InferenceKind {
1460    CausalExplanation,
1461    EvidenceAccumulation,
1462    AnalogicalInference,
1463    Other(String),
1464}
1465
1466/// One inference step.
1467#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1468pub struct InferenceStep {
1469    pub step_index: u32,
1470    pub claim: String,
1471    pub supporting_memories: Vec<WireMemoryId>,
1472    pub contradicting_memories: Vec<WireMemoryId>,
1473    pub confidence: f32,
1474    pub inference_kind: InferenceKind,
1475}
1476
1477/// One streaming REASON_RESP frame (`0x00A3`). The last frame carries
1478/// `is_final = true` and a populated `reason_status`.
1479#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1480pub struct ReasonResponseFrame {
1481    pub inferences: Vec<InferenceStep>,
1482    pub is_final: bool,
1483    pub reason_status: Option<ReasonStatus>,
1484    /// Per-stage read-pipeline trace. Populated only on the final frame and
1485    /// only when the request set `trace = true`; `None` otherwise (and
1486    /// omitted from the wire map so `trace = false` reasons pay nothing).
1487    /// Mirrors `RecallResponseFrame.trace`.
1488    #[serde(skip_serializing_if = "Option::is_none", default)]
1489    pub trace: Option<ReasonTrace>,
1490}
1491
1492/// Per-stage observability for one REASON, surfaced on the final frame when
1493/// the request opted in with `trace = true`. `trace = true` means full
1494/// detail — there is no separate knob — so every field below is populated
1495/// whenever this struct is present at all. Mirrors `RecallTrace`'s
1496/// per-stage-detail precedent, applied to the outward evidence walk instead
1497/// of the retriever fan-out.
1498#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1499pub struct ReasonTrace {
1500    /// The full HNSW hit set the base observation resolved to, not just the
1501    /// subset that seeded the walk.
1502    pub base: ReasonTraceBase,
1503    /// Everything the outward evidence walk touched: considered edges and
1504    /// every prune reason, one bucket per prune point.
1505    pub walk: ReasonTraceWalk,
1506    /// Un-collapsed score components for every surviving evidence item.
1507    pub scoring: Vec<ReasonTraceScoreBreakdown>,
1508    /// Whether topic-alignment centroid computation ran, and why not when
1509    /// it didn't.
1510    pub centroid: ReasonTraceCentroid,
1511}
1512
1513/// The base observation's resolved candidate set, before any evidence walk.
1514#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1515pub struct ReasonTraceBase {
1516    pub candidates: Vec<ReasonTraceCandidate>,
1517}
1518
1519/// One base-candidate hit surfaced in a REASON trace.
1520#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1521pub struct ReasonTraceCandidate {
1522    pub memory_id: WireMemoryId,
1523    pub text: String,
1524    pub score: f32,
1525}
1526
1527/// Everything the outward evidence walk considered and every reason an edge
1528/// or item was pruned before becoming a surviving `InferenceStep`.
1529#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1530pub struct ReasonTraceWalk {
1531    /// Every edge the walk visited at each node, before any pruning — the
1532    /// direct analogue of `RecallTraceRetriever.candidates`.
1533    pub considered: Vec<ReasonTraceEdgeCandidate>,
1534    /// Edges pruned by the edge-kind filter.
1535    pub dropped_by_edge_kind: Vec<ReasonTraceEdgeCandidate>,
1536    /// Edges pruned because the target memory was tombstoned.
1537    pub dropped_by_tombstone: Vec<ReasonTraceIdWithText>,
1538    /// Edges pruned because the target memory was already visited.
1539    pub dropped_by_visited: Vec<ReasonTraceIdWithText>,
1540    /// Evidence items pruned by `confidence_threshold`.
1541    pub dropped_by_confidence: Vec<ReasonTraceScoredId>,
1542    /// Supporting-evidence items dropped by the per-inference trim cap.
1543    pub dropped_by_max_supporting: Vec<ReasonTraceIdWithText>,
1544    /// Contradicting-evidence items dropped by the per-inference trim cap.
1545    pub dropped_by_max_contradicting: Vec<ReasonTraceIdWithText>,
1546}
1547
1548/// One edge the outward walk visited, considered or dropped.
1549#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1550pub struct ReasonTraceEdgeCandidate {
1551    pub memory_id: WireMemoryId,
1552    pub text: String,
1553    pub edge_kind: EdgeKindWire,
1554    pub depth: u32,
1555    pub from_memory_id: WireMemoryId,
1556    pub raw_score: f32,
1557}
1558
1559/// A memory id plus its stored text, with no other payload — the shared
1560/// shape for the walk's plain id-dropped buckets (tombstone / visited /
1561/// trim-cap) so an opaque id is never surfaced without the content that
1562/// explains the drop.
1563#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1564pub struct ReasonTraceIdWithText {
1565    pub memory_id: WireMemoryId,
1566    pub text: String,
1567}
1568
1569/// A memory id paired with the score it was dropped at.
1570#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1571pub struct ReasonTraceScoredId {
1572    pub memory_id: WireMemoryId,
1573    pub text: String,
1574    pub score: f32,
1575}
1576
1577/// The un-collapsed multiplicative score components combined into one
1578/// `InferenceStep.confidence` value.
1579#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1580pub struct ReasonTraceScoreBreakdown {
1581    pub memory_id: WireMemoryId,
1582    pub text: String,
1583    pub base_similarity: f32,
1584    pub decay: f32,
1585    pub weight_product: f32,
1586    pub alignment: f32,
1587    /// Structural-fit nudge from VSA analogical inference (bind/bundle/
1588    /// `analogy_query` over the item's statement-graph triple against the
1589    /// observation's own triple). Neutral `1.0` when no triple is
1590    /// resolvable for the item — a re-rank nudge only, never a gate.
1591    pub analogical_fit: f32,
1592    pub final_score: f32,
1593}
1594
1595/// Whether topic-alignment centroid computation ran for this REASON. The
1596/// server's centroid step silently skips several paths (singleton base,
1597/// text-only observation, missing text, embed error); this makes that
1598/// outcome visible on the wire instead of a debug-log-only signal.
1599#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1600pub struct ReasonTraceCentroid {
1601    pub computed: bool,
1602    /// Populated when `computed = false`; empty otherwise.
1603    pub skipped_reason: Option<String>,
1604}
1605
1606// ===========================================================================
1607// ERROR.
1608// ===========================================================================
1609
1610/// Error category — the retry axis. Integer discriminant on the wire.
1611#[derive(
1612    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
1613)]
1614#[repr(u8)]
1615pub enum ErrorCategoryWire {
1616    Protocol = 0,
1617    Authentication = 1,
1618    Authorization = 2,
1619    Validation = 3,
1620    NotFound = 4,
1621    Conflict = 5,
1622    ResourceExhausted = 6,
1623    Internal = 7,
1624    Unavailable = 8,
1625}
1626
1627/// Wire error code (`u16`). Carried in an ERROR frame; the integer value
1628/// is the stable programmatic handle.
1629/// Fine-grained wire error code.
1630///
1631/// ## Forward compatibility
1632///
1633/// Decoding is **total**: a code this build does not know maps to
1634/// [`ErrorCodeWire::Unknown`] rather than failing the frame. That matters
1635/// because the server adds codes over time, and the Python and TypeScript
1636/// SDKs both model `code` as a plain integer — so a newer server's ERROR
1637/// frame is readable there. Before this, Rust derived `Deserialize_repr` with
1638/// no catch-all, so a single unrecognised code made the whole ERROR frame
1639/// undecodable and the client reported a parse failure instead of the
1640/// server's actual error. That is a divergence from the other two SDKs, not a
1641/// deliberate choice.
1642///
1643/// `Unknown` deliberately carries no payload so the enum keeps its `repr(u16)`
1644/// and every `as u16` cast still works. Callers that need to distinguish an
1645/// unrecognised code use [`ErrorResponse::code_is_unknown`].
1646#[derive(Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr)]
1647#[repr(u16)]
1648pub enum ErrorCodeWire {
1649    // Protocol.
1650    BadMagic = 0x0001,
1651    BadHeaderCrc = 0x0002,
1652    BadPayloadCrc = 0x0003,
1653    BadOpcode = 0x0004,
1654    BadVersion = 0x0005,
1655    BadFrame = 0x0006,
1656    OversizePayload = 0x0007,
1657    ReservedFieldNonZero = 0x0008,
1658    BadFlagCombination = 0x0009,
1659    MalformedPayload = 0x000A,
1660    MalformedVector = 0x000B,
1661    // Connection / handshake.
1662    VersionNotSupported = 0x0020,
1663    NoSuchAuthMethod = 0x0021,
1664    Unauthenticated = 0x0022,
1665    NotAuthenticated = 0x0023,
1666    AuthBackendUnavailable = 0x0024,
1667    SessionExpired = 0x0025,
1668    // Authorization.
1669    PermissionDenied = 0x0030,
1670    AdminPermissionRequired = 0x0031,
1671    WrongShard = 0x0032,
1672    ActAsDenied = 0x0033,
1673    // Validation.
1674    InvalidArgument = 0x0040,
1675    MissingRequiredField = 0x0041,
1676    TextTooLarge = 0x0042,
1677    TextEmpty = 0x0043,
1678    BadSessionId = 0x0044,
1679    BadMemoryKind = 0x0045,
1680    BadEdgeKind = 0x0046,
1681    BadStrategyHint = 0x0047,
1682    TopKOutOfRange = 0x0048,
1683    BudgetTooLarge = 0x0049,
1684    BadModelFingerprint = 0x004A,
1685    PredicateNotInSchema = 0x004B,
1686    RelationTypeNotInSchema = 0x004C,
1687    // Not found.
1688    MemoryNotFound = 0x0050,
1689    ContextNotFound = 0x0051,
1690    SubscriptionNotFound = 0x0052,
1691    SnapshotNotFound = 0x0053,
1692    TxnNotFound = 0x0054,
1693    // Conflict.
1694    IdempotencyConflict = 0x0060,
1695    TransactionConflict = 0x0061,
1696    TransactionTimeout = 0x0062,
1697    StreamIdInUse = 0x0063,
1698    SubscriptionLsnTooOld = 0x0064,
1699    CardinalityViolation = 0x0065,
1700    /// The write's namespace has no usable LLM provider credential.
1701    /// **Terminal** — do not retry; the tenant must register a key.
1702    LlmCredentialMissing = 0x0066,
1703    // Resource exhausted.
1704    OutOfSlots = 0x0070,
1705    OutOfDisk = 0x0071,
1706    OutOfMemory = 0x0072,
1707    RateLimited = 0x0073,
1708    StreamLimitExceeded = 0x0074,
1709    ConnectionLimitExceeded = 0x0075,
1710    TransactionLimitExceeded = 0x0076,
1711    TransactionTooLarge = 0x0077,
1712    // Internal.
1713    Internal = 0x0080,
1714    StorageError = 0x0081,
1715    IndexError = 0x0082,
1716    EmbeddingError = 0x0083,
1717    MetadataError = 0x0084,
1718    Cancelled = 0x0085,
1719    // Unavailable.
1720    ShardUnavailable = 0x0090,
1721    Overloaded = 0x0091,
1722    Restarting = 0x0092,
1723    Maintenance = 0x0093,
1724    RetrieverDegraded = 0x0094,
1725    /// The namespace's LLM credential could not be resolved right now —
1726    /// broker unreachable, timed out, or transiently failing. **Retryable.**
1727    LlmCredentialUnavailable = 0x0095,
1728    // Typed-graph.
1729    SchemaInvalid = 0x0120,
1730    SchemaMigrationRequired = 0x0121,
1731    EntityNotFound = 0x0130,
1732    EntityTypeMismatch = 0x0131,
1733    EntityAmbiguous = 0x0132,
1734    EntityMergeConflict = 0x0133,
1735    StatementNotFound = 0x0140,
1736    StatementObjectTypeMismatch = 0x0141,
1737    StatementContradictsExisting = 0x0142,
1738    QueryTimeout = 0x0160,
1739    QueryOverBudget = 0x0161,
1740    ExtractorDisabled = 0x0170,
1741    ExtractorBudgetExceeded = 0x0171,
1742    ExtractionFailed = 0x0172,
1743    /// A code this build does not recognise. See the type docs: decoding is
1744    /// total so a newer server cannot make an ERROR frame unreadable.
1745    Unknown = 0xFFFF,
1746}
1747
1748impl ErrorCodeWire {
1749    /// Map a raw wire value to a known code, or [`Self::Unknown`].
1750    ///
1751    /// Exhaustive over every code this build knows; anything else is
1752    /// `Unknown`, which is what makes decoding total.
1753    #[must_use]
1754    pub fn from_raw(raw: u16) -> Self {
1755        match raw {
1756            0x0001 => Self::BadMagic,
1757            0x0002 => Self::BadHeaderCrc,
1758            0x0003 => Self::BadPayloadCrc,
1759            0x0004 => Self::BadOpcode,
1760            0x0005 => Self::BadVersion,
1761            0x0006 => Self::BadFrame,
1762            0x0007 => Self::OversizePayload,
1763            0x0008 => Self::ReservedFieldNonZero,
1764            0x0009 => Self::BadFlagCombination,
1765            0x000A => Self::MalformedPayload,
1766            0x000B => Self::MalformedVector,
1767            0x0020 => Self::VersionNotSupported,
1768            0x0021 => Self::NoSuchAuthMethod,
1769            0x0022 => Self::Unauthenticated,
1770            0x0023 => Self::NotAuthenticated,
1771            0x0024 => Self::AuthBackendUnavailable,
1772            0x0025 => Self::SessionExpired,
1773            0x0030 => Self::PermissionDenied,
1774            0x0031 => Self::AdminPermissionRequired,
1775            0x0032 => Self::WrongShard,
1776            0x0033 => Self::ActAsDenied,
1777            0x0040 => Self::InvalidArgument,
1778            0x0041 => Self::MissingRequiredField,
1779            0x0042 => Self::TextTooLarge,
1780            0x0043 => Self::TextEmpty,
1781            0x0044 => Self::BadSessionId,
1782            0x0045 => Self::BadMemoryKind,
1783            0x0046 => Self::BadEdgeKind,
1784            0x0047 => Self::BadStrategyHint,
1785            0x0048 => Self::TopKOutOfRange,
1786            0x0049 => Self::BudgetTooLarge,
1787            0x004A => Self::BadModelFingerprint,
1788            0x004B => Self::PredicateNotInSchema,
1789            0x004C => Self::RelationTypeNotInSchema,
1790            0x0050 => Self::MemoryNotFound,
1791            0x0051 => Self::ContextNotFound,
1792            0x0052 => Self::SubscriptionNotFound,
1793            0x0053 => Self::SnapshotNotFound,
1794            0x0054 => Self::TxnNotFound,
1795            0x0060 => Self::IdempotencyConflict,
1796            0x0061 => Self::TransactionConflict,
1797            0x0062 => Self::TransactionTimeout,
1798            0x0063 => Self::StreamIdInUse,
1799            0x0064 => Self::SubscriptionLsnTooOld,
1800            0x0065 => Self::CardinalityViolation,
1801            0x0070 => Self::OutOfSlots,
1802            0x0071 => Self::OutOfDisk,
1803            0x0072 => Self::OutOfMemory,
1804            0x0073 => Self::RateLimited,
1805            0x0074 => Self::StreamLimitExceeded,
1806            0x0075 => Self::ConnectionLimitExceeded,
1807            0x0076 => Self::TransactionLimitExceeded,
1808            0x0077 => Self::TransactionTooLarge,
1809            0x0080 => Self::Internal,
1810            0x0081 => Self::StorageError,
1811            0x0082 => Self::IndexError,
1812            0x0083 => Self::EmbeddingError,
1813            0x0084 => Self::MetadataError,
1814            0x0085 => Self::Cancelled,
1815            0x0090 => Self::ShardUnavailable,
1816            0x0091 => Self::Overloaded,
1817            0x0092 => Self::Restarting,
1818            0x0093 => Self::Maintenance,
1819            0x0094 => Self::RetrieverDegraded,
1820            0x0120 => Self::SchemaInvalid,
1821            0x0121 => Self::SchemaMigrationRequired,
1822            0x0130 => Self::EntityNotFound,
1823            0x0131 => Self::EntityTypeMismatch,
1824            0x0132 => Self::EntityAmbiguous,
1825            0x0133 => Self::EntityMergeConflict,
1826            0x0140 => Self::StatementNotFound,
1827            0x0141 => Self::StatementObjectTypeMismatch,
1828            0x0142 => Self::StatementContradictsExisting,
1829            0x0160 => Self::QueryTimeout,
1830            0x0161 => Self::QueryOverBudget,
1831            0x0170 => Self::ExtractorDisabled,
1832            0x0171 => Self::ExtractorBudgetExceeded,
1833            0x0172 => Self::ExtractionFailed,
1834            0x0066 => Self::LlmCredentialMissing,
1835            0x0095 => Self::LlmCredentialUnavailable,
1836            _ => Self::Unknown,
1837        }
1838    }
1839}
1840
1841impl<'de> serde::Deserialize<'de> for ErrorCodeWire {
1842    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1843        Ok(Self::from_raw(u16::deserialize(d)?))
1844    }
1845}
1846
1847/// Structured detail attached to an error.
1848#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1849pub struct ErrorDetails {
1850    pub field: Option<String>,
1851    pub expected: Option<String>,
1852    pub actual: Option<String>,
1853}
1854
1855/// ERROR (`0x00FF`).
1856#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1857pub struct ErrorResponse {
1858    pub code: ErrorCodeWire,
1859    pub category: ErrorCategoryWire,
1860    pub message: String,
1861    pub details: Option<ErrorDetails>,
1862    pub retry_after_ms: Option<u32>,
1863}
1864
1865impl ErrorResponse {
1866    /// True when the server sent a code this build does not know. Callers
1867    /// should fall back to [`Self::category`], which is the field the wire
1868    /// contract guarantees is always meaningful.
1869    #[must_use]
1870    pub fn code_is_unknown(&self) -> bool {
1871        self.code == ErrorCodeWire::Unknown
1872    }
1873}
1874
1875// ===========================================================================
1876// Typed-graph payloads exercised by the corpus.
1877// ===========================================================================
1878
1879/// Statement kind. The fieldless variants encode as a CBOR **string** unit
1880/// variant (plain serde, not an integer discriminant); `Custom(u8)` encodes
1881/// as a single-entry CBOR map `{"Custom": <byte>}` — the server does the
1882/// same. The storage-byte mapping is `0 = Fact`, `1 = Preference`,
1883/// `2 = Event`, `3 = Attribute`, `4 = Relation`, `5 = Directive`, and any
1884/// byte `>= 6` is a `Custom(byte)`.
1885#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1886pub enum StatementKindWire {
1887    Fact,
1888    Preference,
1889    Event,
1890    Attribute,
1891    Relation,
1892    Directive,
1893    Custom(u8),
1894}
1895
1896impl StatementKindWire {
1897    /// The storage discriminant byte for this kind.
1898    #[must_use]
1899    pub fn to_storage_byte(self) -> u8 {
1900        match self {
1901            StatementKindWire::Fact => 0,
1902            StatementKindWire::Preference => 1,
1903            StatementKindWire::Event => 2,
1904            StatementKindWire::Attribute => 3,
1905            StatementKindWire::Relation => 4,
1906            StatementKindWire::Directive => 5,
1907            StatementKindWire::Custom(b) => b,
1908        }
1909    }
1910
1911    /// The kind for a storage discriminant byte. Bytes `>= 6` map to
1912    /// `Custom(byte)`.
1913    #[must_use]
1914    pub fn from_storage_byte(byte: u8) -> Self {
1915        match byte {
1916            0 => StatementKindWire::Fact,
1917            1 => StatementKindWire::Preference,
1918            2 => StatementKindWire::Event,
1919            3 => StatementKindWire::Attribute,
1920            4 => StatementKindWire::Relation,
1921            5 => StatementKindWire::Directive,
1922            b => StatementKindWire::Custom(b),
1923        }
1924    }
1925}
1926
1927/// Scalar object value. Externally-tagged.
1928#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1929pub enum StatementValueWire {
1930    Text(String),
1931    Integer(i64),
1932    Float(f64),
1933    Bool(bool),
1934    UnixNanos(u64),
1935    Blob(Vec<u8>),
1936}
1937
1938/// Statement object. Each id variant is a CBOR byte string.
1939#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1940pub enum StatementObjectWire {
1941    EntityRef(#[serde(with = "serde_bytes")] WireUuid),
1942    Value(StatementValueWire),
1943    MemoryRef(#[serde(with = "serde_bytes")] [u8; 16]),
1944    StatementRef(#[serde(with = "serde_bytes")] WireUuid),
1945}
1946
1947/// Evidence reference. `Inline` is an array of byte-string memory ids.
1948#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1949pub enum EvidenceRefWire {
1950    Inline(#[serde(with = "crate::wire::cbor::vec_byte_array16")] Vec<[u8; 16]>),
1951    Overflow(#[serde(with = "serde_bytes")] WireUuid),
1952}
1953
1954/// ENTITY_CREATE (`0x0130`).
1955#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1956pub struct EntityCreateRequest {
1957    pub entity_type_id: u32,
1958    pub canonical_name: String,
1959    pub aliases: Vec<String>,
1960    pub attributes_blob: Vec<u8>,
1961    /// Session that first mentions this entity; `0` = default session.
1962    /// First-mention provenance only — entity identity is session-agnostic.
1963    #[serde(default)]
1964    pub session_id: WireSessionId,
1965    #[serde(with = "serde_bytes")]
1966    pub request_id: WireUuid,
1967    /// Effective identity this entity-create runs as. `None` (omitted on the
1968    /// wire) means the op runs as the connection's own key-bound identity.
1969    #[serde(skip_serializing_if = "Option::is_none", default)]
1970    pub act_as: Option<ActAs>,
1971}
1972
1973/// ENTITY_UPDATE (`0x0132`). Replace an entity's name, aliases, and
1974/// attribute blob wholesale — the way an agent revises what it knows about a
1975/// thing as new detail arrives.
1976#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1977pub struct EntityUpdateRequest {
1978    #[serde(with = "serde_bytes")]
1979    pub entity_id: WireUuid,
1980    pub canonical_name: String,
1981    pub aliases: Vec<String>,
1982    pub attributes_blob: Vec<u8>,
1983    #[serde(with = "serde_bytes")]
1984    pub request_id: WireUuid,
1985}
1986
1987/// ENTITY_UPDATE_RESP (`0x01B2`). The post-update view.
1988#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1989pub struct EntityUpdateResponse {
1990    pub entity: EntityView,
1991}
1992
1993/// ENTITY_RENAME (`0x0133`). Change only the canonical name. `move_to_alias`
1994/// keeps the old name reachable as an alias instead of dropping it, so a
1995/// rename never silently loses a name the graph already links by.
1996#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1997pub struct EntityRenameRequest {
1998    #[serde(with = "serde_bytes")]
1999    pub entity_id: WireUuid,
2000    pub new_canonical_name: String,
2001    pub move_to_alias: bool,
2002    #[serde(with = "serde_bytes")]
2003    pub request_id: WireUuid,
2004}
2005
2006/// ENTITY_RENAME_RESP (`0x01B3`). The post-rename view.
2007#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2008pub struct EntityRenameResponse {
2009    pub entity: EntityView,
2010}
2011
2012/// ENTITY_MERGE (`0x0134`). Fold `merged` into `survivor` when they turn out
2013/// to be the same real-world entity. The merge is reversible within a grace
2014/// window (see the response), so a wrong merge is recoverable rather than
2015/// destructive.
2016#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2017pub struct EntityMergeRequest {
2018    #[serde(with = "serde_bytes")]
2019    pub survivor: WireUuid,
2020    #[serde(with = "serde_bytes")]
2021    pub merged: WireUuid,
2022    pub confidence: f32,
2023    pub reason: String,
2024    #[serde(with = "serde_bytes")]
2025    pub request_id: WireUuid,
2026}
2027
2028/// ENTITY_MERGE_RESP (`0x01B4`). `audit_id` is the merge record (not an
2029/// entity id); `grace_period_seconds` is how long an unmerge can still undo it.
2030#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2031pub struct EntityMergeResponse {
2032    #[serde(with = "serde_bytes")]
2033    pub audit_id: WireUuid,
2034    pub grace_period_seconds: u64,
2035}
2036
2037/// ENTITY_UNMERGE (`0x0135`). Undo a merge, restoring the folded-away entity
2038/// as its own row again. Only valid inside the merge's grace window.
2039#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2040pub struct EntityUnmergeRequest {
2041    #[serde(with = "serde_bytes")]
2042    pub merged_entity: WireUuid,
2043    #[serde(with = "serde_bytes")]
2044    pub request_id: WireUuid,
2045}
2046
2047/// ENTITY_UNMERGE_RESP (`0x01B5`). The id the restored entity now lives under.
2048#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2049pub struct EntityUnmergeResponse {
2050    #[serde(with = "serde_bytes")]
2051    pub restored_entity_id: WireUuid,
2052}
2053
2054/// ENTITY_TOMBSTONE (`0x0138`). Retire an entity with a reason for the audit
2055/// trail. Tombstoning is soft — the row survives (recoverable) but drops out
2056/// of resolution and traversal.
2057#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2058pub struct EntityTombstoneRequest {
2059    #[serde(with = "serde_bytes")]
2060    pub entity_id: WireUuid,
2061    pub reason: String,
2062    #[serde(with = "serde_bytes")]
2063    pub request_id: WireUuid,
2064}
2065
2066/// ENTITY_TOMBSTONE_RESP (`0x01B8`). When the retirement took effect.
2067#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2068pub struct EntityTombstoneResponse {
2069    pub tombstoned_at_unix_nanos: u64,
2070}
2071
2072/// ENTITY_CREATE_RESP (`0x01B0`). The id is a plain CBOR array of bytes,
2073/// not a byte string — the server's response struct omits `serde_bytes`
2074/// on this field.
2075#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2076pub struct EntityCreateResponse {
2077    #[serde(with = "serde_bytes")]
2078    pub entity_id: WireUuid,
2079}
2080
2081/// STATEMENT_CREATE (`0x0140`).
2082#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2083pub struct StatementCreateRequest {
2084    pub kind: StatementKindWire,
2085    #[serde(with = "serde_bytes")]
2086    pub subject: WireUuid,
2087    pub predicate: String,
2088    pub object: StatementObjectWire,
2089    pub confidence: f32,
2090    pub evidence: EvidenceRefWire,
2091    pub extractor_id: u32,
2092    pub valid_from_unix_nanos: u64,
2093    pub valid_to_unix_nanos: u64,
2094    pub event_at_unix_nanos: u64,
2095    pub schema_version: u32,
2096    /// Conversation/run this statement belongs to; `0` = default session.
2097    /// A grouping key, not an isolation boundary.
2098    #[serde(default)]
2099    pub session_id: WireSessionId,
2100    #[serde(with = "serde_bytes")]
2101    pub request_id: WireUuid,
2102    /// Effective identity this statement-create runs as. `None` (omitted on
2103    /// the wire) means the op runs as the connection's own key-bound identity.
2104    #[serde(skip_serializing_if = "Option::is_none", default)]
2105    pub act_as: Option<ActAs>,
2106}
2107
2108/// STATEMENT_CREATE_RESP (`0x01C0`). All ids encode as CBOR byte strings
2109/// (major type 2), matching the server's uniform 16-byte id encoding.
2110#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2111pub struct StatementCreateResponse {
2112    #[serde(with = "serde_bytes")]
2113    pub statement_id: WireUuid,
2114    #[serde(with = "serde_bytes")]
2115    pub auto_superseded: WireUuid,
2116    #[serde(with = "serde_bytes")]
2117    pub chain_root: WireUuid,
2118}
2119
2120/// STATEMENT_SUPERSEDE (`0x0142`). Replace an existing claim with a revised
2121/// one, keeping both linked on the same supersession chain so history stays
2122/// intact — the way an agent updates a belief without erasing what it used to
2123/// hold.
2124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2125pub struct StatementSupersedeRequest {
2126    #[serde(with = "serde_bytes")]
2127    pub old_statement_id: WireUuid,
2128    pub new_statement: StatementCreateRequest,
2129    #[serde(with = "serde_bytes")]
2130    pub request_id: WireUuid,
2131}
2132
2133/// STATEMENT_SUPERSEDE_RESP (`0x01C2`). The new statement plus its chain root
2134/// and monotonically-increasing version.
2135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2136pub struct StatementSupersedeResponse {
2137    #[serde(with = "serde_bytes")]
2138    pub new_statement_id: WireUuid,
2139    #[serde(with = "serde_bytes")]
2140    pub chain_root: WireUuid,
2141    pub version: u32,
2142}
2143
2144/// STATEMENT_TOMBSTONE (`0x0143`). Retire a statement with a coded reason for
2145/// the audit trail. Soft: the row survives (recoverable) but stops answering.
2146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2147pub struct StatementTombstoneRequest {
2148    #[serde(with = "serde_bytes")]
2149    pub statement_id: WireUuid,
2150    pub reason: u8,
2151    pub reason_message: String,
2152    #[serde(with = "serde_bytes")]
2153    pub request_id: WireUuid,
2154}
2155
2156/// STATEMENT_TOMBSTONE_RESP (`0x01C3`). When the retirement took effect.
2157#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2158pub struct StatementTombstoneResponse {
2159    pub tombstoned_at_unix_nanos: u64,
2160}
2161
2162/// STATEMENT_RETRACT (`0x0144`). Assert a claim was wrong (not merely
2163/// superseded). Retraction schedules a hard-zero after a window, so a genuine
2164/// mistake can be scrubbed rather than left tombstoned forever.
2165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2166pub struct StatementRetractRequest {
2167    #[serde(with = "serde_bytes")]
2168    pub statement_id: WireUuid,
2169    pub reason: u8,
2170    pub reason_message: String,
2171    #[serde(with = "serde_bytes")]
2172    pub request_id: WireUuid,
2173}
2174
2175/// STATEMENT_RETRACT_RESP (`0x01C4`). When it was retracted, and when the
2176/// scheduled hard-zero will scrub it.
2177#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2178pub struct StatementRetractResponse {
2179    pub retracted_at_unix_nanos: u64,
2180    pub will_zero_at_unix_nanos: u64,
2181}
2182
2183/// STATEMENT_HISTORY (`0x0145`). Walk every version on a claim's supersession
2184/// chain. A read, so it carries no `request_id`. Keyset-paginated on the
2185/// immutable chain `version`: `limit` in `1..=1000`, `cursor` opaque — empty on
2186/// the first page, then the `next_cursor` echoed from the previous response.
2187#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2188pub struct StatementHistoryRequest {
2189    #[serde(with = "serde_bytes")]
2190    pub anchor_id: WireUuid,
2191    pub include_tombstoned: bool,
2192    pub limit: u32,
2193    pub cursor: Vec<u8>,
2194}
2195
2196/// STATEMENT_HISTORY_RESP (`0x01C5`), one streamed frame. `is_final` marks the
2197/// last frame; `total_versions` is the chain length. `next_cursor` is the opaque
2198/// keyset token to resume from, empty when the chain is exhausted.
2199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2200pub struct StatementHistoryResponseFrame {
2201    pub items: Vec<StatementView>,
2202    #[serde(with = "serde_bytes")]
2203    pub chain_root: WireUuid,
2204    pub total_versions: u32,
2205    pub next_cursor: Vec<u8>,
2206    pub is_final: bool,
2207}
2208
2209/// RELATION_CREATE (`0x0150`).
2210#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2211pub struct RelationCreateRequest {
2212    pub relation_type: String,
2213    #[serde(with = "serde_bytes")]
2214    pub from_entity: WireUuid,
2215    #[serde(with = "serde_bytes")]
2216    pub to_entity: WireUuid,
2217    pub properties_blob: Vec<u8>,
2218    pub evidence: EvidenceRefWire,
2219    pub extractor_id: u32,
2220    pub confidence: f32,
2221    pub valid_from_unix_nanos: u64,
2222    pub valid_to_unix_nanos: u64,
2223    /// Conversation/run this relation belongs to; `0` = default session.
2224    /// A grouping key, not an isolation boundary.
2225    #[serde(default)]
2226    pub session_id: WireSessionId,
2227    #[serde(with = "serde_bytes")]
2228    pub request_id: WireUuid,
2229    /// Effective identity this relation-create runs as. `None` (omitted on the
2230    /// wire) means the op runs as the connection's own key-bound identity.
2231    #[serde(skip_serializing_if = "Option::is_none", default)]
2232    pub act_as: Option<ActAs>,
2233}
2234
2235/// RELATION_CREATE_RESP (`0x01D0`).
2236#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2237pub struct RelationCreateResponse {
2238    #[serde(with = "serde_bytes")]
2239    pub relation_id: WireUuid,
2240}
2241
2242/// SCHEMA_UPLOAD (`0x0120`).
2243#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2244pub struct SchemaUploadRequest {
2245    pub schema_document: String,
2246    pub dry_run: bool,
2247    pub allow_breaking: bool,
2248    #[serde(with = "serde_bytes")]
2249    pub request_id: WireUuid,
2250    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
2251    #[serde(skip_serializing_if = "Option::is_none", default)]
2252    pub act_as: Option<ActAs>,
2253}
2254
2255/// One structured schema parse/validate error.
2256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2257pub struct SchemaValidationErrorWire {
2258    pub code: String,
2259    pub message: String,
2260    pub line: u32,
2261    pub column: u32,
2262    pub length: u32,
2263    pub severity: u8,
2264}
2265
2266/// SCHEMA_UPLOAD_RESP (`0x01A0`).
2267#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2268pub struct SchemaUploadResponse {
2269    pub namespace: String,
2270    pub schema_version: u32,
2271    pub validation_errors: Vec<SchemaValidationErrorWire>,
2272    pub backward_compatible: bool,
2273    pub migration_summary_blob: Vec<u8>,
2274}
2275
2276/// Target kinds for a [`SchemaDropRequest`]. `target_kind` MUST be one of
2277/// these; the server rejects any other discriminant with `InvalidRequest`.
2278///
2279/// Entity types are deliberately absent: they are global in the v1 storage
2280/// model, so dropping one would race rows in other namespaces that reference
2281/// the same shared type — the same reason `SCHEMA_REPLACE` never drops them.
2282pub mod schema_drop_target {
2283    /// Drop a declared predicate.
2284    pub const PREDICATE: u8 = 0;
2285    /// Drop a declared relation_type.
2286    pub const RELATION_TYPE: u8 = 1;
2287}
2288
2289/// `SCHEMA_DROP` (`0x0125`) — surgical narrow of a single declared type.
2290///
2291/// The per-type counterpart to the namespace-wide `SCHEMA_REPLACE`: removes one
2292/// declared predicate or relation_type from the active schema set, then bumps
2293/// the namespace to a new version whose document no longer declares it. Existing
2294/// rows on the dropped type survive as orphans — readable as plain memories, no
2295/// longer enriched from the typed-graph tables.
2296///
2297/// `force` is required only when the target still has live (non-tombstoned)
2298/// rows: `false` with live rows present is rejected with `Conflict` and mutates
2299/// nothing. A type with no live rows drops without `force`.
2300#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2301pub struct SchemaDropRequest {
2302    /// Namespace the target lives in. MUST equal the caller's own namespace,
2303    /// or the server rejects with `Unauthorized`.
2304    pub namespace: String,
2305    /// One of [`schema_drop_target`]. Any other value → `InvalidRequest`.
2306    pub target_kind: u8,
2307    /// Local name of the predicate / relation_type to drop (the qname is
2308    /// `{namespace}:{target_name}`).
2309    pub target_name: String,
2310    /// Confirmation flag required only when the target still has live rows.
2311    pub force: bool,
2312    #[serde(with = "serde_bytes")]
2313    pub request_id: WireUuid,
2314    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
2315    #[serde(skip_serializing_if = "Option::is_none", default)]
2316    pub act_as: Option<ActAs>,
2317}
2318
2319/// `SCHEMA_DROP_RESP` (`0x01A5`).
2320///
2321/// `schema_version` is the new active version after the narrow, or `0` when
2322/// nothing was dropped or the drop was rejected. `dropped` is `true` only when
2323/// a declared row was actually removed. `live_rows` is the count of live rows
2324/// found referencing the target — non-zero with `dropped == false` means the
2325/// drop was refused for lack of `force`.
2326#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2327pub struct SchemaDropResponse {
2328    pub namespace: String,
2329    pub schema_version: u32,
2330    pub target_kind: u8,
2331    pub target_name: String,
2332    pub dropped: bool,
2333    pub live_rows: u32,
2334    pub validation_errors: Vec<SchemaValidationErrorWire>,
2335}
2336
2337/// `SCHEMA_REPLACE` (`0x0127`) — destructive namespace swap.
2338///
2339/// Unlike `SCHEMA_UPLOAD`, this DROPS every declared row in the namespace
2340/// before the new document lands. Entities whose type disappears survive as
2341/// orphans: still readable as plain memories, no longer enriched from the
2342/// typed-graph tables.
2343///
2344/// `force_drop_existing` MUST be `true` — the server rejects `false` with
2345/// `InvalidRequest`. It is a confirmation step for an irreversible operation,
2346/// not a mode switch, which is why the SDK does not default it.
2347#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2348pub struct SchemaReplaceRequest {
2349    /// Schema DSL source. Must declare the same namespace as the connection's,
2350    /// or the server rejects with `InvalidRequest`.
2351    pub schema_document: String,
2352    /// MUST be `true`.
2353    pub force_drop_existing: bool,
2354    #[serde(with = "serde_bytes")]
2355    pub request_id: WireUuid,
2356    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
2357    #[serde(skip_serializing_if = "Option::is_none", default)]
2358    pub act_as: Option<ActAs>,
2359}
2360
2361/// `SCHEMA_REPLACE_RESP` (`0x01A7`).
2362///
2363/// `dropped_count` is how many declared rows were removed before the new
2364/// schema took effect. `schema_version` is always greater than the
2365/// pre-replace version.
2366#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2367pub struct SchemaReplaceResponse {
2368    pub namespace: String,
2369    pub schema_version: u32,
2370    pub dropped_count: u32,
2371    pub validation_errors: Vec<SchemaValidationErrorWire>,
2372}
2373
2374/// Why a stream was cancelled. Carried by [`CancelStreamRequest`].
2375#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2376pub enum CancellationReason {
2377    /// The caller no longer needs the results — the ordinary case when a
2378    /// consumer breaks out of a `recall` loop early.
2379    ClientUnneeded,
2380    /// A client-side deadline elapsed.
2381    Timeout,
2382    /// Anything else, with a free-text explanation.
2383    Other(String),
2384}
2385
2386/// `CANCEL_STREAM` (`0x0050`) — stop an in-flight stream.
2387///
2388/// Sent on a NEW stream id, naming the stream to cancel in
2389/// `target_stream_id`; the server answers `CANCEL_STREAM_ACK` and stops
2390/// emitting frames for the target. Without this a consumer that abandons a
2391/// streamed `recall` leaves the server producing frames nobody reads.
2392#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2393pub struct CancelStreamRequest {
2394    pub target_stream_id: u32,
2395    pub reason: CancellationReason,
2396}
2397
2398/// `CANCEL_STREAM_ACK` (`0x00D0`).
2399#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2400pub struct CancelStreamAck {
2401    pub target_stream_id: u32,
2402    pub cancelled_at_unix_nanos: u64,
2403}
2404
2405/// Time window for a query. `None` bounds are open-ended.
2406#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2407pub struct TimeRangeWire {
2408    pub from_unix_ms: Option<u64>,
2409    pub to_unix_ms: Option<u64>,
2410}
2411
2412/// Retriever family selector, as carried inside [`RetrieverSelectionWire`].
2413///
2414/// Encodes as the **variant-name string**, not the discriminant. `#[repr(u8)]`
2415/// is a memory-layout hint; serde ignores it because this derives plain
2416/// `Serialize`, not `serde_repr::Serialize_repr`. So
2417/// `Explicit([Semantic, Graph])` goes on the wire as
2418/// `{"Explicit": ["Semantic", "Graph"]}`.
2419///
2420/// The discriminants are kept because the server pins them for its own storage,
2421/// and because [`RetrieverNameWire`] — the response-side twin that really is an
2422/// integer — shares them. Reading `#[repr(u8)]` as "this is an integer on the
2423/// wire" is what previously made the Python and TypeScript SDKs send `0`/`2`
2424/// here, which the server cannot deserialize.
2425#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2426#[repr(u8)]
2427pub enum RetrieverWire {
2428    Semantic = 0,
2429    Lexical = 1,
2430    Graph = 2,
2431}
2432
2433/// Auto-routing vs explicit retriever list. Externally tagged (`"Auto"`
2434/// string or `{"Explicit": [...]}` map), matching the server.
2435#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2436pub enum RetrieverSelectionWire {
2437    Auto,
2438    Explicit(Vec<RetrieverWire>),
2439}
2440
2441/// Per-query fusion override.
2442#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2443pub struct FusionConfigWire {
2444    pub k: u32,
2445    pub semantic_weight: f32,
2446    pub lexical_weight: f32,
2447    pub graph_weight: f32,
2448}
2449
2450/// The retriever/fusion selection carried by a query. Embedded by the
2451/// `QUERY_EXPLAIN` / `QUERY_TRACE` debug ops.
2452#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2453pub struct QueryRequest {
2454    pub text: String,
2455    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
2456    pub entity_anchor: Option<WireUuid>,
2457    pub kind_filter: Vec<u8>,
2458    pub predicate_filter: Vec<String>,
2459    /// Sessions to scope the query to. `None` or empty = no restriction
2460    /// (every session in the caller's space). Mirrors
2461    /// [`RecallRequest::session_filter`].
2462    pub session_filter: Option<Vec<WireSessionId>>,
2463    pub time_filter: Option<TimeRangeWire>,
2464    pub as_of_record_time_unix_nanos: Option<u64>,
2465    pub confidence_min: Option<f32>,
2466    pub include_tombstoned: bool,
2467    pub include_superseded: bool,
2468    pub limit: u32,
2469    pub retrievers: RetrieverSelectionWire,
2470    pub fusion_config: Option<FusionConfigWire>,
2471    #[serde(with = "serde_bytes")]
2472    pub request_id: WireUuid,
2473}
2474
2475/// MATERIALIZE_PROCEDURAL (`0x0164`).
2476#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2477pub struct MaterializeProceduralRequest {
2478    #[serde(with = "serde_bytes")]
2479    pub space_id: WireUuid,
2480    pub session_filter: Option<Vec<WireSessionId>>,
2481    pub top_k: u32,
2482    pub min_confidence: f32,
2483    pub categories: Vec<String>,
2484    #[serde(with = "serde_bytes")]
2485    pub request_id: WireUuid,
2486}
2487
2488/// MATERIALIZE_PROCEDURAL_RESP (`0x01E4`).
2489#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2490pub struct MaterializeProceduralResponse {
2491    pub system_block: String,
2492    /// Each id encodes as a CBOR byte string (major type 2) inside the
2493    /// array, matching the server's uniform 16-byte id encoding.
2494    #[serde(with = "crate::wire::cbor::vec_byte_array16")]
2495    pub statement_ids: Vec<WireUuid>,
2496    pub total_candidates: u32,
2497    pub trimmed_by_budget: bool,
2498}
2499
2500// ===========================================================================
2501// SPACE registry — SPACE_CREATE / SPACE_LIST / SPACE_DELETE.
2502// ===========================================================================
2503
2504/// One space row in a [`SpaceListResponse`]. `space_id` is the
2505/// human-readable structured space string from the registry (a CBOR text
2506/// string), not the derived 16-byte storage id.
2507#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2508pub struct SpaceView {
2509    pub space_id: String,
2510    pub created_at_unix_nanos: u64,
2511    pub last_active_unix_nanos: u64,
2512    pub memory_count: u64,
2513    pub session_count: u32,
2514}
2515
2516/// SPACE_CREATE (`0x0070`). Provisions the caller's effective space
2517/// explicitly; idempotent (a create for an existing space returns the
2518/// existing row with `created = false`).
2519#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2520pub struct SpaceCreateRequest {
2521    /// Opaque caller metadata blob (quota hints, labels). `None` for a bare
2522    /// provision (omitted on the wire).
2523    #[serde(skip_serializing_if = "Option::is_none", default)]
2524    pub metadata: Option<Vec<u8>>,
2525    #[serde(with = "serde_bytes")]
2526    pub request_id: WireUuid,
2527    #[serde(skip_serializing_if = "Option::is_none", default)]
2528    pub act_as: Option<ActAs>,
2529}
2530
2531/// SPACE_CREATE_RESP (`0x00F0`). `space_id` is the human-readable structured
2532/// space string this create resolved to (a CBOR text string).
2533#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2534pub struct SpaceCreateResponse {
2535    pub space_id: String,
2536    /// `false` on an idempotent replay of an existing space.
2537    pub created: bool,
2538    pub created_at_unix_nanos: u64,
2539    pub last_active_unix_nanos: u64,
2540    pub memory_count: u64,
2541    pub session_count: u32,
2542}
2543
2544/// SPACE_LIST (`0x0071`). Lists the caller's namespace's spaces. `limit == 0`
2545/// means "no cap".
2546#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2547pub struct SpaceListRequest {
2548    pub limit: u32,
2549    #[serde(skip_serializing_if = "Option::is_none", default)]
2550    pub act_as: Option<ActAs>,
2551}
2552
2553/// SPACE_LIST_RESP (`0x00F1`).
2554#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2555pub struct SpaceListResponse {
2556    pub spaces: Vec<SpaceView>,
2557    /// `true` when the listing covers every shard. v1 lists only the
2558    /// caller-shard's spaces, so this is `false` until cross-shard
2559    /// scatter-gather lands — treat `false` as "partial".
2560    pub cross_shard_complete: bool,
2561}
2562
2563/// SPACE_DELETE (`0x0072`). GDPR erasure of the caller's effective space:
2564/// removes every row under `(namespace, space)`. Hard/immediate.
2565#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2566pub struct SpaceDeleteRequest {
2567    #[serde(with = "serde_bytes")]
2568    pub request_id: WireUuid,
2569    #[serde(skip_serializing_if = "Option::is_none", default)]
2570    pub act_as: Option<ActAs>,
2571}
2572
2573/// SPACE_DELETE_RESP (`0x00F2`). `space_id` is the human-readable structured
2574/// space string this delete targeted (a CBOR text string).
2575#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2576pub struct SpaceDeleteResponse {
2577    pub space_id: String,
2578    /// `false` when the space had no registry row.
2579    pub existed: bool,
2580    /// Number of memories tombstoned by the cascade.
2581    pub memories_forgotten: u64,
2582}
2583
2584// ===========================================================================
2585// SESSION registry — SESSION_CREATE / SESSION_LIST / SESSION_DELETE.
2586// ===========================================================================
2587
2588/// One session row in a [`SessionListResponse`].
2589#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2590pub struct SessionView {
2591    pub session_id: WireSessionId,
2592    pub created_at_unix_nanos: u64,
2593    pub last_active_unix_nanos: u64,
2594    #[serde(skip_serializing_if = "Option::is_none", default)]
2595    pub title: Option<String>,
2596    pub memory_count: u32,
2597}
2598
2599/// SESSION_CREATE (`0x0073`). Provisions a session under the caller's
2600/// effective space explicitly; idempotent.
2601#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2602pub struct SessionCreateRequest {
2603    pub session_id: WireSessionId,
2604    #[serde(skip_serializing_if = "Option::is_none", default)]
2605    pub title: Option<String>,
2606    #[serde(with = "serde_bytes")]
2607    pub request_id: WireUuid,
2608    #[serde(skip_serializing_if = "Option::is_none", default)]
2609    pub act_as: Option<ActAs>,
2610}
2611
2612/// SESSION_CREATE_RESP (`0x00F3`). `space_id` is the derived 16-byte storage
2613/// id (a CBOR byte string).
2614#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2615pub struct SessionCreateResponse {
2616    #[serde(with = "serde_bytes")]
2617    pub space_id: WireUuid,
2618    pub session_id: WireSessionId,
2619    /// `false` on an idempotent replay of an existing session.
2620    pub created: bool,
2621    pub created_at_unix_nanos: u64,
2622    pub last_active_unix_nanos: u64,
2623    pub memory_count: u32,
2624}
2625
2626/// SESSION_LIST (`0x0074`). Lists one `(namespace, space)`'s sessions
2627/// newest-first. `limit == 0` means "no cap".
2628#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2629pub struct SessionListRequest {
2630    pub limit: u32,
2631    #[serde(skip_serializing_if = "Option::is_none", default)]
2632    pub act_as: Option<ActAs>,
2633}
2634
2635/// SESSION_LIST_RESP (`0x00F4`). `space_id` is the derived 16-byte storage id
2636/// (a CBOR byte string).
2637#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2638pub struct SessionListResponse {
2639    #[serde(with = "serde_bytes")]
2640    pub space_id: WireUuid,
2641    pub sessions: Vec<SessionView>,
2642}
2643
2644/// SESSION_DELETE (`0x0075`). Removes a session's memories + graph rows.
2645/// Defaults to soft (7-day grace); `hard = true` zeroes immediately. The
2646/// default session (`session_id = 0`) is non-deletable.
2647#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2648pub struct SessionDeleteRequest {
2649    pub session_id: WireSessionId,
2650    /// `true` ⇒ hard tombstone (immediate); `false` (default) ⇒ soft.
2651    #[serde(default)]
2652    pub hard: bool,
2653    #[serde(with = "serde_bytes")]
2654    pub request_id: WireUuid,
2655    #[serde(skip_serializing_if = "Option::is_none", default)]
2656    pub act_as: Option<ActAs>,
2657}
2658
2659/// SESSION_DELETE_RESP (`0x00F5`). `space_id` is the derived 16-byte storage
2660/// id (a CBOR byte string).
2661#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2662pub struct SessionDeleteResponse {
2663    #[serde(with = "serde_bytes")]
2664    pub space_id: WireUuid,
2665    pub session_id: WireSessionId,
2666    /// `false` when the session had no registry row.
2667    pub existed: bool,
2668    /// Number of memories tombstoned by the cascade.
2669    pub memories_forgotten: u64,
2670}
2671
2672// ===========================================================================
2673// TXN_BEGIN / TXN_COMMIT / TXN_ABORT.
2674// ===========================================================================
2675
2676/// TXN_BEGIN (`0x0040`). The client mints the `txn_id`; the server binds the
2677/// transaction to it for the duration of the session.
2678#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2679pub struct TxnBeginRequest {
2680    #[serde(with = "serde_bytes")]
2681    pub txn_id: WireUuid,
2682    pub timeout_seconds: u32,
2683    /// Effective identity every write buffered in this transaction commits as.
2684    /// Delegation is fixed at begin and applies to the whole txn; `TXN_COMMIT`
2685    /// carries no `act_as` of its own. `None` (omitted on the wire) means the
2686    /// txn commits as the connection's own key-bound identity.
2687    #[serde(skip_serializing_if = "Option::is_none", default)]
2688    pub act_as: Option<ActAs>,
2689}
2690
2691/// TXN_BEGIN_RESP (`0x00C0`).
2692#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2693pub struct TxnBeginResponse {
2694    #[serde(with = "serde_bytes")]
2695    pub txn_id: WireUuid,
2696    pub timeout_seconds: u32,
2697    pub started_at_unix_nanos: u64,
2698}
2699
2700/// TXN_COMMIT (`0x0041`).
2701#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2702pub struct TxnCommitRequest {
2703    #[serde(with = "serde_bytes")]
2704    pub txn_id: WireUuid,
2705}
2706
2707/// TXN_COMMIT_RESP (`0x00C1`).
2708#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2709pub struct TxnCommitResponse {
2710    #[serde(with = "serde_bytes")]
2711    pub txn_id: WireUuid,
2712    pub committed_at_unix_nanos: u64,
2713    pub operations_applied: u32,
2714}
2715
2716/// TXN_ABORT (`0x0042`).
2717#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2718pub struct TxnAbortRequest {
2719    #[serde(with = "serde_bytes")]
2720    pub txn_id: WireUuid,
2721}
2722
2723/// TXN_ABORT_RESP (`0x00C2`).
2724#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2725pub struct TxnAbortResponse {
2726    #[serde(with = "serde_bytes")]
2727    pub txn_id: WireUuid,
2728    pub operations_discarded: u32,
2729}
2730
2731// ===========================================================================
2732// SUBSCRIBE / UNSUBSCRIBE + the subscription event union.
2733// ===========================================================================
2734
2735/// Subscription event kind. Integer discriminant on the wire (mirrors the
2736/// server's `EventType`). The four cognitive variants populate the
2737/// `SubscriptionEvent` cognitive fields; the typed-graph variants zero-fill
2738/// them and carry their body in `graph_payload`.
2739#[derive(
2740    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
2741)]
2742#[repr(u8)]
2743pub enum EventType {
2744    // Cognitive events.
2745    Encoded = 0,
2746    Forgotten = 1,
2747    Reclaimed = 2,
2748    KindChanged = 3,
2749
2750    // Typed-graph events (graph_payload populated).
2751    EntityCreated = 16,
2752    EntityUpdated = 17,
2753    EntityRenamed = 18,
2754    EntityMerged = 19,
2755    EntityUnmerged = 20,
2756    EntityTombstoned = 21,
2757    StatementCreated = 22,
2758    StatementSuperseded = 23,
2759    StatementTombstoned = 24,
2760    RelationCreated = 25,
2761    RelationSuperseded = 26,
2762    StageCompleted = 27,
2763    SchemaUpdated = 29,
2764    RelationTombstoned = 30,
2765
2766    // Unified-edge change feed (edge_payload populated).
2767    EdgeAdded = 31,
2768    EdgeRemoved = 32,
2769    EdgeSuperseded = 33,
2770}
2771
2772/// Verdict of a completed background stage. Integer discriminant.
2773#[derive(
2774    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
2775)]
2776#[repr(u8)]
2777pub enum StageOutcome {
2778    Ok = 0,
2779    Empty = 1,
2780    Failed = 2,
2781}
2782
2783/// Audit verdict for an extractor stage. Integer discriminant.
2784#[derive(
2785    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
2786)]
2787#[repr(u8)]
2788pub enum StageAuditStatus {
2789    Succeeded = 0,
2790    PartiallyApplied = 1,
2791    Failed = 2,
2792    Skipped = 3,
2793}
2794
2795/// [`StagePayload::AutoEdge`] body: how many similarity edges the auto-edge
2796/// stage wrote for the source memory.
2797#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2798pub struct StageAutoEdgePayload {
2799    pub edges_written: u32,
2800}
2801
2802/// [`StagePayload::TemporalEdge`] body: how many temporal (followed-by) edges
2803/// the temporal-edge stage wrote.
2804#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2805pub struct StageTemporalEdgePayload {
2806    pub edges_written: u32,
2807}
2808
2809/// [`StagePayload::Extractor`] body: the typed-graph rows the extractor stage
2810/// produced (entity / statement / relation counts) plus its audit verdict.
2811#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2812pub struct StageExtractorPayload {
2813    pub entity_count: u32,
2814    pub statement_count: u32,
2815    pub relation_count: u32,
2816    pub audit_status: StageAuditStatus,
2817    /// Populated only when `audit_status == Failed`; empty otherwise.
2818    pub error_message: String,
2819}
2820
2821/// [`StagePayload::Hype`] body: hypothetical questions generated for the new
2822/// memory (write-time HyPE), embedded and inserted into the memory HNSW
2823/// index.
2824#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2825pub struct StageHypePayload {
2826    /// How many hypothetical questions were embedded, persisted, and
2827    /// inserted into the memory HNSW index.
2828    pub questions_written: u32,
2829    /// LLM micro-USD spent generating the questions (`0` on a cache hit).
2830    pub cost_micro_usd: u64,
2831}
2832
2833/// Per-stage detail sidecar on `StageCompleted` events. Externally-tagged
2834/// enum (CBOR map keyed by the variant name), matching the server.
2835#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2836pub enum StagePayload {
2837    AutoEdge(StageAutoEdgePayload),
2838    TemporalEdge(StageTemporalEdgePayload),
2839    Extractor(StageExtractorPayload),
2840    Hype(StageHypePayload),
2841}
2842
2843/// Vector-similarity subscription filter.
2844#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
2845pub struct SimilarityFilter {
2846    pub reference_memory_id: WireMemoryId,
2847    pub threshold: f32,
2848}
2849
2850/// Selects which events a subscription receives. All fields `None`/empty
2851/// means "everything routed to this shard".
2852#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2853pub struct SubscriptionFilter {
2854    pub session_filter: Option<Vec<WireSessionId>>,
2855    pub kinds: Option<Vec<MemoryKindWire>>,
2856    pub similar_to: Option<SimilarityFilter>,
2857    /// Subset of space ids whose events the subscriber wants. `None` or
2858    /// empty = all spaces on the shard.
2859    #[serde(with = "crate::wire::cbor::opt_vec_byte_array16")]
2860    pub spaces: Option<Vec<WireUuid>>,
2861    /// Subset of memory ids whose events the subscriber wants. `None` or
2862    /// empty = all memories. Lets a client scope a subscription to a single
2863    /// in-flight write (e.g. to watch that write's async derivation stages
2864    /// complete) without seeing unrelated traffic on a busy shard.
2865    pub memory_ids: Option<Vec<WireMemoryId>>,
2866}
2867
2868/// SUBSCRIBE (`0x0030`). Opens a long-lived push stream; the server emits
2869/// `SUBSCRIBE_EVENT` (`0x00B0`) frames on the subscription's stream id until
2870/// the client sends `UNSUBSCRIBE` (`0x0031`).
2871#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2872pub struct SubscribeRequest {
2873    pub filter: SubscriptionFilter,
2874    pub include_history: bool,
2875    pub from_lsn: Option<u64>,
2876    pub max_inflight: u32,
2877    /// Effective identity this subscription runs as, on behalf of the
2878    /// authenticated connection principal. `None` (the common case, and
2879    /// omitted on the wire) means the op runs as the connection's own
2880    /// key-bound identity.
2881    #[serde(skip_serializing_if = "Option::is_none", default)]
2882    pub act_as: Option<ActAs>,
2883}
2884
2885/// Side-channel payload on an `EdgeAdded` / `EdgeRemoved` / `EdgeSuperseded`
2886/// event. Covers both memory-graph edges and typed-graph relations.
2887#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2888pub struct EdgeEventPayload {
2889    /// `0` = Memory, `1` = Entity.
2890    pub from_kind: u8,
2891    #[serde(with = "serde_bytes")]
2892    pub from_id: WireUuid,
2893    pub to_kind: u8,
2894    #[serde(with = "serde_bytes")]
2895    pub to_id: WireUuid,
2896    /// `0` = Builtin memory-graph kind, `1` = Mentions, `2` = Typed relation.
2897    pub edge_kind_tag: u8,
2898    pub edge_kind_byte: u8,
2899    /// `Some(_)` for typed-relation events.
2900    pub relation_type_id: Option<u32>,
2901    pub weight: f32,
2902    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
2903    pub relation_id: Option<WireUuid>,
2904    #[serde(with = "crate::wire::cbor::opt_byte_array16")]
2905    pub superseded_relation_id: Option<WireUuid>,
2906    /// `0` = EXPLICIT, `1` = AUTO_DERIVED.
2907    pub origin: u8,
2908}
2909
2910/// SUBSCRIBE_EVENT (`0x00B0`) — a single server-push event frame.
2911#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2912pub struct SubscriptionEvent {
2913    pub event_type: EventType,
2914    pub memory_id: WireMemoryId,
2915    pub session_id: WireSessionId,
2916    pub text: String,
2917    pub kind: MemoryKindWire,
2918    pub salience: f32,
2919    pub timestamp_unix_nanos: u64,
2920    pub lsn: u64,
2921    /// `None` for cognitive events; `Some(_)` for typed-graph events.
2922    pub graph_payload: Option<GraphEventPayload>,
2923    /// `Some(_)` for `EdgeAdded` / `EdgeRemoved` / `EdgeSuperseded`.
2924    pub edge_payload: Option<EdgeEventPayload>,
2925    /// `Some(_)` when `event_type == StageCompleted`.
2926    pub stage_kind: Option<StageKind>,
2927    pub stage_outcome: Option<StageOutcome>,
2928    pub stage_payload: Option<StagePayload>,
2929}
2930
2931/// UNSUBSCRIBE (`0x0031`). `target_stream_id` is the stream id of the
2932/// subscription to tear down.
2933#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2934pub struct UnsubscribeRequest {
2935    pub target_stream_id: u32,
2936}
2937
2938/// UNSUBSCRIBE_RESP (`0x00B1`).
2939#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2940pub struct UnsubscribeResponse {
2941    pub target_stream_id: u32,
2942    pub final_lsn: u64,
2943}
2944
2945// ---------------------------------------------------------------------------
2946// Typed-graph subscription event bodies.
2947// ---------------------------------------------------------------------------
2948
2949/// Typed payload for a typed-graph SUBSCRIBE event. Externally-tagged enum
2950/// (CBOR map keyed by the variant name), matching the server. Discriminated
2951/// at the application level by [`SubscriptionEvent::event_type`].
2952#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2953pub enum GraphEventPayload {
2954    EntityCreated(EntityCreatedEvent),
2955    EntityUpdated(EntityUpdatedEvent),
2956    EntityRenamed(EntityRenamedEvent),
2957    EntityMerged(EntityMergedEvent),
2958    EntityUnmerged(EntityUnmergedEvent),
2959    EntityTombstoned(EntityTombstonedEvent),
2960    StatementCreated(StatementCreatedEvent),
2961    StatementSuperseded(StatementSupersededEvent),
2962    StatementTombstoned(StatementTombstonedEvent),
2963    RelationCreated(RelationCreatedEvent),
2964    RelationSuperseded(RelationSupersededEvent),
2965    RelationTombstoned(RelationTombstonedEvent),
2966    SchemaUpdated(SchemaUpdatedEvent),
2967}
2968
2969/// [`GraphEventPayload::EntityCreated`] body: a new typed entity was created.
2970#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2971pub struct EntityCreatedEvent {
2972    #[serde(with = "serde_bytes")]
2973    pub entity_id: WireUuid,
2974    pub entity_type_id: u32,
2975    pub canonical_name: String,
2976}
2977
2978/// [`GraphEventPayload::EntityUpdated`] body: an entity's attributes changed;
2979/// `embedding_version_changed` flags whether its vector was re-embedded.
2980#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2981pub struct EntityUpdatedEvent {
2982    #[serde(with = "serde_bytes")]
2983    pub entity_id: WireUuid,
2984    pub entity_type_id: u32,
2985    pub canonical_name: String,
2986    pub embedding_version_changed: bool,
2987}
2988
2989/// [`GraphEventPayload::EntityRenamed`] body: an entity's canonical name
2990/// changed; `old_moved_to_alias` says whether the old name was kept as an alias.
2991#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2992pub struct EntityRenamedEvent {
2993    #[serde(with = "serde_bytes")]
2994    pub entity_id: WireUuid,
2995    pub old_canonical_name: String,
2996    pub new_canonical_name: String,
2997    pub old_moved_to_alias: bool,
2998}
2999
3000/// [`GraphEventPayload::EntityMerged`] body: two entities were unified into
3001/// `survivor`; reports how many statements and relations were rerouted.
3002#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
3003pub struct EntityMergedEvent {
3004    #[serde(with = "serde_bytes")]
3005    pub survivor: WireUuid,
3006    #[serde(with = "serde_bytes")]
3007    pub merged: WireUuid,
3008    #[serde(with = "serde_bytes")]
3009    pub audit_id: WireUuid,
3010    pub confidence: f32,
3011    pub statements_rerouted: u32,
3012    pub relations_rerouted: u32,
3013}
3014
3015/// [`GraphEventPayload::EntityUnmerged`] body: a prior merge was reversed,
3016/// splitting `restored_entity_id` back out of `from_survivor`.
3017#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
3018pub struct EntityUnmergedEvent {
3019    #[serde(with = "serde_bytes")]
3020    pub restored_entity_id: WireUuid,
3021    #[serde(with = "serde_bytes")]
3022    pub from_survivor: WireUuid,
3023    #[serde(with = "serde_bytes")]
3024    pub audit_id: WireUuid,
3025}
3026
3027/// [`GraphEventPayload::EntityTombstoned`] body: an entity was soft-deleted, with
3028/// the operator-supplied reason.
3029#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3030pub struct EntityTombstonedEvent {
3031    #[serde(with = "serde_bytes")]
3032    pub entity_id: WireUuid,
3033    pub reason: String,
3034}
3035
3036/// [`GraphEventPayload::StatementCreated`] body: a new typed statement was
3037/// asserted about `subject`.
3038#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3039pub struct StatementCreatedEvent {
3040    #[serde(with = "serde_bytes")]
3041    pub statement_id: WireUuid,
3042    /// 1=Fact, 2=Preference, 3=Event.
3043    pub kind: u8,
3044    #[serde(with = "serde_bytes")]
3045    pub subject: WireUuid,
3046    pub predicate: String,
3047    pub confidence: f32,
3048}
3049
3050/// [`GraphEventPayload::StatementSuperseded`] body: `new_statement_id` replaced
3051/// `old_statement_id`; `chain_root` identifies the supersession chain's origin.
3052#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
3053pub struct StatementSupersededEvent {
3054    #[serde(with = "serde_bytes")]
3055    pub old_statement_id: WireUuid,
3056    #[serde(with = "serde_bytes")]
3057    pub new_statement_id: WireUuid,
3058    #[serde(with = "serde_bytes")]
3059    pub chain_root: WireUuid,
3060}
3061
3062/// [`GraphEventPayload::StatementTombstoned`] body: a statement was soft-deleted,
3063/// with the operator-supplied reason.
3064#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3065pub struct StatementTombstonedEvent {
3066    #[serde(with = "serde_bytes")]
3067    pub statement_id: WireUuid,
3068    pub reason: String,
3069}
3070
3071/// [`GraphEventPayload::RelationCreated`] body: a new typed relation was created
3072/// from `from` to `to`.
3073#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3074pub struct RelationCreatedEvent {
3075    #[serde(with = "serde_bytes")]
3076    pub relation_id: WireUuid,
3077    pub relation_type: String,
3078    #[serde(with = "serde_bytes")]
3079    pub from: WireUuid,
3080    #[serde(with = "serde_bytes")]
3081    pub to: WireUuid,
3082}
3083
3084/// [`GraphEventPayload::RelationSuperseded`] body: `new_relation_id` replaced
3085/// `old_relation_id`.
3086#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
3087pub struct RelationSupersededEvent {
3088    #[serde(with = "serde_bytes")]
3089    pub old_relation_id: WireUuid,
3090    #[serde(with = "serde_bytes")]
3091    pub new_relation_id: WireUuid,
3092}
3093
3094/// [`GraphEventPayload::RelationTombstoned`] body: a relation was soft-deleted,
3095/// with the operator-supplied reason.
3096#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3097pub struct RelationTombstonedEvent {
3098    #[serde(with = "serde_bytes")]
3099    pub relation_id: WireUuid,
3100    pub reason: String,
3101}
3102
3103/// [`GraphEventPayload::SchemaUpdated`] body: a namespace's schema advanced from
3104/// `from_version` to `to_version` (via SCHEMA_UPLOAD or SCHEMA_REPLACE).
3105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3106pub struct SchemaUpdatedEvent {
3107    pub namespace: String,
3108    pub from_version: u32,
3109    pub to_version: u32,
3110    /// Always `true` in v1.
3111    pub backward_compatible: bool,
3112}
3113
3114// ===========================================================================
3115// GET_CAPABILITIES.
3116// ===========================================================================
3117
3118/// GET_CAPABILITIES (`0x0032`). Empty request — capabilities are
3119/// server-side state, so the client has nothing to send. Kept as a
3120/// struct (not a unit type) so the encoding matches every other request
3121/// body (a CBOR map, here empty).
3122#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
3123pub struct GetCapabilitiesRequest {
3124    /// Run as a tenant on a shared-pool connection: the server then filters
3125    /// `schema_namespaces` to the effective namespace. Absent → `0xA0`.
3126    #[serde(skip_serializing_if = "Option::is_none", default)]
3127    pub act_as: Option<ActAs>,
3128}
3129
3130/// Capability snapshot returned by the server.
3131#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3132pub struct Capabilities {
3133    /// True when the cross-encoder reranker is loaded on this shard.
3134    pub rerank: bool,
3135    /// True when the LLM extractor tier is enabled.
3136    pub llm_extractor: bool,
3137    /// True when the classifier (GLiNER) extractor tier is enabled.
3138    pub classifier_extractor: bool,
3139    /// True when the pattern extractor tier is enabled.
3140    pub pattern_extractor: bool,
3141    /// User schema namespaces currently active on the shard (excludes the
3142    /// always-on `brain` system namespace).
3143    pub schema_namespaces: Vec<String>,
3144    /// Embedding vector dimensionality the shard's embedder produces.
3145    pub vector_dim: u16,
3146}
3147
3148/// GET_CAPABILITIES_RESP (`0x00B2`).
3149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3150pub struct GetCapabilitiesResponse {
3151    pub capabilities: Capabilities,
3152}
3153
3154// ===========================================================================
3155// EXTRACTOR_LIST.
3156// ===========================================================================
3157
3158/// EXTRACTOR_LIST (`0x0124`). Empty request — extraction is always-on and
3159/// takes no arguments, so every registered extractor is returned. Kept as a
3160/// struct (not a unit type) so the encoding matches every other request body
3161/// (a CBOR map, here empty).
3162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
3163pub struct ExtractorListRequest {}
3164
3165/// One registered extractor row in [`ExtractorListResponseFrame`].
3166#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3167pub struct ExtractorListItem {
3168    pub extractor_id: u32,
3169    pub namespace: String,
3170    pub name: String,
3171    /// `0`=pattern, `1`=classifier, `2`=llm.
3172    pub kind: u8,
3173    pub schema_version: u32,
3174    pub created_at_unix_nanos: u64,
3175}
3176
3177/// EXTRACTOR_LIST_RESP (`0x01A4`). Single-frame snapshot of the always-on
3178/// extractor registry.
3179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3180pub struct ExtractorListResponseFrame {
3181    pub items: Vec<ExtractorListItem>,
3182    pub total: u32,
3183    pub is_final: bool,
3184}
3185
3186// ===========================================================================
3187// ENTITY_GET / ENTITY_LIST.
3188// ===========================================================================
3189
3190/// Read-side view of an entity. Mirrors the server's `EntityView`.
3191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3192pub struct EntityView {
3193    #[serde(with = "serde_bytes")]
3194    pub entity_id: WireUuid,
3195    pub entity_type_id: u32,
3196    pub canonical_name: String,
3197    pub normalized_name: String,
3198    pub aliases: Vec<String>,
3199    pub attributes_blob: Vec<u8>,
3200    pub mention_count: u32,
3201    pub created_at_unix_nanos: u64,
3202    pub updated_at_unix_nanos: u64,
3203    /// `[0; 16]` when not merged; consumers treat all-zero as None.
3204    #[serde(with = "serde_bytes")]
3205    pub merged_into: WireUuid,
3206    pub embedding_version: u32,
3207    pub flags: u32,
3208}
3209
3210/// ENTITY_GET (`0x0131`).
3211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3212pub struct EntityGetRequest {
3213    #[serde(with = "serde_bytes")]
3214    pub entity_id: WireUuid,
3215    /// Effective identity this get runs as. `None` (omitted on the wire) means
3216    /// the op runs as the connection's own key-bound identity. Honored only when
3217    /// the connection principal holds the `ACT_AS` grant and the target
3218    /// namespace is in its `may_act` allowlist.
3219    #[serde(skip_serializing_if = "Option::is_none", default)]
3220    pub act_as: Option<ActAs>,
3221}
3222
3223/// ENTITY_GET_RESP (`0x01B1`).
3224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3225pub struct EntityGetResponse {
3226    pub entity: EntityView,
3227    /// Merge audit trail: the chain of redirect ids the GET walked through to
3228    /// reach `entity` (the surviving row), in order, EXCLUDING the survivor
3229    /// itself. Empty on a direct hit (the requested id was live). For
3230    /// `A → B → C`, `ENTITY_GET(A)` returns `entity = C` and
3231    /// `resolved_from = [A, B]`. Each id encodes as a CBOR array of 16 ints
3232    /// (plain `[u8; 16]`), matching the server's default serde encoding for
3233    /// this field.
3234    pub resolved_from: Vec<WireUuid>,
3235}
3236
3237/// ENTITY_LIST (`0x0137`). Empty/zero fields mean "no filter".
3238#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3239pub struct EntityListRequest {
3240    /// `0` = no filter; otherwise an entity type id.
3241    pub entity_type_id: u32,
3242    /// Empty = no filter.
3243    pub name_prefix: String,
3244    pub mention_count_min: u32,
3245    pub include_tombstoned: bool,
3246    pub include_merged: bool,
3247    /// 1..=1000.
3248    pub limit: u32,
3249    /// Empty on first page; opaque continuation token otherwise.
3250    pub cursor: Vec<u8>,
3251    /// Effective identity this list runs as. `None` (omitted on the wire) means
3252    /// the op runs as the connection's own key-bound identity. Honored only when
3253    /// the connection principal holds the `ACT_AS` grant and the target
3254    /// namespace is in its `may_act` allowlist.
3255    #[serde(skip_serializing_if = "Option::is_none", default)]
3256    pub act_as: Option<ActAs>,
3257}
3258
3259/// One entity in an ENTITY_LIST response batch.
3260#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3261pub struct EntityListItem {
3262    pub entity: EntityView,
3263}
3264
3265/// One streaming ENTITY_LIST_RESP frame (`0x01B7`). The last frame carries
3266/// `is_final = true`. Mirrors `RecallResponseFrame`'s streaming shape.
3267#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3268pub struct EntityListResponseFrame {
3269    pub items: Vec<EntityListItem>,
3270    /// Empty `next_cursor` on the final frame means "exhausted"; non-empty
3271    /// means "more pages available, resume with this".
3272    pub next_cursor: Vec<u8>,
3273    pub cumulative_count: u32,
3274    pub is_final: bool,
3275}
3276
3277// ===========================================================================
3278// ENTITY_RESOLVE.
3279// ===========================================================================
3280
3281/// ENTITY_RESOLVE (`0x0136`). Resolves a candidate name to an existing entity
3282/// (and optionally creates one). The server currently requires
3283/// `entity_type_hint != 0` and resolves by exact canonical name (tier 1).
3284#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3285pub struct EntityResolveRequest {
3286    pub candidate_name: String,
3287    pub resolution_context: String,
3288    /// `0` = no hint; otherwise an entity type id.
3289    pub entity_type_hint: u32,
3290    pub allow_create: bool,
3291    #[serde(with = "serde_bytes")]
3292    pub request_id: WireUuid,
3293    /// Effective identity this resolve runs as. `None` (omitted on the wire)
3294    /// means the op runs as the connection's own key-bound identity. Honored
3295    /// only when the connection principal holds the `ACT_AS` grant and the
3296    /// target namespace is in its `may_act` allowlist.
3297    #[serde(skip_serializing_if = "Option::is_none", default)]
3298    pub act_as: Option<ActAs>,
3299}
3300
3301/// Resolution outcome. Integer discriminant on the wire (`1`-based, mirroring
3302/// the server's `ResolutionOutcomeWire`).
3303#[derive(
3304    Clone, Copy, Debug, Eq, PartialEq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
3305)]
3306#[repr(u8)]
3307pub enum ResolutionOutcomeWire {
3308    Resolved = 1,
3309    Created = 2,
3310    Ambiguous = 3,
3311    NotFound = 4,
3312}
3313
3314/// ENTITY_RESOLVE_RESP (`0x01B6`).
3315#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3316pub struct EntityResolveResponse {
3317    pub outcome: ResolutionOutcomeWire,
3318    /// Which tier resolved (1..=5; 0 if unresolved).
3319    pub tier: u8,
3320    pub confidence: f32,
3321    /// Populated when `outcome == Resolved | Created` (single id);
3322    /// `[0; 16]` for `Ambiguous | NotFound`.
3323    #[serde(with = "serde_bytes")]
3324    pub resolved_entity: WireUuid,
3325    /// Populated when `outcome == Ambiguous`; ranked by score.
3326    #[serde(with = "crate::wire::cbor::vec_byte_array16")]
3327    pub candidate_ids: Vec<WireUuid>,
3328    /// `[0; 16]` unless an ambiguity audit was written.
3329    #[serde(with = "serde_bytes")]
3330    pub audit_id: WireUuid,
3331}
3332
3333// ===========================================================================
3334// STATEMENT_GET / STATEMENT_LIST.
3335// ===========================================================================
3336
3337/// Read-side projection of a statement. Mirrors the server's
3338/// `StatementView`. Optional value-side fields collapse to sentinel zero
3339/// (`[0; 16]` for ids, `0` for nanos).
3340#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3341pub struct StatementView {
3342    #[serde(with = "serde_bytes")]
3343    pub statement_id: WireUuid,
3344    pub kind: StatementKindWire,
3345    #[serde(with = "serde_bytes")]
3346    pub subject: WireUuid,
3347    #[serde(with = "serde_bytes")]
3348    pub subject_pending_audit_id: WireUuid,
3349    pub predicate: String,
3350    pub object: StatementObjectWire,
3351    pub confidence: f32,
3352    pub evidence: EvidenceRefWire,
3353    pub extractor_id: u32,
3354    pub extracted_at_unix_nanos: u64,
3355    pub schema_version: u32,
3356    pub valid_from_unix_nanos: u64,
3357    pub valid_to_unix_nanos: u64,
3358    pub event_at_unix_nanos: u64,
3359    pub version: u32,
3360    #[serde(with = "serde_bytes")]
3361    pub superseded_by: WireUuid,
3362    #[serde(with = "serde_bytes")]
3363    pub supersedes: WireUuid,
3364    #[serde(with = "serde_bytes")]
3365    pub chain_root: WireUuid,
3366    pub tombstoned: bool,
3367    pub tombstoned_at_unix_nanos: u64,
3368    pub tombstone_reason: u8,
3369    pub flags: u32,
3370    /// `true` iff this statement is stateful.
3371    pub is_stateful: bool,
3372}
3373
3374/// STATEMENT_GET (`0x0141`).
3375#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3376pub struct StatementGetRequest {
3377    #[serde(with = "serde_bytes")]
3378    pub statement_id: WireUuid,
3379    /// If `true` and the row is superseded, the server returns the current
3380    /// statement in the chain (with `returned_via_supersession = true`).
3381    pub follow_supersession: bool,
3382    /// Effective identity this get runs as. `None` (omitted on the wire) means
3383    /// the op runs as the connection's own key-bound identity. Honored only when
3384    /// the connection principal holds the `ACT_AS` grant and the target
3385    /// namespace is in its `may_act` allowlist.
3386    #[serde(skip_serializing_if = "Option::is_none", default)]
3387    pub act_as: Option<ActAs>,
3388}
3389
3390/// STATEMENT_GET_RESP (`0x01C1`).
3391#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3392pub struct StatementGetResponse {
3393    pub statement: StatementView,
3394    /// `true` iff `follow_supersession` redirected to a later chain entry.
3395    pub returned_via_supersession: bool,
3396}
3397
3398/// STATEMENT_LIST (`0x0146`). Empty/zero fields mean "no filter".
3399/// `kind == 0` = no kind filter; otherwise `1 = Fact / 2 = Preference /
3400/// 3 = Event`.
3401#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3402pub struct StatementListRequest {
3403    #[serde(with = "serde_bytes")]
3404    pub subject: WireUuid,
3405    pub predicate: String,
3406    pub kind: u8,
3407    pub min_confidence: f32,
3408    pub time_range_start_unix_nanos: u64,
3409    pub time_range_end_unix_nanos: u64,
3410    pub only_current: bool,
3411    pub include_tombstoned: bool,
3412    pub limit: u32,
3413    pub cursor: Vec<u8>,
3414    /// Effective identity this list runs as. `None` (omitted on the wire) means
3415    /// the op runs as the connection's own key-bound identity. Honored only when
3416    /// the connection principal holds the `ACT_AS` grant and the target
3417    /// namespace is in its `may_act` allowlist.
3418    #[serde(skip_serializing_if = "Option::is_none", default)]
3419    pub act_as: Option<ActAs>,
3420}
3421
3422/// One streaming STATEMENT_LIST_RESP frame (`0x01C6`).
3423#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3424pub struct StatementListResponseFrame {
3425    pub items: Vec<StatementView>,
3426    pub next_cursor: Vec<u8>,
3427    pub cumulative_count: u32,
3428    pub is_final: bool,
3429}
3430
3431// ===========================================================================
3432// RELATION_LIST_FROM / RELATION_LIST_TO.
3433// ===========================================================================
3434
3435/// Read-side projection of a relation. Mirrors the server's
3436/// `RelationView`. `flags` bit 0 = `is_symmetric`.
3437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3438pub struct RelationView {
3439    #[serde(with = "serde_bytes")]
3440    pub relation_id: WireUuid,
3441    #[serde(with = "serde_bytes")]
3442    pub chain_root: WireUuid,
3443    pub relation_type: String,
3444    #[serde(with = "serde_bytes")]
3445    pub from_entity: WireUuid,
3446    #[serde(with = "serde_bytes")]
3447    pub to_entity: WireUuid,
3448    pub properties_blob: Vec<u8>,
3449    pub evidence: EvidenceRefWire,
3450    pub extractor_id: u32,
3451    pub extracted_at_unix_nanos: u64,
3452    pub confidence: f32,
3453    pub valid_from_unix_nanos: u64,
3454    pub valid_to_unix_nanos: u64,
3455    pub version: u32,
3456    #[serde(with = "serde_bytes")]
3457    pub superseded_by: WireUuid,
3458    #[serde(with = "serde_bytes")]
3459    pub supersedes: WireUuid,
3460    pub tombstoned: bool,
3461    pub tombstoned_at_unix_nanos: u64,
3462    pub flags: u32,
3463}
3464
3465/// RELATION_LIST_FROM (`0x0154`). `relation_type_filter == ""` → any type;
3466/// `time_range_*_unix_nanos == 0` → no time bound.
3467#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3468pub struct RelationListFromRequest {
3469    #[serde(with = "serde_bytes")]
3470    pub from_entity: WireUuid,
3471    pub relation_type_filter: String,
3472    pub time_range_start_unix_nanos: u64,
3473    pub time_range_end_unix_nanos: u64,
3474    pub include_superseded: bool,
3475    pub include_tombstoned: bool,
3476    pub limit: u32,
3477    pub cursor: Vec<u8>,
3478    /// Effective identity this list runs as. `None` (omitted on the wire) means
3479    /// the op runs as the connection's own key-bound identity.
3480    #[serde(skip_serializing_if = "Option::is_none", default)]
3481    pub act_as: Option<ActAs>,
3482}
3483
3484/// One streaming RELATION_LIST_FROM_RESP frame (`0x01D4`).
3485#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3486pub struct RelationListFromResponseFrame {
3487    pub items: Vec<RelationView>,
3488    pub next_cursor: Vec<u8>,
3489    pub cumulative_count: u32,
3490    pub is_final: bool,
3491}
3492
3493/// RELATION_LIST_TO (`0x0155`). Identical shape to LIST_FROM but filters on
3494/// `to_entity`.
3495#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3496pub struct RelationListToRequest {
3497    #[serde(with = "serde_bytes")]
3498    pub to_entity: WireUuid,
3499    pub relation_type_filter: String,
3500    pub time_range_start_unix_nanos: u64,
3501    pub time_range_end_unix_nanos: u64,
3502    pub include_superseded: bool,
3503    pub include_tombstoned: bool,
3504    pub limit: u32,
3505    pub cursor: Vec<u8>,
3506    /// Effective identity this list runs as. `None` (omitted on the wire) means
3507    /// the op runs as the connection's own key-bound identity.
3508    #[serde(skip_serializing_if = "Option::is_none", default)]
3509    pub act_as: Option<ActAs>,
3510}
3511
3512/// One streaming RELATION_LIST_TO_RESP frame (`0x01D5`).
3513#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3514pub struct RelationListToResponseFrame {
3515    pub items: Vec<RelationView>,
3516    pub next_cursor: Vec<u8>,
3517    pub cumulative_count: u32,
3518    pub is_final: bool,
3519}
3520
3521// ===========================================================================
3522// SCHEMA_GET / SCHEMA_LIST / SCHEMA_VALIDATE.
3523// ===========================================================================
3524
3525/// SCHEMA_GET (`0x0121`). `version == 0` → active version.
3526#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3527pub struct SchemaGetRequest {
3528    pub namespace: String,
3529    pub version: u32,
3530    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
3531    #[serde(skip_serializing_if = "Option::is_none", default)]
3532    pub act_as: Option<ActAs>,
3533}
3534
3535/// SCHEMA_GET_RESP (`0x01A1`).
3536#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3537pub struct SchemaGetResponse {
3538    pub namespace: String,
3539    pub schema_version: u32,
3540    /// Verbatim DSL text if uploaded as such; empty for programmatic uploads.
3541    pub schema_document: String,
3542    /// `serde_json::to_vec(&Schema)` of the parsed AST.
3543    pub source_blob: Vec<u8>,
3544    pub uploaded_at_unix_nanos: u64,
3545    pub validator_version: u32,
3546}
3547
3548/// SCHEMA_LIST (`0x0122`). `limit == 0` → unlimited (server-capped).
3549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3550pub struct SchemaListRequest {
3551    pub namespace: String,
3552    pub limit: u32,
3553    pub cursor: Vec<u8>,
3554    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
3555    #[serde(skip_serializing_if = "Option::is_none", default)]
3556    pub act_as: Option<ActAs>,
3557}
3558
3559/// One entry in a SCHEMA_LIST response.
3560#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3561pub struct SchemaListItemWire {
3562    pub schema_version: u32,
3563    pub uploaded_at_unix_nanos: u64,
3564    pub validator_version: u32,
3565    pub has_source_text: bool,
3566}
3567
3568/// One streaming SCHEMA_LIST_RESP frame (`0x01A2`). Items are newest first.
3569#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3570pub struct SchemaListResponseFrame {
3571    pub namespace: String,
3572    pub items: Vec<SchemaListItemWire>,
3573    pub total: u32,
3574    pub next_cursor: Vec<u8>,
3575    pub is_final: bool,
3576}
3577
3578/// SCHEMA_VALIDATE (`0x0123`). Dry-run; never touches storage.
3579#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3580pub struct SchemaValidateRequest {
3581    pub schema_document: String,
3582    /// Run as a tenant on a shared-pool connection (see [`ActAs`]).
3583    #[serde(skip_serializing_if = "Option::is_none", default)]
3584    pub act_as: Option<ActAs>,
3585}
3586
3587/// SCHEMA_VALIDATE_RESP (`0x01A3`).
3588#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3589pub struct SchemaValidateResponse {
3590    /// Namespace parsed from the document; `""` if parse failed before
3591    /// reaching `namespace`.
3592    pub namespace: String,
3593    /// `current_active + 1` if validation passed; `0` otherwise.
3594    pub would_be_version: u32,
3595    pub validation_errors: Vec<SchemaValidationErrorWire>,
3596}
3597
3598// ---------------------------------------------------------------------------
3599// Keepalive (PING / PONG / SERVER_PING / CLIENT_PONG).
3600//
3601// Field names are the CBOR map keys on the wire, so they must match the
3602// server's `brain-protocol` definitions byte-for-byte.
3603// ---------------------------------------------------------------------------
3604
3605/// BYE (`0x001F`) — end the session cleanly.
3606///
3607/// A goodbye is a CBOR map, **not** an empty payload. All three SDKs used to
3608/// send zero bytes here, which the server cannot decode as a `ByeRequest`; it
3609/// tolerates it today rather than rejecting, but the corpus `req_bye` vector
3610/// pins the correct form.
3611#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
3612pub struct ByeRequest {
3613    /// Serialized unconditionally, as `null` when absent — the server declares
3614    /// this field with no serde attributes, and every other `Option` in this
3615    /// SDK mirrors the server's declaration exactly so the manifest comparison
3616    /// stays free of exceptions. A reason-less goodbye is therefore
3617    /// `{"reason": null}`, not an empty map; the server accepts either, since
3618    /// serde visits `none` for a missing `Option` field.
3619    pub reason: Option<String>,
3620}
3621
3622/// PING (`0x0010`, client→server) — RTT probe.
3623#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
3624pub struct PingRequest {
3625    pub client_timestamp_unix_nanos: u64,
3626}
3627
3628/// PONG (`0x0090`, server→client) — reply to PING, echoes the client's
3629/// timestamp and adds the server's so the client can measure RTT.
3630#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
3631pub struct PongResponse {
3632    pub client_timestamp_unix_nanos: u64,
3633    pub server_timestamp_unix_nanos: u64,
3634}
3635
3636/// SERVER_PING (`0x0091`, server→client) — the server's idle-timer
3637/// heartbeat. The client MUST answer with `CLIENT_PONG` or the server
3638/// closes the connection after its ping timeout.
3639#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
3640pub struct ServerPingResponse {
3641    pub server_timestamp_unix_nanos: u64,
3642}
3643
3644/// CLIENT_PONG (`0x0011`, client→server) — reply to `SERVER_PING`,
3645/// echoing the server's timestamp plus the client's own.
3646#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
3647pub struct ClientPongRequest {
3648    pub server_timestamp_unix_nanos: u64,
3649    pub client_timestamp_unix_nanos: u64,
3650}
3651
3652// ---------------------------------------------------------------------------
3653// Relation lifecycle + traversal — fetch one (get), revise (supersede), retire
3654// (tombstone), or walk the graph from an entity (traverse).
3655// ---------------------------------------------------------------------------
3656
3657/// RELATION_GET (`0x0151`). `follow_supersession` returns the current head of a
3658/// superseded relation's chain rather than the (retired) id asked for.
3659#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3660pub struct RelationGetRequest {
3661    #[serde(with = "serde_bytes")]
3662    pub relation_id: WireUuid,
3663    pub follow_supersession: bool,
3664    /// Effective identity this get runs as. `None` (omitted on the wire) means
3665    /// the op runs as the connection's own key-bound identity.
3666    #[serde(skip_serializing_if = "Option::is_none", default)]
3667    pub act_as: Option<ActAs>,
3668}
3669
3670/// RELATION_GET_RESP (`0x01D1`). `returned_via_supersession` flags that the
3671/// view is the chain head, not the exact id requested.
3672#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3673pub struct RelationGetResponse {
3674    pub relation: RelationView,
3675    pub returned_via_supersession: bool,
3676}
3677
3678/// RELATION_SUPERSEDE (`0x0152`). Revise a relation, keeping both on one chain.
3679#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3680pub struct RelationSupersedeRequest {
3681    #[serde(with = "serde_bytes")]
3682    pub old_relation_id: WireUuid,
3683    pub new_relation: RelationCreateRequest,
3684    #[serde(with = "serde_bytes")]
3685    pub request_id: WireUuid,
3686}
3687
3688/// RELATION_SUPERSEDE_RESP (`0x01D2`). New id + monotonic version.
3689#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
3690pub struct RelationSupersedeResponse {
3691    #[serde(with = "serde_bytes")]
3692    pub new_relation_id: WireUuid,
3693    pub version: u32,
3694}
3695
3696/// RELATION_TOMBSTONE (`0x0153`). Soft-retire a relation with a reason.
3697#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3698pub struct RelationTombstoneRequest {
3699    #[serde(with = "serde_bytes")]
3700    pub relation_id: WireUuid,
3701    pub reason: String,
3702    #[serde(with = "serde_bytes")]
3703    pub request_id: WireUuid,
3704}
3705
3706/// RELATION_TOMBSTONE_RESP (`0x01D3`).
3707#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
3708pub struct RelationTombstoneResponse {
3709    pub tombstoned_at_unix_nanos: u64,
3710}
3711
3712/// One hop in a traversal path: the relation edge crossed and its endpoints.
3713#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3714pub struct TraversalStepWire {
3715    #[serde(with = "serde_bytes")]
3716    pub relation_id: WireUuid,
3717    #[serde(with = "serde_bytes")]
3718    pub from: WireUuid,
3719    #[serde(with = "serde_bytes")]
3720    pub to: WireUuid,
3721    pub relation_type: String,
3722    pub depth: u32,
3723}
3724
3725/// One path the traversal found, as an ordered list of steps.
3726#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3727pub struct TraversalPathWire {
3728    pub steps: Vec<TraversalStepWire>,
3729}
3730
3731/// RELATION_TRAVERSE (`0x0156`). Multi-hop walk of the relation graph from
3732/// `start_entity`. `direction` is outgoing/incoming/both; the bounds cap the
3733/// search; `time_at_unix_nanos` walks the graph as it stood at a record time.
3734#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3735pub struct RelationTraverseRequest {
3736    #[serde(with = "serde_bytes")]
3737    pub start_entity: WireUuid,
3738    pub relation_types: Vec<String>,
3739    pub direction: u8,
3740    pub max_depth: u32,
3741    pub max_nodes: u32,
3742    pub time_at_unix_nanos: u64,
3743    pub include_superseded: bool,
3744    #[serde(with = "serde_bytes")]
3745    pub request_id: WireUuid,
3746    /// Effective identity this traversal runs as. `None` (omitted on the wire)
3747    /// means the op runs as the connection's own key-bound identity. Honored
3748    /// only when the connection principal holds the `ACT_AS` grant and the
3749    /// target namespace is in its `may_act` allowlist.
3750    #[serde(skip_serializing_if = "Option::is_none", default)]
3751    pub act_as: Option<ActAs>,
3752}
3753
3754/// RELATION_TRAVERSE_RESP (`0x01D6`), one streamed frame. `is_final` marks the
3755/// last; `truncated` flags a bound was hit before the graph was exhausted.
3756#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3757pub struct RelationTraverseResponseFrame {
3758    pub paths: Vec<TraversalPathWire>,
3759    pub total_paths: u32,
3760    pub truncated: bool,
3761    pub is_final: bool,
3762}
3763
3764// ---------------------------------------------------------------------------
3765// Query introspection debug surface — the plan (explain) and execution trace.
3766// ---------------------------------------------------------------------------
3767
3768/// QUERY_EXPLAIN (`0x0161`). Ask for the plan of a query without running it.
3769#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3770pub struct QueryExplainRequest {
3771    pub query: QueryRequest,
3772}
3773
3774/// QUERY_EXPLAIN_RESP (`0x01E1`). The plan plus an estimated cost.
3775#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3776pub struct QueryExplainResponse {
3777    pub plan_text: String,
3778    pub estimated_cost_ms: f32,
3779}
3780
3781/// QUERY_TRACE (`0x0162`). Run a query and return its per-stage execution trace.
3782#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3783pub struct QueryTraceRequest {
3784    pub query: QueryRequest,
3785}
3786
3787/// QUERY_TRACE_RESP (`0x01E2`). The trace plus total latency.
3788#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
3789pub struct QueryTraceResponse {
3790    pub trace_text: String,
3791    pub total_latency_ms: f64,
3792}
3793
3794#[cfg(test)]
3795mod error_code_forward_compat_tests {
3796    use super::*;
3797
3798    #[test]
3799    fn known_codes_round_trip_through_from_raw() {
3800        assert_eq!(
3801            ErrorCodeWire::from_raw(0x0033),
3802            ErrorCodeWire::ActAsDenied,
3803            "an existing code must still decode to its own variant"
3804        );
3805        assert_eq!(
3806            ErrorCodeWire::from_raw(0x0066),
3807            ErrorCodeWire::LlmCredentialMissing
3808        );
3809        assert_eq!(
3810            ErrorCodeWire::from_raw(0x0095),
3811            ErrorCodeWire::LlmCredentialUnavailable
3812        );
3813    }
3814
3815    #[test]
3816    fn an_unrecognised_code_decodes_to_unknown_instead_of_failing() {
3817        // The defect this closes: `Deserialize_repr` with no catch-all made a
3818        // single unrecognised code fail the WHOLE ErrorResponse, so a client
3819        // talking to a newer server reported a parse error instead of the
3820        // server's actual error. Python and TypeScript both model `code` as a
3821        // plain integer and never had this problem.
3822        assert_eq!(ErrorCodeWire::from_raw(0xDEAD), ErrorCodeWire::Unknown);
3823        assert_eq!(ErrorCodeWire::from_raw(0x7FFF), ErrorCodeWire::Unknown);
3824    }
3825
3826    #[test]
3827    fn an_error_response_carrying_an_unknown_code_still_decodes() {
3828        // End-to-end: the frame must remain readable, and `category` — the
3829        // field the wire contract guarantees is always meaningful — must
3830        // survive so the client can still decide whether to retry.
3831        let json = serde_json::json!({
3832            "code": 0xDEAD_u16,
3833            "category": 8,
3834            "message": "from a newer server",
3835            "details": null,
3836            "retry_after_ms": null,
3837        });
3838        let decoded: ErrorResponse =
3839            serde_json::from_value(json).expect("an unknown code must not break the frame");
3840        assert!(decoded.code_is_unknown());
3841        assert_eq!(decoded.category, ErrorCategoryWire::Unavailable);
3842        assert_eq!(decoded.message, "from a newer server");
3843    }
3844}