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