use serde_json::{Value, json};
#[derive(Debug, Clone, PartialEq)]
pub enum MemoryTimelineEvent {
DreamStarted { realm: String, run_id: String },
DreamCompleted {
realm: String,
run_id: String,
ops_committed: usize,
detail: Value,
},
DreamSkipped { realm: String, reason: String },
RecordPromoted {
realm: String,
record_id: String,
source_record_id: Option<String>,
scope_kind: String,
scope_key: String,
proposal_id: Option<String>,
gated: bool,
},
QuarantineVerdict {
realm: String,
record_id: String,
verdict: String,
rationale: Option<String>,
},
QuarantineReleaseBlocked {
realm: String,
record_id: String,
verdict: String,
class: String,
},
ConflictSignal {
realm: String,
entity: String,
topic: String,
reason: String,
},
QuarantinedWrite {
realm: String,
author: String,
reason: String,
},
TaintTransition {
identity: Option<String>,
session_key: String,
kind: String,
source: String,
},
BudgetDenied {
realm: String,
stage: String,
reason: String,
},
PromotionPendingGate {
realm: String,
pending_id: String,
record_id: String,
scope_kind: String,
scope_key: String,
},
HarvestCompleted {
realm: String,
identity: String,
promoted: usize,
tombstoned: usize,
},
DistillationTimedOut {
identity: String,
session_key: String,
cause: String,
},
HygieneProposed {
identity: String,
session_key: String,
cause: String,
ops: usize,
flagged_active_records: Vec<String>,
},
HygieneApplied {
identity: String,
session_key: String,
cause: String,
parent_revision: String,
revision: String,
ops: usize,
flagged_active_records: Vec<String>,
},
HygieneBlocked {
identity: String,
session_key: String,
cause: String,
reason: String,
},
HygieneSkipped {
identity: String,
session_key: String,
cause: String,
reason: String,
},
}
impl MemoryTimelineEvent {
pub fn event_type(&self) -> &'static str {
match self {
Self::DreamStarted { .. } => "memory.dream.started",
Self::DreamCompleted { .. } => "memory.dream.completed",
Self::DreamSkipped { .. } => "memory.dream.skipped",
Self::RecordPromoted { .. } => "memory.record.promoted",
Self::QuarantineVerdict { .. } => "memory.quarantine.verdict",
Self::QuarantineReleaseBlocked { .. } => "memory.quarantine.release_blocked",
Self::ConflictSignal { .. } => "memory.conflict.signal",
Self::QuarantinedWrite { .. } => "memory.write.quarantined",
Self::TaintTransition { .. } => "memory.taint.transition",
Self::BudgetDenied { .. } => "memory.budget.denied",
Self::PromotionPendingGate { .. } => "memory.promotion.pending_gate",
Self::HarvestCompleted { .. } => "memory.harvest.completed",
Self::DistillationTimedOut { .. } => "memory.distill.timed_out",
Self::HygieneProposed { .. } => "memory.hygiene.proposed",
Self::HygieneApplied { .. } => "memory.hygiene.applied",
Self::HygieneBlocked { .. } => "memory.hygiene.blocked",
Self::HygieneSkipped { .. } => "memory.hygiene.skipped",
}
}
pub fn identity(&self) -> Option<&str> {
match self {
Self::TaintTransition { identity, .. } => identity.as_deref(),
Self::HarvestCompleted { identity, .. }
| Self::DistillationTimedOut { identity, .. }
| Self::HygieneProposed { identity, .. }
| Self::HygieneApplied { identity, .. }
| Self::HygieneBlocked { identity, .. }
| Self::HygieneSkipped { identity, .. } => Some(identity),
_ => None,
}
}
pub fn data(&self) -> Value {
match self {
Self::DreamStarted { realm, run_id } => json!({
"realm": realm,
"run_id": run_id,
}),
Self::DreamCompleted {
realm,
run_id,
ops_committed,
detail,
} => json!({
"realm": realm,
"run_id": run_id,
"ops_committed": ops_committed,
"detail": detail,
}),
Self::DreamSkipped { realm, reason } => json!({
"realm": realm,
"reason": reason,
}),
Self::RecordPromoted {
realm,
record_id,
source_record_id,
scope_kind,
scope_key,
proposal_id,
gated,
} => json!({
"realm": realm,
"record_id": record_id,
"source_record_id": source_record_id,
"scope_kind": scope_kind,
"scope_key": scope_key,
"proposal_id": proposal_id,
"gated": gated,
}),
Self::QuarantineVerdict {
realm,
record_id,
verdict,
rationale,
} => json!({
"realm": realm,
"record_id": record_id,
"verdict": verdict,
"rationale": rationale,
}),
Self::QuarantineReleaseBlocked {
realm,
record_id,
verdict,
class,
} => json!({
"realm": realm,
"record_id": record_id,
"verdict": verdict,
"class": class,
}),
Self::ConflictSignal {
realm,
entity,
topic,
reason,
} => json!({
"realm": realm,
"entity": entity,
"topic": topic,
"reason": reason,
}),
Self::QuarantinedWrite {
realm,
author,
reason,
} => json!({
"realm": realm,
"author": author,
"reason": reason,
}),
Self::TaintTransition {
identity,
session_key,
kind,
source,
} => json!({
"identity": identity,
"session_key": session_key,
"kind": kind,
"source": source,
}),
Self::BudgetDenied {
realm,
stage,
reason,
} => json!({
"realm": realm,
"stage": stage,
"reason": reason,
}),
Self::PromotionPendingGate {
realm,
pending_id,
record_id,
scope_kind,
scope_key,
} => json!({
"realm": realm,
"pending_id": pending_id,
"record_id": record_id,
"scope_kind": scope_kind,
"scope_key": scope_key,
}),
Self::HarvestCompleted {
realm,
identity,
promoted,
tombstoned,
} => json!({
"realm": realm,
"identity": identity,
"promoted": promoted,
"tombstoned": tombstoned,
}),
Self::DistillationTimedOut {
identity,
session_key,
cause,
} => json!({
"identity": identity,
"session_key": session_key,
"cause": cause,
}),
Self::HygieneProposed {
identity,
session_key,
cause,
ops,
flagged_active_records,
} => json!({
"identity": identity,
"session_key": session_key,
"cause": cause,
"ops": ops,
"flagged_active_records": flagged_active_records,
}),
Self::HygieneApplied {
identity,
session_key,
cause,
parent_revision,
revision,
ops,
flagged_active_records,
} => json!({
"identity": identity,
"session_key": session_key,
"cause": cause,
"parent_revision": parent_revision,
"revision": revision,
"ops": ops,
"flagged_active_records": flagged_active_records,
}),
Self::HygieneBlocked {
identity,
session_key,
cause,
reason,
} => json!({
"identity": identity,
"session_key": session_key,
"cause": cause,
"reason": reason,
}),
Self::HygieneSkipped {
identity,
session_key,
cause,
reason,
} => json!({
"identity": identity,
"session_key": session_key,
"cause": cause,
"reason": reason,
}),
}
}
}
pub trait MemoryEventSink: Send + Sync {
fn emit(&self, event: MemoryTimelineEvent);
}
#[cfg(test)]
pub(crate) struct CollectingEventSink {
pub events: std::sync::Mutex<Vec<MemoryTimelineEvent>>,
}
#[cfg(test)]
impl CollectingEventSink {
pub(crate) fn new() -> Self {
Self {
events: std::sync::Mutex::new(Vec::new()),
}
}
pub(crate) fn types(&self) -> Vec<&'static str> {
self.events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.map(MemoryTimelineEvent::event_type)
.collect()
}
}
#[cfg(test)]
impl MemoryEventSink for CollectingEventSink {
fn emit(&self, event: MemoryTimelineEvent) {
self.events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(event);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_types_are_stable_wire_contract() {
let event = MemoryTimelineEvent::DreamSkipped {
realm: "family".to_string(),
reason: "budget".to_string(),
};
assert_eq!(event.event_type(), "memory.dream.skipped");
assert_eq!(event.data(), json!({"realm": "family", "reason": "budget"}));
assert_eq!(event.identity(), None);
let event = MemoryTimelineEvent::TaintTransition {
identity: Some("identity:luka".to_string()),
session_key: "sess-1".to_string(),
kind: "tainted".to_string(),
source: "mcp:web".to_string(),
};
assert_eq!(event.event_type(), "memory.taint.transition");
assert_eq!(event.identity(), Some("identity:luka"));
}
}