use sha2::{Digest, Sha256};
const ID_HEX_LEN: usize = 32;
#[derive(Debug, Clone, Copy, Default)]
pub struct EventIdentity<'a> {
pub agent: &'a str,
pub session_id: Option<&'a str>,
pub source_id: Option<&'a str>,
pub ts: i64,
pub role: &'a str,
pub extra: Option<&'a str>,
}
pub fn event_id(identity: EventIdentity<'_>) -> String {
let mut hasher = Sha256::new();
write_field(&mut hasher, identity.agent.as_bytes());
write_field(&mut hasher, identity.session_id.unwrap_or("").as_bytes());
write_field(&mut hasher, identity.source_id.unwrap_or("").as_bytes());
write_field(&mut hasher, &identity.ts.to_be_bytes());
write_field(&mut hasher, identity.role.as_bytes());
write_field(&mut hasher, identity.extra.unwrap_or("").as_bytes());
hex(&hasher.finalize())[..ID_HEX_LEN].to_string()
}
pub fn text_hash(text: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(text.as_bytes());
hex(&hasher.finalize())[..ID_HEX_LEN].to_string()
}
fn write_field(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn identity() -> EventIdentity<'static> {
EventIdentity {
agent: "claude-code",
session_id: Some("sess-1"),
source_id: Some("uuid-1"),
ts: 1_754_300_000_000,
role: "assistant",
extra: None,
}
}
#[test]
fn same_content_same_id() {
assert_eq!(event_id(identity()), event_id(identity()));
}
#[test]
fn id_is_stable_across_runs() {
assert_eq!(event_id(identity()), "6cbc1e2d028a0bc4fee7ae15c0405df6");
}
#[test]
fn different_content_different_id() {
let base = event_id(identity());
let mut other = identity();
other.ts += 1;
assert_ne!(event_id(other), base);
let mut other = identity();
other.role = "user";
assert_ne!(event_id(other), base);
let mut other = identity();
other.extra = Some("1");
assert_ne!(event_id(other), base);
}
#[test]
fn field_boundaries_are_unambiguous() {
let a = EventIdentity {
agent: "ab",
session_id: Some("c"),
..EventIdentity::default()
};
let b = EventIdentity {
agent: "a",
session_id: Some("bc"),
..EventIdentity::default()
};
assert_ne!(event_id(a), event_id(b));
}
#[test]
fn text_hash_is_deterministic() {
assert_eq!(text_hash("run the tests"), text_hash("run the tests"));
assert_ne!(text_hash("run the tests"), text_hash("run the test"));
assert_eq!(text_hash("").len(), ID_HEX_LEN);
}
}