1use crate::blob::{BlobAddress, BlobDescriptor};
26use crate::entity::{
27 Attempt, AttentionItem, AuthorityGrant, Command, DispatchNode, EngineSession, Evidence, Gate,
28 IngestedRecord, Lease, Message, Receipt, Task, Worktree,
29};
30use crate::envelope::{CommandEnvelope, EventEnvelope, JsonValue};
31use crate::ids::{
32 BlobUploadId, ByteCount, CommandId, EventCount, EventId, RequestId, Seq, WriterEpoch,
33};
34use crate::inherited::OrchestratorCheckpoint;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum ProtocolVersion {
47 V1,
48}
49
50impl ProtocolVersion {
51 pub const fn as_u32(self) -> u32 {
52 match self {
53 Self::V1 => 1,
54 }
55 }
56
57 pub const fn from_u32(value: u32) -> Option<Self> {
58 match value {
59 1 => Some(Self::V1),
60 _ => None,
61 }
62 }
63}
64
65impl std::fmt::Display for ProtocolVersion {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "{}", self.as_u32())
68 }
69}
70
71impl serde::Serialize for ProtocolVersion {
72 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
73 s.serialize_u32(self.as_u32())
74 }
75}
76
77impl<'de> serde::Deserialize<'de> for ProtocolVersion {
78 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
79 let raw = u32::deserialize(d)?;
80 Self::from_u32(raw).ok_or_else(|| {
81 serde::de::Error::custom(format!("unsupported protocol major version {raw}"))
82 })
83 }
84}
85
86impl specta::Type for ProtocolVersion {
87 fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
88 <u32 as specta::Type>::definition(types)
89 }
90}
91
92pub const PROTOCOL_MINOR: u32 = 0;
96
97pub const CONTRACT_VERSION: u32 = 1;
102
103pub const FRAME_LENGTH_PREFIX_BYTES: usize = 4;
105
106pub const FRAME_BODY_MIN_BYTES: u32 = 1;
109
110pub const FRAME_BODY_MAX_BYTES: u32 = 4 * 1024 * 1024;
112
113pub const HELLO_MAX_BYTES: u32 = 64 * 1024;
115
116pub const HELLO_DEADLINE_SECS: u64 = 5;
118
119pub const CONNECTION_INGRESS_BYTES_PER_WINDOW: usize = 8 * 1024 * 1024;
128
129pub const CONNECTION_EGRESS_BYTES_PER_WINDOW: usize = 8 * 1024 * 1024;
131
132pub const CONNECTION_BUDGET_WINDOW_SECS: u64 = 1;
140
141pub const SLOW_CONSUMER_TIMEOUT_SECS: u64 = 30;
144
145pub const SUBSCRIPTION_POLL_SECS: u64 = 5;
154
155pub const MAX_SUBSCRIPTIONS_PER_CONNECTION: usize = 8;
162
163pub const MAX_CAPABILITIES: usize = 64;
165
166pub const CAPABILITY_NAME_MAX_BYTES: usize = 64;
168
169const _: () = {
174 assert!(HELLO_MAX_BYTES < FRAME_BODY_MAX_BYTES);
175 assert!(FRAME_BODY_MIN_BYTES >= 1);
176 assert!(SUBSCRIPTION_POLL_SECS < SLOW_CONSUMER_TIMEOUT_SECS);
180 assert!(crate::blob::BLOB_CHUNK_BYTES.div_ceil(3) * 4 < FRAME_BODY_MAX_BYTES as usize);
181};
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
185#[repr(u8)]
186pub enum FrameKind {
187 Json = 0x01,
189}
190
191pub const FRAME_KIND_RESERVED_STREAM: u8 = 0x02;
195
196impl FrameKind {
197 pub const fn as_u8(self) -> u8 {
198 self as u8
199 }
200
201 pub const fn from_u8(value: u8) -> Option<Self> {
204 match value {
205 0x01 => Some(Self::Json),
206 _ => None,
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum CapabilityNameError {
218 Empty,
219 TooLong,
220 NotSnakeCase,
222}
223
224impl std::fmt::Display for CapabilityNameError {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 Self::Empty => f.write_str("capability name is empty"),
228 Self::TooLong => write!(
229 f,
230 "capability name exceeds {CAPABILITY_NAME_MAX_BYTES} bytes"
231 ),
232 Self::NotSnakeCase => {
233 f.write_str("capability name must be snake_case: [a-z][a-z0-9_]*, no trailing _")
234 }
235 }
236 }
237}
238
239impl std::error::Error for CapabilityNameError {}
240
241#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
247#[serde(transparent)]
248pub struct CapabilityName(String);
249
250impl CapabilityName {
251 pub fn new(value: impl AsRef<str>) -> Result<Self, CapabilityNameError> {
252 let value = value.as_ref();
253 if value.is_empty() {
254 return Err(CapabilityNameError::Empty);
255 }
256 if value.len() > CAPABILITY_NAME_MAX_BYTES {
257 return Err(CapabilityNameError::TooLong);
258 }
259 let bytes = value.as_bytes();
260 let head_ok = bytes[0].is_ascii_lowercase();
261 let body_ok = bytes
262 .iter()
263 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'_');
264 let tail_ok = bytes[bytes.len() - 1] != b'_';
265 if !(head_ok && body_ok && tail_ok) {
266 return Err(CapabilityNameError::NotSnakeCase);
267 }
268 Ok(Self(value.to_owned()))
269 }
270
271 pub fn as_str(&self) -> &str {
272 &self.0
273 }
274}
275
276impl std::fmt::Display for CapabilityName {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.write_str(&self.0)
279 }
280}
281
282impl<'de> serde::Deserialize<'de> for CapabilityName {
283 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
284 let raw = <std::borrow::Cow<'de, str>>::deserialize(d)?;
285 Self::new(raw.as_ref()).map_err(serde::de::Error::custom)
286 }
287}
288
289impl specta::Type for CapabilityName {
291 fn definition(types: &mut specta::Types) -> specta::datatype::DataType {
292 <String as specta::Type>::definition(types)
293 }
294}
295
296#[derive(
303 Debug,
304 Clone,
305 Copy,
306 PartialEq,
307 Eq,
308 PartialOrd,
309 Ord,
310 Hash,
311 serde::Serialize,
312 serde::Deserialize,
313 specta::Type,
314)]
315#[serde(rename_all = "snake_case")]
316pub enum KernelErrorCode {
317 Handshake,
320 UnsupportedVersion,
322 Capability,
325 Validation,
327 DuplicateKey,
329 FrameSize,
331 Sealed,
333 AlreadyActive,
335 NotFound,
336 StaleVersion,
338 IllegalEdge,
340 Authority,
343 IdempotencyConflict,
345 Fenced,
347 Overloaded,
349 SlowConsumer,
351 Storage,
353 Schema,
355 Privilege,
358 BlobIntegrity,
360 BlobTombstoned,
362}
363
364impl KernelErrorCode {
365 pub const ALL: &'static [Self] = &[
367 Self::Handshake,
368 Self::UnsupportedVersion,
369 Self::Capability,
370 Self::Validation,
371 Self::DuplicateKey,
372 Self::FrameSize,
373 Self::Sealed,
374 Self::AlreadyActive,
375 Self::NotFound,
376 Self::StaleVersion,
377 Self::IllegalEdge,
378 Self::Authority,
379 Self::IdempotencyConflict,
380 Self::Fenced,
381 Self::Overloaded,
382 Self::SlowConsumer,
383 Self::Storage,
384 Self::Schema,
385 Self::Privilege,
386 Self::BlobIntegrity,
387 Self::BlobTombstoned,
388 ];
389
390 pub const fn as_str(self) -> &'static str {
392 match self {
393 Self::Handshake => "handshake",
394 Self::UnsupportedVersion => "unsupported_version",
395 Self::Capability => "capability",
396 Self::Validation => "validation",
397 Self::DuplicateKey => "duplicate_key",
398 Self::FrameSize => "frame_size",
399 Self::Sealed => "sealed",
400 Self::AlreadyActive => "already_active",
401 Self::NotFound => "not_found",
402 Self::StaleVersion => "stale_version",
403 Self::IllegalEdge => "illegal_edge",
404 Self::Authority => "authority",
405 Self::IdempotencyConflict => "idempotency_conflict",
406 Self::Fenced => "fenced",
407 Self::Overloaded => "overloaded",
408 Self::SlowConsumer => "slow_consumer",
409 Self::Storage => "storage",
410 Self::Schema => "schema",
411 Self::Privilege => "privilege",
412 Self::BlobIntegrity => "blob_integrity",
413 Self::BlobTombstoned => "blob_tombstoned",
414 }
415 }
416}
417
418impl std::fmt::Display for KernelErrorCode {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.write_str(self.as_str())
421 }
422}
423
424#[derive(
431 Debug,
432 Clone,
433 Copy,
434 PartialEq,
435 Eq,
436 PartialOrd,
437 Ord,
438 Hash,
439 serde::Serialize,
440 serde::Deserialize,
441 specta::Type,
442)]
443#[serde(rename_all = "snake_case")]
444pub enum ProjectionKind {
445 Task,
446 Attempt,
447 EngineSession,
448 Message,
449 Command,
450 Gate,
451 AuthorityGrant,
452 Receipt,
453 Evidence,
454 AttentionItem,
455 Worktree,
456 Lease,
457 DispatchNode,
458 OrchestratorCheckpoint,
459 IngestedRecord,
460}
461
462impl ProjectionKind {
463 pub const ALL: &'static [Self] = &[
465 Self::Task,
466 Self::Attempt,
467 Self::EngineSession,
468 Self::Message,
469 Self::Command,
470 Self::Gate,
471 Self::AuthorityGrant,
472 Self::Receipt,
473 Self::Evidence,
474 Self::AttentionItem,
475 Self::Worktree,
476 Self::Lease,
477 Self::DispatchNode,
478 Self::OrchestratorCheckpoint,
479 Self::IngestedRecord,
480 ];
481
482 pub const fn as_str(self) -> &'static str {
488 match self {
489 Self::Task => "task",
490 Self::Attempt => "attempt",
491 Self::EngineSession => "engine_session",
492 Self::Message => "message",
493 Self::Command => "command",
494 Self::Gate => "gate",
495 Self::AuthorityGrant => "authority_grant",
496 Self::Receipt => "receipt",
497 Self::Evidence => "evidence",
498 Self::AttentionItem => "attention_item",
499 Self::Worktree => "worktree",
500 Self::Lease => "lease",
501 Self::DispatchNode => "dispatch_node",
502 Self::OrchestratorCheckpoint => "orchestrator_checkpoint",
503 Self::IngestedRecord => "ingested_record",
504 }
505 }
506}
507
508#[allow(clippy::large_enum_variant)]
517#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
518#[serde(
519 tag = "projection_type",
520 rename_all = "snake_case",
521 deny_unknown_fields
522)]
523pub enum ProjectionRecord {
524 Task {
525 task: Task,
526 },
527 Attempt {
528 attempt: Attempt,
529 },
530 EngineSession {
531 engine_session: EngineSession,
532 },
533 Message {
534 message: Message,
535 },
536 Command {
537 command: Command,
538 },
539 Gate {
540 gate: Gate,
541 },
542 AuthorityGrant {
543 authority_grant: AuthorityGrant,
544 },
545 Receipt {
546 receipt: Receipt,
547 },
548 Evidence {
549 evidence: Evidence,
550 },
551 AttentionItem {
552 attention_item: AttentionItem,
553 },
554 Worktree {
555 worktree: Worktree,
556 },
557 Lease {
558 lease: Lease,
559 },
560 DispatchNode {
561 dispatch_node: DispatchNode,
562 },
563 OrchestratorCheckpoint {
564 orchestrator_checkpoint: OrchestratorCheckpoint,
565 },
566 IngestedRecord {
567 ingested_record: IngestedRecord,
568 },
569}
570
571impl ProjectionRecord {
572 pub const fn kind(&self) -> ProjectionKind {
575 match self {
576 Self::Task { .. } => ProjectionKind::Task,
577 Self::Attempt { .. } => ProjectionKind::Attempt,
578 Self::EngineSession { .. } => ProjectionKind::EngineSession,
579 Self::Message { .. } => ProjectionKind::Message,
580 Self::Command { .. } => ProjectionKind::Command,
581 Self::Gate { .. } => ProjectionKind::Gate,
582 Self::AuthorityGrant { .. } => ProjectionKind::AuthorityGrant,
583 Self::Receipt { .. } => ProjectionKind::Receipt,
584 Self::Evidence { .. } => ProjectionKind::Evidence,
585 Self::AttentionItem { .. } => ProjectionKind::AttentionItem,
586 Self::Worktree { .. } => ProjectionKind::Worktree,
587 Self::Lease { .. } => ProjectionKind::Lease,
588 Self::DispatchNode { .. } => ProjectionKind::DispatchNode,
589 Self::OrchestratorCheckpoint { .. } => ProjectionKind::OrchestratorCheckpoint,
590 Self::IngestedRecord { .. } => ProjectionKind::IngestedRecord,
591 }
592 }
593}
594
595#[allow(clippy::large_enum_variant)]
609#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
610#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
611pub enum KernelRequest {
612 Health {},
614 Status {},
616 Watermark {},
618 VerifySealed {},
621 SubmitCommand {
623 envelope: CommandEnvelope,
624 },
625 GetProjection {
626 projection: ProjectionKind,
627 id: String,
628 },
629 ListProjection {
630 projection: ProjectionKind,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 #[specta(optional)]
633 cursor: Option<String>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 #[specta(optional)]
636 limit: Option<u32>,
637 },
638 ReadEvents {
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 #[specta(optional)]
644 cursor: Option<Seq>,
645 limit: u32,
646 },
647 SubscribeEvents {
650 #[serde(default, skip_serializing_if = "Option::is_none")]
651 #[specta(optional)]
652 cursor: Option<Seq>,
653 },
654 BlobBegin {
655 media_type: String,
656 byte_size: ByteCount,
657 },
658 BlobChunk {
663 upload_id: BlobUploadId,
664 sequence: u32,
666 data_base64: String,
667 },
668 BlobCommit {
671 upload_id: BlobUploadId,
672 address: BlobAddress,
673 },
674 BlobAbort {
675 upload_id: BlobUploadId,
676 },
677 BlobRead {
678 address: BlobAddress,
679 offset: ByteCount,
680 length: ByteCount,
681 },
682 BlobStat {
683 address: BlobAddress,
684 },
685}
686
687#[allow(clippy::large_enum_variant)]
694#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
695#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
696pub enum KernelResult {
697 Health {
698 ready: bool,
699 sealed: bool,
700 },
701 Status {
702 sealed: bool,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
704 #[specta(optional)]
705 watermark: Option<Seq>,
706 writer_epoch: WriterEpoch,
708 contract_version: u32,
711 public_revision: String,
713 },
714 Watermark {
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 #[specta(optional)]
718 watermark: Option<Seq>,
719 },
720 SealedVerification {
723 sealed: bool,
724 genesis_event_id: EventId,
725 genesis_watermark: Seq,
726 event_count: EventCount,
729 },
730 CommandApplied {
733 command_id: CommandId,
734 events: Vec<EventEnvelope>,
735 watermark: Seq,
736 },
737 Projection {
738 record: ProjectionRecord,
739 },
740 ProjectionPage {
741 records: Vec<ProjectionRecord>,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
743 #[specta(optional)]
744 next_cursor: Option<String>,
745 },
746 Events {
747 events: Vec<EventEnvelope>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 #[specta(optional)]
751 cursor: Option<Seq>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
753 #[specta(optional)]
754 watermark: Option<Seq>,
755 },
756 Subscribed {
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 #[specta(optional)]
760 cursor: Option<Seq>,
761 },
762 BlobBegun {
763 upload_id: BlobUploadId,
764 },
765 BlobChunkAccepted {
766 upload_id: BlobUploadId,
767 sequence: u32,
768 },
769 BlobCommitted {
770 descriptor: BlobDescriptor,
771 deduplicated: bool,
774 },
775 BlobAborted {
776 upload_id: BlobUploadId,
777 },
778 BlobBytes {
779 address: BlobAddress,
780 offset: ByteCount,
781 data_base64: String,
782 },
783 BlobStat {
784 descriptor: BlobDescriptor,
785 },
786 Error {
787 code: KernelErrorCode,
788 message: String,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
793 #[specta(optional, type = Option<JsonValue>)]
794 detail: Option<serde_json::Value>,
795 },
796}
797
798#[allow(clippy::large_enum_variant)]
804#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
805#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
806pub enum ClientControl {
807 Hello {
809 protocol_major: ProtocolVersion,
810 protocol_minor: u32,
811 capabilities: Vec<CapabilityName>,
813 #[serde(default, skip_serializing_if = "Option::is_none")]
816 #[specta(optional)]
817 client: Option<String>,
818 },
819 Request {
820 request_id: RequestId,
821 request: KernelRequest,
822 },
823}
824
825#[allow(clippy::large_enum_variant)]
827#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
828#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
829pub enum ServerControl {
830 HelloAck {
831 protocol_major: ProtocolVersion,
832 protocol_minor: u32,
834 capabilities: Vec<CapabilityName>,
837 sealed: bool,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
839 #[specta(optional)]
840 watermark: Option<Seq>,
841 },
842 HelloRefusal {
843 code: KernelErrorCode,
844 message: String,
845 },
846 Response {
847 request_id: RequestId,
848 result: KernelResult,
849 },
850 EventBatch {
853 request_id: RequestId,
854 events: Vec<EventEnvelope>,
855 cursor: Seq,
856 },
857 StreamClosed {
860 request_id: RequestId,
861 code: KernelErrorCode,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
863 #[specta(optional)]
864 last_cursor: Option<Seq>,
865 },
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871
872 #[test]
873 fn protocol_major_is_exact_and_unknown_majors_are_refused() {
874 assert_eq!(ProtocolVersion::V1.as_u32(), 1);
875 assert_eq!(ProtocolVersion::from_u32(1), Some(ProtocolVersion::V1));
876 assert_eq!(ProtocolVersion::from_u32(0), None);
877 assert_eq!(ProtocolVersion::from_u32(2), None);
878
879 assert_eq!(
882 serde_json::to_value(ProtocolVersion::V1).expect("serialize"),
883 serde_json::json!(1)
884 );
885 assert!(serde_json::from_value::<ProtocolVersion>(serde_json::json!(2)).is_err());
886 }
887
888 #[test]
889 fn frame_bounds_match_the_accepted_codec() {
890 assert_eq!(FRAME_LENGTH_PREFIX_BYTES, 4);
891 assert_eq!(FRAME_BODY_MIN_BYTES, 1);
892 assert_eq!(FRAME_BODY_MAX_BYTES, 4_194_304);
893 assert_eq!(HELLO_MAX_BYTES, 65_536);
894 assert_eq!(CONNECTION_INGRESS_BYTES_PER_WINDOW, 8 * 1024 * 1024);
895 assert_eq!(CONNECTION_EGRESS_BYTES_PER_WINDOW, 8 * 1024 * 1024);
896 assert_eq!(CONNECTION_BUDGET_WINDOW_SECS, 1);
897 assert!(CONNECTION_INGRESS_BYTES_PER_WINDOW > FRAME_BODY_MAX_BYTES as usize);
901 assert!(CONNECTION_EGRESS_BYTES_PER_WINDOW > FRAME_BODY_MAX_BYTES as usize);
902 assert_eq!(SLOW_CONSUMER_TIMEOUT_SECS, 30);
903 assert_eq!(SUBSCRIPTION_POLL_SECS, 5);
904 assert_eq!(MAX_SUBSCRIPTIONS_PER_CONNECTION, 8);
905 assert_eq!(HELLO_DEADLINE_SECS, 5);
906 assert_eq!(MAX_CAPABILITIES, 64);
907 }
910
911 #[test]
912 fn frame_kind_admits_json_only_and_reserves_the_engine_byte() {
913 assert_eq!(FrameKind::Json.as_u8(), 0x01);
914 assert_eq!(FrameKind::from_u8(0x01), Some(FrameKind::Json));
915 assert_eq!(FrameKind::from_u8(FRAME_KIND_RESERVED_STREAM), None);
917 assert_eq!(FrameKind::from_u8(0x00), None);
918 assert_eq!(FrameKind::from_u8(0xFF), None);
919 }
920
921 #[test]
922 fn capability_names_are_strict_snake_case() {
923 assert!(CapabilityName::new("event_subscribe").is_ok());
924 assert!(CapabilityName::new("blob").is_ok());
925 assert!(CapabilityName::new("v2_read").is_ok());
926 for bad in [
927 "",
928 "EventSubscribe",
929 "event-subscribe",
930 "event subscribe",
931 "_leading",
932 "trailing_",
933 "1numeric",
934 "event.subscribe",
935 "événement",
936 ] {
937 assert!(CapabilityName::new(bad).is_err(), "accepted {bad}");
938 }
939 assert!(CapabilityName::new("a".repeat(CAPABILITY_NAME_MAX_BYTES)).is_ok());
940 assert!(CapabilityName::new("a".repeat(CAPABILITY_NAME_MAX_BYTES + 1)).is_err());
941 assert!(serde_json::from_str::<CapabilityName>("\"EventSubscribe\"").is_err());
943 let ok: CapabilityName = serde_json::from_str("\"event_subscribe\"").expect("decode");
944 assert_eq!(ok.as_str(), "event_subscribe");
945 }
946
947 #[test]
948 fn error_codes_are_unique_snake_case_and_agree_with_serde() {
949 for code in KernelErrorCode::ALL {
950 assert_eq!(
951 serde_json::to_value(code).expect("serialize"),
952 serde_json::json!(code.as_str()),
953 "as_str() disagrees with serde for {code:?}"
954 );
955 assert!(
956 code.as_str()
957 .bytes()
958 .all(|b| b.is_ascii_lowercase() || b == b'_'),
959 "{code:?} is not snake_case"
960 );
961 }
962 let mut wire: Vec<&str> = KernelErrorCode::ALL.iter().map(|c| c.as_str()).collect();
963 let total = wire.len();
964 wire.sort_unstable();
965 wire.dedup();
966 assert_eq!(wire.len(), total, "two error codes share a wire value");
967 assert_eq!(total, 21);
968 assert_eq!(
969 KernelErrorCode::IdempotencyConflict.as_str(),
970 "idempotency_conflict"
971 );
972 }
973
974 #[test]
975 fn record_field_name_equals_the_tag() {
976 for record in sample_records() {
979 let json = serde_json::to_value(&record).expect("serialize");
980 let object = json.as_object().expect("record is an object");
981 let tag = object["projection_type"].as_str().expect("tagged");
982 let fields: Vec<&str> = object
983 .keys()
984 .map(String::as_str)
985 .filter(|k| *k != "projection_type")
986 .collect();
987 assert_eq!(fields, [tag], "projection {tag} has fields {fields:?}");
988 }
989 }
990
991 #[test]
992 fn every_projection_kind_has_exactly_one_record_variant() {
993 let records = sample_records();
997 assert_eq!(records.len(), ProjectionKind::ALL.len());
998 let kinds: Vec<ProjectionKind> = records.iter().map(ProjectionRecord::kind).collect();
999 assert_eq!(kinds, ProjectionKind::ALL.to_vec());
1000 for record in &records {
1001 let json = serde_json::to_value(record).expect("serialize");
1002 let tag = json["projection_type"].as_str().expect("tagged");
1003 let kind_wire = serde_json::to_value(record.kind()).expect("serialize kind");
1004 assert_eq!(serde_json::json!(tag), kind_wire);
1005 assert_eq!(record.kind().as_str(), tag);
1009 let back: ProjectionRecord = serde_json::from_value(json).expect("deserialize");
1010 assert_eq!(&back, record);
1011 }
1012 }
1013
1014 #[test]
1015 fn control_frames_round_trip_with_their_type_tags() {
1016 let hello = ClientControl::Hello {
1017 protocol_major: ProtocolVersion::V1,
1018 protocol_minor: PROTOCOL_MINOR,
1019 capabilities: vec![
1020 CapabilityName::new("event_subscribe").expect("valid"),
1021 CapabilityName::new("blob").expect("valid"),
1022 ],
1023 client: None,
1024 };
1025 let json = serde_json::to_value(&hello).expect("serialize");
1026 assert_eq!(json["type"], "hello");
1027 assert_eq!(json["protocol_major"], 1);
1028 assert!(json.get("client").is_none(), "absent optional was emitted");
1029 assert_eq!(
1030 serde_json::from_value::<ClientControl>(json).expect("deserialize"),
1031 hello
1032 );
1033
1034 let watermark = ServerControl::Response {
1035 request_id: RequestId::new("req-1"),
1036 result: KernelResult::Watermark {
1037 watermark: Some(Seq::new(9_007_199_254_740_993)),
1038 },
1039 };
1040 let json = serde_json::to_value(&watermark).expect("serialize");
1041 assert_eq!(json["type"], "response");
1042 assert_eq!(json["result"]["type"], "watermark");
1043 assert_eq!(json["result"]["watermark"], "9007199254740993");
1045 assert_eq!(
1046 serde_json::from_value::<ServerControl>(json).expect("deserialize"),
1047 watermark
1048 );
1049
1050 let empty =
1053 serde_json::to_value(KernelResult::Watermark { watermark: None }).expect("serialize");
1054 assert_eq!(empty, serde_json::json!({ "type": "watermark" }));
1055 }
1056
1057 #[test]
1058 fn a_refusal_is_a_value_on_the_ordinary_response_path() {
1059 let refused = ServerControl::Response {
1060 request_id: RequestId::new("req-2"),
1061 result: KernelResult::Error {
1062 code: KernelErrorCode::StaleVersion,
1063 message: "version conflict".into(),
1064 detail: Some(serde_json::json!({ "actual": 7, "expected": 6 })),
1065 },
1066 };
1067 let json = serde_json::to_value(&refused).expect("serialize");
1068 assert_eq!(json["result"]["code"], "stale_version");
1069 assert_eq!(json["result"]["detail"]["actual"], 7);
1070 assert_eq!(
1071 serde_json::from_value::<ServerControl>(json).expect("deserialize"),
1072 refused
1073 );
1074 }
1075
1076 #[test]
1077 fn unit_requests_carry_only_their_tag() {
1078 for (request, tag) in [
1079 (KernelRequest::Health {}, "health"),
1080 (KernelRequest::Status {}, "status"),
1081 (KernelRequest::Watermark {}, "watermark"),
1082 (KernelRequest::VerifySealed {}, "verify_sealed"),
1083 ] {
1084 let json = serde_json::to_value(&request).expect("serialize");
1085 assert_eq!(json, serde_json::json!({ "type": tag }));
1086 assert_eq!(
1087 serde_json::from_value::<KernelRequest>(json).expect("deserialize"),
1088 request
1089 );
1090 }
1091 }
1092
1093 fn sample_records() -> Vec<ProjectionRecord> {
1095 use crate::envelope::Actor;
1096 use crate::fsm::{
1097 AttemptState, CommandState, GateVerdict, LeaseMode, LeaseState, MessageState, TaskState,
1098 };
1099 use crate::ids::{
1100 AttemptId, AttentionItemId, AuthorityGrantId, DispatchNodeId, EngineId,
1101 EngineSessionId, EvidenceId, GateId, IdempotencyKey, IngestedRecordId, LeaseId,
1102 MessageId, ReceiptId, TaskId, Timestamp, WorktreeId,
1103 };
1104
1105 let ts = || Timestamp::new("2026-07-28T00:00:00Z");
1106 let actor = || Actor {
1107 kind: "kernel".into(),
1108 id: None,
1109 };
1110 vec![
1111 ProjectionRecord::Task {
1112 task: Task {
1113 id: TaskId::new("task-1"),
1114 version: 1,
1115 state: TaskState::Submitted,
1116 kind: None,
1117 title: None,
1118 spec_ref: None,
1119 project: None,
1120 priority: None,
1121 tracker_ref: None,
1122 created_at: ts(),
1123 updated_at: ts(),
1124 },
1125 },
1126 ProjectionRecord::Attempt {
1127 attempt: Attempt {
1128 id: AttemptId::new("att-1"),
1129 version: 1,
1130 state: AttemptState::Queued,
1131 task_id: TaskId::new("task-1"),
1132 engine: EngineId::new("engine-a"),
1133 capability: None,
1134 role: None,
1135 model_lane: None,
1136 permission_profile: None,
1137 worktree_lease_id: None,
1138 base_sha: None,
1139 budget: None,
1140 result_schema_ref: None,
1141 provider_session_ref: None,
1142 runtime_ref: None,
1143 runtime_started_at: None,
1144 exit_code: None,
1145 provider_terminal_event: None,
1146 result_valid: None,
1147 evidence_manifest_ref: None,
1148 gate_result: None,
1149 created_at: ts(),
1150 updated_at: ts(),
1151 },
1152 },
1153 ProjectionRecord::EngineSession {
1154 engine_session: EngineSession {
1155 id: EngineSessionId::new("sess-1"),
1156 attempt_id: AttemptId::new("att-1"),
1157 engine: EngineId::new("engine-a"),
1158 provider_session_ref: None,
1159 started_at: ts(),
1160 ended_at: None,
1161 },
1162 },
1163 ProjectionRecord::Message {
1164 message: Message {
1165 id: MessageId::new("msg-1"),
1166 version: 1,
1167 state: MessageState::Accepted,
1168 idempotency_key: IdempotencyKey::new("send-once"),
1169 correlation_id: None,
1170 reply_to: None,
1171 sender: None,
1172 recipient: None,
1173 channel: None,
1174 kind: None,
1175 payload: None,
1176 deadline: None,
1177 delivery_attempts: 0,
1178 dead_letter_reason: None,
1179 delivery_refs: None,
1180 created_at: ts(),
1181 updated_at: ts(),
1182 },
1183 },
1184 ProjectionRecord::Command {
1185 command: Command {
1186 id: CommandId::new("cmd-1"),
1187 version: 1,
1188 state: CommandState::Issued,
1189 kind: "stop_attempt".into(),
1190 target: None,
1191 actor: None,
1192 idempotency_key: None,
1193 outcome: None,
1194 created_at: ts(),
1195 updated_at: ts(),
1196 },
1197 },
1198 ProjectionRecord::Gate {
1199 gate: Gate {
1200 id: GateId::new("gate-1"),
1201 version: 1,
1202 attempt_id: None,
1203 phase_ref: None,
1204 kind: None,
1205 verdict: GateVerdict::Pending,
1206 evidence_ref: None,
1207 created_at: ts(),
1208 updated_at: ts(),
1209 },
1210 },
1211 ProjectionRecord::AuthorityGrant {
1212 authority_grant: AuthorityGrant {
1213 id: AuthorityGrantId::new("grant-1"),
1214 grantee: actor(),
1215 action_class: "deploy".into(),
1216 scope: None,
1217 granted_at: ts(),
1218 expires_at: None,
1219 revoked_at: None,
1220 revoke_reason: None,
1221 receipt_id: None,
1222 },
1223 },
1224 ProjectionRecord::Receipt {
1225 receipt: Receipt {
1226 id: ReceiptId::new("r-1"),
1227 actor: actor(),
1228 action: "state_flip".into(),
1229 subject_type: "attempt".into(),
1230 subject_id: "att-1".into(),
1231 from: None,
1232 to: None,
1233 observed_basis: None,
1234 ts: ts(),
1235 },
1236 },
1237 ProjectionRecord::Evidence {
1238 evidence: Evidence {
1239 id: EvidenceId::new("ev-1"),
1240 kind: "transcript".into(),
1241 r#ref: "blob://transcript".into(),
1242 digest: None,
1243 byte_size: None,
1244 created_at: ts(),
1245 },
1246 },
1247 ProjectionRecord::AttentionItem {
1248 attention_item: AttentionItem {
1249 id: AttentionItemId::new("att-item-1"),
1250 kind: "risk_tag".into(),
1251 summary: "data-migration pages".into(),
1252 subject_ref: None,
1253 raised_by: None,
1254 raised_at: ts(),
1255 resolved_at: None,
1256 resolution: None,
1257 },
1258 },
1259 ProjectionRecord::Worktree {
1260 worktree: Worktree {
1261 id: WorktreeId::new("wt-1"),
1262 repo: "gridwork".into(),
1263 path: "/w/kernel".into(),
1264 branch: "feature/kernel".into(),
1265 base_sha: None,
1266 lease_id: None,
1267 dirty: false,
1268 unpushed: false,
1269 released_at: None,
1270 disposition: None,
1271 created_at: ts(),
1272 },
1273 },
1274 ProjectionRecord::Lease {
1275 lease: Lease {
1276 id: LeaseId::new("lease-1"),
1277 version: 1,
1278 state: LeaseState::Held,
1279 mode: LeaseMode::Exclusive,
1280 holder: None,
1281 scope: None,
1282 repo: None,
1283 path: None,
1284 branch: None,
1285 base_sha: None,
1286 fence_token: None,
1287 heartbeat_at: None,
1288 expires_at: None,
1289 dirty: false,
1290 unpushed: false,
1291 disposition: None,
1292 created_at: ts(),
1293 updated_at: ts(),
1294 },
1295 },
1296 ProjectionRecord::DispatchNode {
1297 dispatch_node: DispatchNode {
1298 id: DispatchNodeId::new("node-1"),
1299 version: 1,
1300 parent_id: None,
1301 attempt_id: None,
1302 kind: "orchestrator".into(),
1303 state: crate::entity::DISPATCH_NODE_INITIAL_STATE.into(),
1304 label: None,
1305 created_at: ts(),
1306 updated_at: ts(),
1307 },
1308 },
1309 ProjectionRecord::OrchestratorCheckpoint {
1310 orchestrator_checkpoint: OrchestratorCheckpoint {
1311 orchestrator_id: None,
1312 seq: Seq::new(1),
1313 native_session_ref: None,
1314 active_goal: None,
1315 active_step_ref: None,
1316 latest_command_ref: None,
1317 open_attempts: None,
1318 leases: None,
1319 pending_approvals: None,
1320 budget_cursor: None,
1321 },
1322 },
1323 ProjectionRecord::IngestedRecord {
1324 ingested_record: IngestedRecord {
1325 id: IngestedRecordId::new("ingest:proj-1:key-1"),
1326 kind: crate::ingestion::IngestionKind::Memory,
1327 payload: serde_json::json!({ "text": "recalled" }),
1328 payload_ref: None,
1329 ingested_by: None,
1330 event_seq: Seq::new(7),
1331 ingested_at: ts(),
1332 },
1333 },
1334 ]
1335 }
1336}