use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const CHANNEL_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ChannelState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
Rejected,
Stale,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum ChannelActor {
Human { name: String },
Agent { id: String },
System,
}
impl ChannelActor {
pub fn local_human() -> Self {
let name = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "you".to_string());
ChannelActor::Human { name }
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ParticipantRole {
Owner,
Router,
Delegate,
Observer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Participant {
pub actor: ChannelActor,
pub role: ParticipantRole,
pub joined_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Goal {
#[serde(default)]
pub statement: String,
#[serde(default)]
pub acceptance_criteria: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
pub v: u32,
pub id: String,
pub title: String,
#[serde(default)]
pub goal: Goal,
pub state: ChannelState,
pub owner: ChannelActor,
#[serde(default)]
pub participants: Vec<Participant>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum EventKind {
Message,
Delegation,
Handoff,
ToolCall,
ToolResult,
StateChange,
Artifact,
HitlRequest,
HitlResponse,
Note,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelEvent {
pub seq: u64,
pub ts: DateTime<Utc>,
pub actor: ChannelActor,
pub kind: EventKind,
#[serde(default)]
pub payload: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sig: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_version: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_state_serializes_kebab() {
let j = serde_json::to_string(&ChannelState::InputRequired).unwrap();
assert_eq!(j, "\"input-required\"");
}
#[test]
fn event_round_trips() {
let ev = ChannelEvent {
seq: 3,
ts: Utc::now(),
actor: ChannelActor::Agent { id: "qa".into() },
kind: EventKind::Message,
payload: serde_json::json!({ "text": "hello", "task_id": "t-1" }),
idempotency_key: None,
sig: None,
key_version: None,
};
let line = serde_json::to_string(&ev).unwrap();
let back: ChannelEvent = serde_json::from_str(&line).unwrap();
assert_eq!(back.seq, 3);
assert_eq!(back.actor, ChannelActor::Agent { id: "qa".into() });
assert_eq!(back.payload["text"], "hello");
}
#[test]
fn system_actor_round_trips() {
let a = ChannelActor::System;
let j = serde_json::to_string(&a).unwrap();
assert_eq!(j, "{\"kind\":\"system\"}");
let back: ChannelActor = serde_json::from_str(&j).unwrap();
assert_eq!(back, ChannelActor::System);
}
#[test]
fn event_omits_sig_fields_when_absent_and_reads_old_rows() {
let ev = ChannelEvent {
seq: 0,
ts: Utc::now(),
actor: ChannelActor::System,
kind: EventKind::Note,
payload: serde_json::json!({}),
idempotency_key: None,
sig: None,
key_version: None,
};
let line = serde_json::to_string(&ev).unwrap();
assert!(!line.contains("sig"), "sig must be omitted when None");
assert!(
!line.contains("key_version"),
"key_version must be omitted when None"
);
let old = r#"{"seq":0,"ts":"2026-06-16T00:00:00Z","actor":{"kind":"system"},"kind":"note","payload":{}}"#;
let back: ChannelEvent = serde_json::from_str(old).unwrap();
assert_eq!(back.sig, None);
assert_eq!(back.key_version, None);
}
}