use super::graph::{TranscriptHistoryState, TranscriptRevisionBody, TranscriptRewriteCommit};
use sha2::{Digest, Sha256};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, OnceLock};
pub(super) const TRANSCRIPT_GRAPH_FACT_HEAL_PROBE_CURRENT: u8 = 1;
pub(super) const TRANSCRIPT_GRAPH_FACT_VALIDATED: u8 = 2;
pub(super) fn transcript_graph_shape_key(
fact: u8,
digest_format: u32,
head: &str,
commits: &[TranscriptRewriteCommit],
revisions: &[TranscriptRevisionBody],
) -> Option<String> {
let mut hasher = Sha256::new();
hasher.update([fact]);
hasher.update((digest_format as u64).to_le_bytes());
hasher.update((head.len() as u64).to_le_bytes());
hasher.update(head.as_bytes());
hasher.update((revisions.len() as u64).to_le_bytes());
for body in revisions {
hasher.update((body.revision.len() as u64).to_le_bytes());
hasher.update(body.revision.as_bytes());
match body.parent_revision.as_deref() {
Some(parent) => {
hasher.update([1]);
hasher.update((parent.len() as u64).to_le_bytes());
hasher.update(parent.as_bytes());
}
None => hasher.update([0]),
}
let created_at = serde_json::to_vec(&body.created_at).ok()?;
hasher.update((created_at.len() as u64).to_le_bytes());
hasher.update(&created_at);
hasher.update((body.messages.len() as u64).to_le_bytes());
for message in &body.messages {
hash_digest_erased_message_fields(&mut hasher, message)?;
}
}
hasher.update((commits.len() as u64).to_le_bytes());
for commit in commits {
let bytes = serde_json::to_vec(commit).ok()?;
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
}
let digest = hasher.finalize();
let mut out = String::with_capacity(2 + digest.len() * 2);
out.push(char::from(b'0' + fact));
out.push(':');
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in digest {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
Some(out)
}
fn hash_digest_erased_message_fields(hasher: &mut Sha256, message: &crate::Message) -> Option<()> {
use crate::Message;
fn hash_time(hasher: &mut Sha256, at: &crate::types::MessageTimestamp) -> Option<()> {
let bytes = serde_json::to_vec(at).ok()?;
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
Some(())
}
fn hash_identity(
hasher: &mut Sha256,
identity: &crate::types::TranscriptMessageIdentity,
) -> Option<()> {
let bytes = serde_json::to_vec(identity).ok()?;
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
Some(())
}
fn hash_image_forms(hasher: &mut Sha256, blocks: &[crate::types::ContentBlock]) {
for block in blocks {
if let crate::types::ContentBlock::Image { data, .. } = block {
match data {
crate::types::ImageData::Inline { .. } => hasher.update([b'i']),
crate::types::ImageData::Blob { .. } => hasher.update([b'b']),
}
}
}
}
match message {
Message::System(system) => {
hasher.update([1]);
hash_time(hasher, &system.created_at)?;
}
Message::SystemNotice(notice) => {
hasher.update([2]);
hash_time(hasher, ¬ice.created_at)?;
for block in ¬ice.blocks {
match block {
crate::types::SystemNoticeBlock::Comms { content, .. }
| crate::types::SystemNoticeBlock::ExternalEvent { content, .. } => {
hash_image_forms(hasher, content);
}
_ => {}
}
}
}
Message::User(user) => {
hasher.update([3]);
hash_identity(hasher, &user.identity)?;
hash_time(hasher, &user.created_at)?;
hash_image_forms(hasher, &user.content);
}
Message::BlockAssistant(assistant) => {
hasher.update([4]);
hash_identity(hasher, &assistant.identity)?;
hash_time(hasher, &assistant.created_at)?;
}
Message::ToolResults {
created_at,
results,
..
} => {
hasher.update([5]);
hash_time(hasher, created_at)?;
for result in results {
hash_image_forms(hasher, &result.content);
}
}
}
Some(())
}
struct BoundedTranscriptGraphMemo<V> {
capacity: usize,
entries: HashMap<String, V>,
order: VecDeque<String>,
}
impl<V> BoundedTranscriptGraphMemo<V> {
fn new(capacity: usize) -> Self {
Self {
capacity,
entries: HashMap::new(),
order: VecDeque::new(),
}
}
fn get(&self, key: &str) -> Option<&V> {
self.entries.get(key)
}
fn record(&mut self, key: String, value: V) {
if self.entries.contains_key(&key) {
return;
}
while self.entries.len() >= self.capacity {
let Some(evicted) = self.order.pop_front() else {
break;
};
self.entries.remove(&evicted);
}
self.order.push_back(key.clone());
self.entries.insert(key, value);
}
}
const TRANSCRIPT_GRAPH_HEAL_PROBE_MEMO_CAPACITY: usize = 4096;
static TRANSCRIPT_GRAPH_HEAL_PROBE_MEMO: OnceLock<Mutex<BoundedTranscriptGraphMemo<()>>> =
OnceLock::new();
fn transcript_graph_heal_probe_memo() -> &'static Mutex<BoundedTranscriptGraphMemo<()>> {
TRANSCRIPT_GRAPH_HEAL_PROBE_MEMO.get_or_init(|| {
Mutex::new(BoundedTranscriptGraphMemo::new(
TRANSCRIPT_GRAPH_HEAL_PROBE_MEMO_CAPACITY,
))
})
}
const TRANSCRIPT_GRAPH_VALIDATED_MEMO_BYTE_BUDGET: usize = 256 * 1024 * 1024;
const TRANSCRIPT_GRAPH_VALIDATED_MEMO_ENTRY_CAP: usize =
TRANSCRIPT_GRAPH_VALIDATED_MEMO_BYTE_BUDGET / 2;
struct ByteBudgetedTranscriptGraphMemo {
budget_bytes: usize,
entry_cap_bytes: usize,
retained_bytes: usize,
entries: HashMap<String, (Arc<TranscriptHistoryState>, usize)>,
order: VecDeque<String>,
}
impl ByteBudgetedTranscriptGraphMemo {
fn new(budget_bytes: usize, entry_cap_bytes: usize) -> Self {
Self {
budget_bytes,
entry_cap_bytes,
retained_bytes: 0,
entries: HashMap::new(),
order: VecDeque::new(),
}
}
fn get(&mut self, key: &str) -> Option<Arc<TranscriptHistoryState>> {
let (state, _) = self.entries.get(key)?;
let state = Arc::clone(state);
if let Some(position) = self.order.iter().position(|entry| entry == key) {
let entry = self.order.remove(position);
if let Some(entry) = entry {
self.order.push_back(entry);
}
}
Some(state)
}
fn record(&mut self, key: String, value: Arc<TranscriptHistoryState>, bytes: usize) {
if bytes > self.entry_cap_bytes || self.entries.contains_key(&key) {
return;
}
while self.retained_bytes + bytes > self.budget_bytes {
let Some(evicted) = self.order.pop_front() else {
break;
};
if let Some((_, evicted_bytes)) = self.entries.remove(&evicted) {
self.retained_bytes = self.retained_bytes.saturating_sub(evicted_bytes);
}
}
self.order.push_back(key.clone());
self.retained_bytes += bytes;
self.entries.insert(key, (value, bytes));
}
}
static TRANSCRIPT_GRAPH_VALIDATED_MEMO: OnceLock<Mutex<ByteBudgetedTranscriptGraphMemo>> =
OnceLock::new();
fn transcript_graph_validated_memo() -> &'static Mutex<ByteBudgetedTranscriptGraphMemo> {
TRANSCRIPT_GRAPH_VALIDATED_MEMO.get_or_init(|| {
Mutex::new(ByteBudgetedTranscriptGraphMemo::new(
TRANSCRIPT_GRAPH_VALIDATED_MEMO_BYTE_BUDGET,
TRANSCRIPT_GRAPH_VALIDATED_MEMO_ENTRY_CAP,
))
})
}
pub(super) fn transcript_graph_heal_probe_is_memoized(key: &str) -> bool {
if std::env::var_os("MEERKAT_DISABLE_GRAPH_DECODE_MEMO").is_some() {
return false;
}
transcript_graph_heal_probe_memo()
.lock()
.map(|memo| memo.get(key).is_some())
.unwrap_or(false)
}
pub(super) fn record_transcript_graph_heal_probe(key: String) {
if let Ok(mut memo) = transcript_graph_heal_probe_memo().lock() {
memo.record(key, ());
}
}
pub(super) fn memoized_validated_transcript_graph(
key: &str,
) -> Option<Arc<TranscriptHistoryState>> {
if std::env::var_os("MEERKAT_DISABLE_GRAPH_DECODE_MEMO").is_some() {
return None;
}
transcript_graph_validated_memo()
.lock()
.ok()
.and_then(|mut memo| memo.get(key))
}
pub(super) fn record_validated_transcript_graph(key: String, state: Arc<TranscriptHistoryState>) {
let Some(bytes) = approximate_serialized_bytes(state.as_ref()) else {
return;
};
if let Ok(mut memo) = transcript_graph_validated_memo().lock() {
memo.record(key, state, bytes);
}
}
pub(in crate::session) fn record_producer_validated_transcript_graph(
state: Arc<TranscriptHistoryState>,
approx_bytes: usize,
) {
if std::env::var_os("MEERKAT_DISABLE_GRAPH_DECODE_MEMO").is_some() {
return;
}
let Some(key) = transcript_graph_shape_key(
TRANSCRIPT_GRAPH_FACT_VALIDATED,
state.digest_format,
&state.head,
&state.commits,
&state.revisions,
) else {
return;
};
if let Ok(mut memo) = transcript_graph_validated_memo().lock() {
memo.record(key, state, approx_bytes);
}
}
pub(in crate::session) fn approximate_json_bytes(value: &serde_json::Value) -> usize {
match value {
serde_json::Value::Null => 4,
serde_json::Value::Bool(_) => 5,
serde_json::Value::Number(_) => 12,
serde_json::Value::String(text) => text.len().saturating_add(8),
serde_json::Value::Array(items) => items
.iter()
.map(approximate_json_bytes)
.fold(2usize.saturating_add(items.len()), usize::saturating_add),
serde_json::Value::Object(entries) => entries
.iter()
.map(|(key, value)| {
key.len()
.saturating_add(4)
.saturating_add(approximate_json_bytes(value))
})
.fold(2usize, usize::saturating_add),
}
}
fn approximate_serialized_bytes<T: serde::Serialize>(value: &T) -> Option<usize> {
struct CountingWriter(usize);
impl std::io::Write for CountingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0 = self.0.saturating_add(buf.len());
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut writer = CountingWriter(0);
serde_json::to_writer(&mut writer, value).ok()?;
Some(writer.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TranscriptGraphValidationMode {
FullVerify,
DecodeMemoized,
}