gemini_adk_rs/
state.rs

1//! Typed key-value state container for agents.
2//!
3//! Supports optional delta tracking for transactional state management
4//! and prefix-scoped accessors for namespace isolation.
5
6use std::collections::{HashMap, VecDeque};
7use std::marker::PhantomData;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::SystemTime;
11
12use dashmap::DashMap;
13
14use crate::clock::{SharedClock, system_clock};
15use serde_json::Value;
16
17const DEFAULT_MUTATION_JOURNAL_CAPACITY: usize = 1024;
18
19/// A compile-time typed state key that eliminates typo bugs and type mismatches.
20///
21/// Create as a const and use with `State::get_key()` / `State::set_key()`:
22///
23/// ```rust,ignore
24/// const TURN_COUNT: StateKey<u32> = StateKey::new("session:turn_count");
25/// const SENTIMENT: StateKey<String> = StateKey::new("derived:sentiment");
26///
27/// state.set_key(&TURN_COUNT, 5);
28/// let count: Option<u32> = state.get_key(&TURN_COUNT);
29/// ```
30pub struct StateKey<T> {
31    key: &'static str,
32    _phantom: PhantomData<fn() -> T>,
33}
34
35impl<T> StateKey<T> {
36    /// Create a new typed state key.
37    pub const fn new(key: &'static str) -> Self {
38        Self {
39            key,
40            _phantom: PhantomData,
41        }
42    }
43
44    /// The string key.
45    pub const fn key(&self) -> &'static str {
46        self.key
47    }
48}
49
50/// Where a state mutation came from.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum StateMutationOrigin {
54    /// Regular `State::set` or prefixed state write.
55    Set,
56    /// Direct committed-store write that bypasses delta tracking.
57    SetCommitted,
58    /// Removal of a single key.
59    Remove,
60    /// Removal caused by clearing a prefix.
61    ClearPrefix,
62    /// Delta changes committed into the base state.
63    Commit,
64}
65
66/// A single state mutation recorded in the bounded mutation journal.
67///
68/// Serializes to/from JSON for durable journaling (see [`JournalSink`]);
69/// `timestamp` is encoded as integer milliseconds since the Unix epoch.
70#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
71pub struct StateMutation {
72    /// Monotonic sequence number assigned when the mutation was recorded.
73    pub sequence: u64,
74    /// State key that changed.
75    pub key: String,
76    /// Value before the mutation, or `None` when the key did not exist.
77    pub old: Option<Value>,
78    /// Value after the mutation, or `None` when the key was removed.
79    pub new: Option<Value>,
80    /// Operation that recorded the mutation.
81    pub origin: StateMutationOrigin,
82    /// Wall-clock time at which the mutation was recorded.
83    /// Serialized as milliseconds since the Unix epoch (`timestamp_ms`).
84    #[serde(rename = "timestamp_ms", with = "systemtime_epoch_millis")]
85    pub timestamp: SystemTime,
86    /// Whether the mutation was written to a delta-tracked view.
87    pub delta: bool,
88}
89
90/// Serde codec mapping [`SystemTime`] to/from integer epoch milliseconds.
91mod systemtime_epoch_millis {
92    use std::time::{Duration, SystemTime, UNIX_EPOCH};
93
94    use serde::{Deserialize, Deserializer, Serializer};
95
96    pub(super) fn serialize<S: Serializer>(t: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
97        let millis = t
98            .duration_since(UNIX_EPOCH)
99            .map(|d| d.as_millis() as u64)
100            .unwrap_or(0);
101        ser.serialize_u64(millis)
102    }
103
104    pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
105        let millis = u64::deserialize(de)?;
106        Ok(UNIX_EPOCH + Duration::from_millis(millis))
107    }
108}
109
110/// Synchronous, durable sink for state mutations.
111///
112/// The in-memory mutation journal is a bounded ring (1024 entries) — long
113/// sessions lose history. A `JournalSink` receives every mutation as it is
114/// recorded so it can be persisted in full.
115///
116/// `write` runs on the state-write hot path (under the journal lock): it must
117/// be cheap, must not await, and must not panic — implementations log internal
118/// errors instead of surfacing them.
119pub trait JournalSink: Send + Sync {
120    /// Persist one mutation. Must not panic; log errors internally.
121    fn write(&self, m: &StateMutation);
122}
123
124/// Shared, swappable [`JournalSink`] slot — one slot per [`State`] family
125/// (clones and delta views share it, like the in-memory ring).
126#[derive(Clone, Default)]
127struct JournalSinkSlot(Arc<parking_lot::RwLock<Option<Arc<dyn JournalSink>>>>);
128
129/// The value that stands in for a redacted key wherever state leaves the
130/// process.
131pub const REDACTED: &str = "[redacted]";
132
133/// The redacted keys shared by a `State`, its clones and its delta views.
134#[derive(Clone, Default, Debug)]
135struct RedactionSlot(Arc<parking_lot::RwLock<Arc<std::collections::BTreeSet<String>>>>);
136
137/// The clock shared by a `State`, its clones and its delta views.
138#[derive(Clone)]
139struct ClockSlot(Arc<parking_lot::RwLock<SharedClock>>);
140
141impl Default for ClockSlot {
142    fn default() -> Self {
143        Self(Arc::new(parking_lot::RwLock::new(system_clock())))
144    }
145}
146
147impl std::fmt::Debug for ClockSlot {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_tuple("ClockSlot").field(&*self.0.read()).finish()
150    }
151}
152
153impl std::fmt::Debug for JournalSinkSlot {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        let installed = self.0.read().is_some();
156        f.debug_tuple("JournalSinkSlot").field(&installed).finish()
157    }
158}
159
160const JOURNAL_FLUSH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
161
162/// Log a journal-sink internal error without panicking the write path
163/// (journaling is infallible by contract, so the error is only reported).
164fn journal_log_error(context: &'static str, e: &dyn std::fmt::Display) {
165    tracing::warn!(error = %e, "{context}");
166}
167
168struct FileJournalInner {
169    writer: std::io::BufWriter<std::fs::File>,
170    last_flush: std::time::Instant,
171}
172
173/// Durable [`JournalSink`] writing one JSON object per line (JSONL).
174///
175/// Writes are buffered behind a `parking_lot::Mutex` and flushed at least
176/// every second and on drop. I/O errors are logged via `tracing::warn!` —
177/// journaling never panics a state write.
178///
179/// ```jsonl
180/// {"sequence":1,"key":"app:last_city","old":null,"new":"London","origin":"set","timestamp_ms":1718000000000,"delta":false}
181/// ```
182pub struct FileJournalSink {
183    inner: parking_lot::Mutex<FileJournalInner>,
184}
185
186impl FileJournalSink {
187    /// Create (truncating) the journal file at `path`.
188    pub fn create(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
189        let file = std::fs::File::create(path)?;
190        Ok(Self {
191            inner: parking_lot::Mutex::new(FileJournalInner {
192                writer: std::io::BufWriter::new(file),
193                last_flush: std::time::Instant::now(),
194            }),
195        })
196    }
197
198    /// Flush buffered mutations to disk now.
199    pub fn flush(&self) {
200        let mut inner = self.inner.lock();
201        if let Err(e) = std::io::Write::flush(&mut inner.writer) {
202            journal_log_error("FileJournalSink flush failed", &e);
203        }
204        inner.last_flush = std::time::Instant::now();
205    }
206}
207
208impl JournalSink for FileJournalSink {
209    fn write(&self, m: &StateMutation) {
210        let line = match serde_json::to_string(m) {
211            Ok(line) => line,
212            Err(e) => {
213                journal_log_error("FileJournalSink serialize failed", &e);
214                return;
215            }
216        };
217        let mut inner = self.inner.lock();
218        if let Err(e) = std::io::Write::write_all(&mut inner.writer, line.as_bytes())
219            .and_then(|()| std::io::Write::write_all(&mut inner.writer, b"\n"))
220        {
221            journal_log_error("FileJournalSink write failed", &e);
222            return;
223        }
224        if inner.last_flush.elapsed() >= JOURNAL_FLUSH_INTERVAL {
225            if let Err(e) = std::io::Write::flush(&mut inner.writer) {
226                journal_log_error("FileJournalSink flush failed", &e);
227            }
228            inner.last_flush = std::time::Instant::now();
229        }
230    }
231}
232
233/// Read a journal written by [`FileJournalSink`].
234pub fn read_journal(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<StateMutation>> {
235    parse_journal(&std::fs::read_to_string(path)?)
236}
237
238/// Parse journal JSONL text: one [`StateMutation`] per non-blank line.
239pub fn parse_journal(data: &str) -> std::io::Result<Vec<StateMutation>> {
240    data.lines()
241        .enumerate()
242        .filter(|(_, line)| !line.trim().is_empty())
243        .map(|(n, line)| {
244            serde_json::from_str(line).map_err(|e| {
245                std::io::Error::new(
246                    std::io::ErrorKind::InvalidData,
247                    format!("journal line {}: {e}", n + 1),
248                )
249            })
250        })
251        .collect()
252}
253
254impl Drop for FileJournalSink {
255    fn drop(&mut self) {
256        if let Err(e) = std::io::Write::flush(&mut self.inner.lock().writer) {
257            journal_log_error("FileJournalSink final flush failed", &e);
258        }
259    }
260}
261
262/// In-memory [`JournalSink`] for tests and replay harnesses. Unbounded.
263#[derive(Default)]
264pub struct MemoryJournalSink {
265    entries: parking_lot::Mutex<Vec<StateMutation>>,
266}
267
268impl MemoryJournalSink {
269    /// Create an empty sink.
270    pub fn new() -> Self {
271        Self::default()
272    }
273
274    /// Snapshot all recorded mutations (in write order).
275    pub fn entries(&self) -> Vec<StateMutation> {
276        self.entries.lock().clone()
277    }
278
279    /// Number of recorded mutations.
280    pub fn len(&self) -> usize {
281        self.entries.lock().len()
282    }
283
284    /// Whether nothing has been recorded yet.
285    pub fn is_empty(&self) -> bool {
286        self.entries.lock().is_empty()
287    }
288}
289
290impl JournalSink for MemoryJournalSink {
291    fn write(&self, m: &StateMutation) {
292        self.entries.lock().push(m.clone());
293    }
294}
295
296/// Error returned by fallible state reads and writes.
297#[derive(Debug, thiserror::Error)]
298pub enum StateError {
299    /// The value could not be serialized to JSON.
300    #[error("failed to serialize state value for key '{key}': {source}")]
301    Serialize {
302        /// The key that was being written.
303        key: String,
304        /// The underlying serde error.
305        source: serde_json::Error,
306    },
307    /// A value is present at the key but does not deserialize to the
308    /// requested type (see [`State::try_get`]).
309    #[error("state value at key '{key}' is not the requested type: {source}")]
310    WrongType {
311        /// The key that was being read.
312        key: String,
313        /// The underlying serde error.
314        source: serde_json::Error,
315    },
316}
317
318/// A pending write in a delta-tracked view.
319///
320/// Unlike a bare value, this distinguishes a *write* from a *removal* so that a
321/// delta can record tombstones and `rollback()` can restore the base state
322/// after removals and prefix clears.
323#[derive(Debug, Clone)]
324enum DeltaOp {
325    /// Set the key to this value on commit.
326    Put(Value),
327    /// Remove the key on commit (tombstone — shadows the committed value).
328    Delete,
329}
330
331/// Provenance and confidence for a single state slot — the evidence behind a
332/// value, aggregated from the mutation journal and the `state_meta:{key}` record.
333///
334/// This is what lets the model confirm principled-ly ("I heard 6, right?"):
335/// whether a slot was directly set, resolved from a system, or carries low
336/// confidence, and when it last changed.
337#[derive(Debug, Clone, serde::Serialize)]
338pub struct SlotEvidence {
339    /// The state key.
340    pub key: String,
341    /// Whether the key currently has a value.
342    pub present: bool,
343    /// The current value, if any.
344    pub value: Option<Value>,
345    /// Provenance source from `state_meta:{key}.source` (e.g. `agent`/`fetch`/
346    /// `llm`/`extraction`), if recorded.
347    pub source: Option<String>,
348    /// Confidence from `state_meta:{key}.confidence` (0.0–1.0), if recorded.
349    pub confidence: Option<f64>,
350    /// Journal sequence of the most recent write to this key, if still in the
351    /// bounded journal window.
352    pub last_sequence: Option<u64>,
353    /// Origin of the most recent recorded write, if known.
354    pub last_origin: Option<StateMutationOrigin>,
355}
356
357/// A concurrent, type-safe state container that agents read from and write to.
358///
359/// By default, `set()` writes directly to the inner store. When delta tracking
360/// is enabled via `with_delta_tracking()`, writes go to a separate delta map
361/// (with tombstones) that can be atomically committed or rolled back.
362#[derive(Debug, Clone)]
363pub struct State {
364    inner: Arc<DashMap<String, Value>>,
365    delta: Arc<DashMap<String, DeltaOp>>,
366    mutations: Arc<std::sync::Mutex<VecDeque<StateMutation>>>,
367    next_mutation_sequence: Arc<AtomicU64>,
368    mutation_capacity: usize,
369    journal_sink: JournalSinkSlot,
370    clock: ClockSlot,
371    redacted: RedactionSlot,
372    track_delta: bool,
373}
374
375impl Default for State {
376    fn default() -> Self {
377        Self::new()
378    }
379}
380
381impl State {
382    /// Create a new empty state container.
383    pub fn new() -> Self {
384        Self {
385            inner: Arc::new(DashMap::new()),
386            delta: Arc::new(DashMap::new()),
387            mutations: Arc::new(std::sync::Mutex::new(VecDeque::new())),
388            next_mutation_sequence: Arc::new(AtomicU64::new(1)),
389            mutation_capacity: DEFAULT_MUTATION_JOURNAL_CAPACITY,
390            journal_sink: JournalSinkSlot::default(),
391            clock: ClockSlot::default(),
392            redacted: RedactionSlot::default(),
393            track_delta: false,
394        }
395    }
396
397    /// Create a new State with delta tracking enabled.
398    /// Writes go to the delta map; reads check delta first, then inner.
399    pub fn with_delta_tracking(&self) -> State {
400        State {
401            inner: self.inner.clone(),
402            delta: Arc::new(DashMap::new()),
403            mutations: self.mutations.clone(),
404            next_mutation_sequence: self.next_mutation_sequence.clone(),
405            mutation_capacity: self.mutation_capacity,
406            journal_sink: self.journal_sink.clone(),
407            clock: self.clock.clone(),
408            redacted: self.redacted.clone(),
409            track_delta: true,
410        }
411    }
412
413    /// Install a durable [`JournalSink`] that receives every state mutation.
414    ///
415    /// The sink is shared with all clones and delta views of this `State`
416    /// (like the in-memory ring) and is invoked synchronously on the write
417    /// path — keep it cheap. The in-memory ring keeps serving
418    /// [`recent_mutations`](Self::recent_mutations)/[`evidence`](Self::evidence);
419    /// the sink adds unbounded durability.
420    pub fn set_journal_sink(&self, sink: Arc<dyn JournalSink>) {
421        *self.journal_sink.0.write() = Some(sink);
422    }
423
424    /// Builder-style variant of [`set_journal_sink`](Self::set_journal_sink).
425    pub fn with_journal_sink(self, sink: Arc<dyn JournalSink>) -> Self {
426        self.set_journal_sink(sink);
427        self
428    }
429
430    /// Replace the [`Clock`](crate::clock::Clock) this state and everything
431    /// sharing it read the time from. Defaults to the system clock.
432    ///
433    /// The clock is shared with every clone and delta view, like the journal
434    /// sink. Swap it before a session starts; components that captured an
435    /// instant from the old clock compare it against the new one.
436    pub fn set_clock(&self, clock: SharedClock) {
437        *self.clock.0.write() = clock;
438    }
439
440    /// Builder-style variant of [`set_clock`](Self::set_clock).
441    pub fn with_clock(self, clock: SharedClock) -> Self {
442        self.set_clock(clock);
443        self
444    }
445
446    /// The clock this state reads the time from.
447    pub fn clock(&self) -> SharedClock {
448        self.clock.0.read().clone()
449    }
450
451    /// Mark keys as sensitive: wherever this state's values leave the
452    /// process, a redacted key's value is replaced by [`REDACTED`]. That
453    /// covers the durable journal sink, persistence snapshots
454    /// ([`to_redacted_hashmap`](Self::to_redacted_hashmap)) and the runtime's
455    /// extraction events. Reads inside the process (`get`, guards, tools)
456    /// still see the real value.
457    ///
458    /// A key `k` also covers its scoped forms: `app:k`, `user:k`,
459    /// `state_meta:k`, and any other `prefix:k`. Shared with clones and
460    /// delta views. Adds to the keys already marked.
461    pub fn redact_keys<I, S>(&self, keys: I)
462    where
463        I: IntoIterator<Item = S>,
464        S: Into<String>,
465    {
466        let mut guard = self.redacted.0.write();
467        let mut set = (**guard).clone();
468        set.extend(keys.into_iter().map(Into::into));
469        *guard = Arc::new(set);
470    }
471
472    /// The keys marked sensitive with [`redact_keys`](Self::redact_keys).
473    pub fn redacted_keys(&self) -> std::collections::BTreeSet<String> {
474        (**self.redacted.0.read()).clone()
475    }
476
477    /// Whether `key` (or the base key under its scope prefix) is marked
478    /// sensitive.
479    pub fn is_redacted(&self, key: &str) -> bool {
480        let set = self.redacted.0.read().clone();
481        if set.is_empty() {
482            return false;
483        }
484        set.contains(key)
485            || key
486                .rsplit_once(':')
487                .is_some_and(|(_, base)| set.contains(base))
488    }
489
490    /// `value` as it may leave the process under `key`.
491    pub fn redact_value(&self, key: &str, value: &Value) -> Value {
492        if self.is_redacted(key) {
493            Value::String(REDACTED.into())
494        } else {
495            value.clone()
496        }
497    }
498
499    /// Mask the redacted fields of a JSON object (one level deep); other
500    /// values pass through.
501    pub fn redact_fields(&self, value: &Value) -> Value {
502        match value {
503            Value::Object(obj) if !self.redacted.0.read().is_empty() => Value::Object(
504                obj.iter()
505                    .map(|(k, v)| (k.clone(), self.redact_value(k, v)))
506                    .collect(),
507            ),
508            other => other.clone(),
509        }
510    }
511
512    /// [`to_hashmap`](Self::to_hashmap) with redacted keys masked: what a
513    /// persistence snapshot or an export should hold.
514    pub fn to_redacted_hashmap(&self) -> HashMap<String, Value> {
515        self.to_hashmap()
516            .into_iter()
517            .map(|(k, v)| {
518                let v = self.redact_value(&k, &v);
519                (k, v)
520            })
521            .collect()
522    }
523
524    /// Get a value by key, attempting to deserialize to the requested type.
525    /// When delta tracking is enabled, checks delta first, then inner.
526    ///
527    /// This is the *lenient* read: a value that is present but of the wrong
528    /// type is reported as `None`, indistinguishable from an absent key. Use
529    /// [`try_get`](Self::try_get) when that distinction matters.
530    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
531        self.get_raw(key)
532            .and_then(|v| serde_json::from_value(v).ok())
533    }
534
535    /// Get a value by key, distinguishing "absent" from "present but the wrong
536    /// type".
537    ///
538    /// Returns `Ok(None)` when no value is stored at `key` (after the same
539    /// delta → inner → `derived:` lookup as [`get`](Self::get)), `Ok(Some(v))`
540    /// when the stored value deserializes to `T`, and
541    /// [`StateError::WrongType`] when a value exists but does not. This is the
542    /// *strict* read; [`get`](Self::get) is the lenient form that folds the
543    /// error case into `None`.
544    pub fn try_get<T: serde::de::DeserializeOwned>(
545        &self,
546        key: &str,
547    ) -> Result<Option<T>, StateError> {
548        match self.get_raw(key) {
549            None => Ok(None),
550            Some(v) => {
551                serde_json::from_value(v)
552                    .map(Some)
553                    .map_err(|source| StateError::WrongType {
554                        key: key.to_string(),
555                        source,
556                    })
557            }
558        }
559    }
560
561    /// Borrow a value by key without cloning, applying `f` to the reference.
562    ///
563    /// This is the zero-copy alternative to `get_raw()`. The closure receives
564    /// a `&Value` directly from the DashMap ref-guard, avoiding the
565    /// `Value::clone()` + `serde_json::from_value()` overhead of `get()`.
566    ///
567    /// Lookup order: delta (if tracking) → inner → derived fallback.
568    pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
569    where
570        F: FnOnce(&Value) -> R,
571    {
572        if self.track_delta {
573            match self.delta.get(key).map(|r| r.value().clone()) {
574                Some(DeltaOp::Put(v)) => return Some(f(&v)),
575                Some(DeltaOp::Delete) => return None, // tombstone shadows inner
576                None => {}
577            }
578        }
579        if let Some(ref_multi) = self.inner.get(key) {
580            return Some(f(ref_multi.value()));
581        }
582        if !key.contains(':') {
583            let mut derived_key = String::with_capacity(8 + key.len());
584            use std::fmt::Write;
585            let _ = write!(derived_key, "derived:{key}");
586            if self.track_delta {
587                match self.delta.get(&derived_key).map(|r| r.value().clone()) {
588                    Some(DeltaOp::Put(v)) => return Some(f(&v)),
589                    Some(DeltaOp::Delete) => return None,
590                    None => {}
591                }
592            }
593            if let Some(ref_multi) = self.inner.get(&derived_key) {
594                return Some(f(ref_multi.value()));
595            }
596        }
597        None
598    }
599
600    /// Get a raw JSON value by key.
601    /// When delta tracking is enabled, checks delta first, then inner.
602    /// If the key is not found and doesn't contain a prefix, also checks `derived:{key}`
603    /// as a transparent fallback for computed variables.
604    pub fn get_raw(&self, key: &str) -> Option<Value> {
605        if self.track_delta {
606            match self.delta.get(key).map(|r| r.value().clone()) {
607                Some(DeltaOp::Put(v)) => return Some(v),
608                Some(DeltaOp::Delete) => return None, // tombstone shadows inner
609                None => {}
610            }
611        }
612        if let Some(v) = self.inner.get(key) {
613            return Some(v.value().clone());
614        }
615        // Transparent derived fallback: if key has no prefix, check derived:{key}
616        if !key.contains(':') {
617            use std::fmt::Write;
618            let mut derived_key = String::with_capacity(8 + key.len());
619            let _ = write!(derived_key, "derived:{key}");
620            if self.track_delta {
621                match self.delta.get(&derived_key).map(|r| r.value().clone()) {
622                    Some(DeltaOp::Put(v)) => return Some(v),
623                    Some(DeltaOp::Delete) => return None,
624                    None => {}
625                }
626            }
627            return self.inner.get(&derived_key).map(|v| v.value().clone());
628        }
629        None
630    }
631
632    /// Get a typed value using a `StateKey<T>` (lenient — a wrong-typed value
633    /// reads as `None`; see [`get`](Self::get)).
634    pub fn get_key<T: serde::de::DeserializeOwned>(&self, key: &StateKey<T>) -> Option<T> {
635        self.get(key.key())
636    }
637
638    /// Get a typed value using a `StateKey<T>`, distinguishing "absent" from
639    /// "present but the wrong type" (see [`try_get`](Self::try_get)).
640    pub fn try_get_key<T: serde::de::DeserializeOwned>(
641        &self,
642        key: &StateKey<T>,
643    ) -> Result<Option<T>, StateError> {
644        self.try_get(key.key())
645    }
646
647    /// Set a typed value using a `StateKey<T>`.
648    ///
649    /// Returns [`StateError`] if `value` cannot be serialized to JSON.
650    pub fn set_key<T: serde::Serialize>(
651        &self,
652        key: &StateKey<T>,
653        value: T,
654    ) -> Result<(), StateError> {
655        self.set(key.key(), value)
656    }
657
658    /// Zero-copy borrow using a `StateKey<T>`.
659    pub fn with_key<T, F, R>(&self, key: &StateKey<T>, f: F) -> Option<R>
660    where
661        F: FnOnce(&Value) -> R,
662    {
663        self.with(key.key(), f)
664    }
665
666    /// Set a value by key.
667    ///
668    /// When delta tracking is enabled, writes to the delta view instead of the
669    /// committed store. Returns [`StateError`] if `value` cannot be serialized
670    /// to JSON — a public SDK write never panics on caller data.
671    pub fn set(
672        &self,
673        key: impl Into<String>,
674        value: impl serde::Serialize,
675    ) -> Result<(), StateError> {
676        let key = key.into();
677        let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
678            key: key.clone(),
679            source,
680        })?;
681        self.put_value(key, v, StateMutationOrigin::Set);
682        Ok(())
683    }
684
685    /// Infallible internal write of an already-serialized [`Value`].
686    ///
687    /// Shared by `set` and the value-level helpers (`merge`/`pick`/`rename`/
688    /// `from_hashmap`) so those do not re-serialize and cannot fail.
689    fn put_value(&self, key: String, v: Value, origin: StateMutationOrigin) {
690        let old = self.get_raw(&key);
691        if self.track_delta {
692            self.delta.insert(key.clone(), DeltaOp::Put(v.clone()));
693        } else {
694            self.inner.insert(key.clone(), v.clone());
695        }
696        self.record_mutation(key, old, Some(v), origin);
697    }
698
699    /// Set a value directly in the committed store, bypassing delta tracking.
700    ///
701    /// Returns [`StateError`] if `value` cannot be serialized to JSON.
702    pub fn set_committed(
703        &self,
704        key: impl Into<String>,
705        value: impl serde::Serialize,
706    ) -> Result<(), StateError> {
707        let key = key.into();
708        let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
709            key: key.clone(),
710            source,
711        })?;
712        let old = self.inner.insert(key.clone(), v.clone());
713        self.record_mutation(key, old, Some(v), StateMutationOrigin::SetCommitted);
714        Ok(())
715    }
716
717    /// Atomically read-modify-write a value under a per-key lock.
718    ///
719    /// If the key doesn't exist, `default` is used as the initial value. The
720    /// function `f` receives the current value and returns the new value. The
721    /// read-modify-write is performed while holding the map shard for `key`, so
722    /// concurrent `modify` calls on the same key do not lose updates. Returns
723    /// the new value, or [`StateError`] if it cannot be serialized.
724    pub fn modify<T, F>(&self, key: &str, default: T, f: F) -> Result<T, StateError>
725    where
726        T: serde::Serialize + serde::de::DeserializeOwned,
727        F: FnOnce(T) -> T,
728    {
729        use dashmap::mapref::entry::Entry;
730
731        let serialize = |key: &str, val: &T| {
732            serde_json::to_value(val).map_err(|source| StateError::Serialize {
733                key: key.to_string(),
734                source,
735            })
736        };
737
738        if self.track_delta {
739            // Atomic w.r.t. the delta shard; the committed base is read as the
740            // initial value only when the delta has no entry for this key.
741            match self.delta.entry(key.to_string()) {
742                Entry::Occupied(mut o) => {
743                    let current = match o.get() {
744                        DeltaOp::Put(v) => serde_json::from_value(v.clone()).unwrap_or(default),
745                        DeltaOp::Delete => default,
746                    };
747                    let old = self.inner.get(key).map(|r| r.value().clone());
748                    let new_val = f(current);
749                    let v = serialize(key, &new_val)?;
750                    o.insert(DeltaOp::Put(v.clone()));
751                    self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
752                    Ok(new_val)
753                }
754                Entry::Vacant(slot) => {
755                    let base = self
756                        .inner
757                        .get(key)
758                        .and_then(|r| serde_json::from_value(r.value().clone()).ok());
759                    let old = self.inner.get(key).map(|r| r.value().clone());
760                    let new_val = f(base.unwrap_or(default));
761                    let v = serialize(key, &new_val)?;
762                    slot.insert(DeltaOp::Put(v.clone()));
763                    self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
764                    Ok(new_val)
765                }
766            }
767        } else {
768            match self.inner.entry(key.to_string()) {
769                Entry::Occupied(mut o) => {
770                    let old = o.get().clone();
771                    let current = serde_json::from_value(old.clone()).unwrap_or(default);
772                    let new_val = f(current);
773                    let v = serialize(key, &new_val)?;
774                    o.insert(v.clone());
775                    self.record_mutation(
776                        key.to_string(),
777                        Some(old),
778                        Some(v),
779                        StateMutationOrigin::Set,
780                    );
781                    Ok(new_val)
782                }
783                Entry::Vacant(slot) => {
784                    let new_val = f(default);
785                    let v = serialize(key, &new_val)?;
786                    slot.insert(v.clone());
787                    self.record_mutation(key.to_string(), None, Some(v), StateMutationOrigin::Set);
788                    Ok(new_val)
789                }
790            }
791        }
792    }
793
794    /// Check if a key exists (in delta or inner).
795    ///
796    /// Applies the same transparent `derived:` fallback as [`Self::get`],
797    /// [`Self::get_raw`] and [`Self::with`]: an unprefixed key also matches the
798    /// computed variable `derived:{key}`. Flow predicates (`is_set`, `captured`)
799    /// evaluate through this method, so without the fallback a computed value
800    /// would read as permanently unknown while `get` returned it fine.
801    pub fn contains(&self, key: &str) -> bool {
802        if self.track_delta {
803            match self.delta.get(key).map(|r| r.value().clone()) {
804                Some(DeltaOp::Put(_)) => return true,
805                Some(DeltaOp::Delete) => return false, // tombstone shadows inner
806                None => {}
807            }
808        }
809        if self.inner.contains_key(key) {
810            return true;
811        }
812        if !key.contains(':') {
813            let derived_key = format!("derived:{key}");
814            if self.track_delta {
815                match self.delta.get(&derived_key).map(|r| r.value().clone()) {
816                    Some(DeltaOp::Put(_)) => return true,
817                    Some(DeltaOp::Delete) => return false,
818                    None => {}
819                }
820            }
821            return self.inner.contains_key(&derived_key);
822        }
823        false
824    }
825
826    /// Remove a key.
827    ///
828    /// In delta-tracking mode this records a tombstone in the delta view and
829    /// leaves the committed store untouched, so a subsequent `rollback()` fully
830    /// restores the base state. Returns the value that was visible before removal.
831    pub fn remove(&self, key: &str) -> Option<Value> {
832        if self.track_delta {
833            let removed = self.get_raw(key);
834            // Tombstone in the delta — never mutate `inner` directly, so rollback
835            // can restore the committed value.
836            self.delta.insert(key.to_string(), DeltaOp::Delete);
837            if let Some(ref old) = removed {
838                self.record_mutation(
839                    key.to_string(),
840                    Some(old.clone()),
841                    None,
842                    StateMutationOrigin::Remove,
843                );
844            }
845            removed
846        } else {
847            let removed = self.inner.remove(key).map(|(_, v)| v);
848            if let Some(ref old) = removed {
849                self.record_mutation(
850                    key.to_string(),
851                    Some(old.clone()),
852                    None,
853                    StateMutationOrigin::Remove,
854                );
855            }
856            removed
857        }
858    }
859
860    /// Get all keys (from both inner and delta when tracking).
861    ///
862    /// Keys tombstoned in the delta are excluded.
863    pub fn keys(&self) -> Vec<String> {
864        if !self.track_delta || self.delta.is_empty() {
865            return self.inner.iter().map(|r| r.key().clone()).collect();
866        }
867        let mut seen =
868            std::collections::HashSet::with_capacity(self.inner.len() + self.delta.len());
869        let mut keys = Vec::with_capacity(self.inner.len() + self.delta.len());
870        // Delta first so tombstones win over committed entries.
871        for entry in self.delta.iter() {
872            let key = entry.key().clone();
873            seen.insert(key.clone());
874            if matches!(entry.value(), DeltaOp::Put(_)) {
875                keys.push(key);
876            }
877        }
878        for entry in self.inner.iter() {
879            let key = entry.key().clone();
880            if seen.insert(key.clone()) {
881                keys.push(key);
882            }
883        }
884        keys
885    }
886
887    /// Create a new State containing only the specified keys.
888    pub fn pick(&self, keys: &[&str]) -> State {
889        let new = State::new().with_clock(self.clock());
890        new.redact_keys(self.redacted_keys());
891        for key in keys {
892            if let Some(v) = self.get_raw(key) {
893                new.put_value((*key).to_string(), v, StateMutationOrigin::Set);
894            }
895        }
896        new
897    }
898
899    /// Merge another state into this one (other's values overwrite on conflict).
900    pub fn merge(&self, other: &State) {
901        for entry in other.inner.iter() {
902            self.put_value(
903                entry.key().clone(),
904                entry.value().clone(),
905                StateMutationOrigin::Set,
906            );
907        }
908    }
909
910    /// Rename a key.
911    pub fn rename(&self, from: &str, to: &str) {
912        if let Some(v) = self.remove(from) {
913            self.put_value(to.to_string(), v, StateMutationOrigin::Set);
914        }
915    }
916
917    // ── Delta methods ──────────────────────────────────────────────────────
918
919    /// Whether delta tracking is enabled.
920    pub fn is_tracking_delta(&self) -> bool {
921        self.track_delta
922    }
923
924    /// Whether there are uncommitted delta changes.
925    pub fn has_delta(&self) -> bool {
926        self.track_delta && !self.delta.is_empty()
927    }
928
929    /// Get a snapshot of the current delta's pending writes (tombstones omitted).
930    pub fn delta(&self) -> HashMap<String, Value> {
931        self.delta
932            .iter()
933            .filter_map(|entry| match entry.value() {
934                DeltaOp::Put(v) => Some((entry.key().clone(), v.clone())),
935                DeltaOp::Delete => None,
936            })
937            .collect()
938    }
939
940    /// Commit delta changes into the inner store, then clear the delta.
941    ///
942    /// Pending puts are applied and tombstones remove the committed key, so a
943    /// removal made under delta tracking becomes durable only at commit time.
944    pub fn commit(&self) {
945        // Snapshot first so we don't iterate the delta while mutating `inner`.
946        let ops: Vec<(String, DeltaOp)> = self
947            .delta
948            .iter()
949            .map(|e| (e.key().clone(), e.value().clone()))
950            .collect();
951        for (key, op) in ops {
952            match op {
953                DeltaOp::Put(value) => {
954                    let old = self.inner.insert(key.clone(), value.clone());
955                    self.record_mutation_with_delta(
956                        key,
957                        old,
958                        Some(value),
959                        StateMutationOrigin::Commit,
960                        false,
961                    );
962                }
963                DeltaOp::Delete => {
964                    if let Some((_, old)) = self.inner.remove(&key) {
965                        self.record_mutation_with_delta(
966                            key,
967                            Some(old),
968                            None,
969                            StateMutationOrigin::Commit,
970                            false,
971                        );
972                    }
973                }
974            }
975        }
976        self.delta.clear();
977    }
978
979    /// Discard all uncommitted delta changes, restoring the committed base state.
980    ///
981    /// Because removals and prefix clears under delta tracking only write
982    /// tombstones (never mutating `inner`), dropping the delta is sufficient to
983    /// restore the base — including keys that were removed in the transaction.
984    pub fn rollback(&self) {
985        self.delta.clear();
986    }
987
988    // ── Prefix accessors ───────────────────────────────────────────────────
989
990    /// Access state with the `app:` prefix scope.
991    pub fn app(&self) -> PrefixedState<'_> {
992        PrefixedState {
993            state: self,
994            prefix: "app:",
995        }
996    }
997
998    /// Access state with the `user:` prefix scope.
999    pub fn user(&self) -> PrefixedState<'_> {
1000        PrefixedState {
1001            state: self,
1002            prefix: "user:",
1003        }
1004    }
1005
1006    /// Access state with the `temp:` prefix scope.
1007    pub fn temp(&self) -> PrefixedState<'_> {
1008        PrefixedState {
1009            state: self,
1010            prefix: "temp:",
1011        }
1012    }
1013
1014    /// Access state with the `session:` prefix scope (auto-tracked signals).
1015    pub fn session(&self) -> PrefixedState<'_> {
1016        PrefixedState {
1017            state: self,
1018            prefix: "session:",
1019        }
1020    }
1021
1022    /// Access state with the `turn:` prefix scope (reset each turn).
1023    pub fn turn(&self) -> PrefixedState<'_> {
1024        PrefixedState {
1025            state: self,
1026            prefix: "turn:",
1027        }
1028    }
1029
1030    /// Access state with the `bg:` prefix scope (background tasks).
1031    pub fn bg(&self) -> PrefixedState<'_> {
1032        PrefixedState {
1033            state: self,
1034            prefix: "bg:",
1035        }
1036    }
1037
1038    /// Access read-only state with the `derived:` prefix scope (computed vars only).
1039    pub fn derived(&self) -> ReadOnlyPrefixedState<'_> {
1040        ReadOnlyPrefixedState {
1041            state: self,
1042            prefix: "derived:",
1043        }
1044    }
1045
1046    // ── Utility methods ───────────────────────────────────────────────────
1047
1048    /// Snapshot the values of specific keys. Returns HashMap of key -> current value.
1049    /// Used by watchers to capture state before mutations.
1050    pub fn snapshot_values(&self, keys: &[&str]) -> HashMap<String, Value> {
1051        keys.iter()
1052            .filter_map(|&k| self.get_raw(k).map(|v| (k.to_string(), v)))
1053            .collect()
1054    }
1055
1056    /// Diff current state against a previous snapshot.
1057    /// Returns Vec of (key, old_value, new_value) for keys that changed.
1058    pub fn diff_values(
1059        &self,
1060        prev: &HashMap<String, Value>,
1061        keys: &[&str],
1062    ) -> Vec<(String, Value, Value)> {
1063        keys.iter()
1064            .filter_map(|&k| {
1065                let old = prev.get(k);
1066                let new = self.get_raw(k);
1067                match (old, new) {
1068                    (Some(o), Some(n)) if o != &n => Some((k.to_string(), o.clone(), n)),
1069                    (None, Some(n)) => Some((k.to_string(), Value::Null, n)),
1070                    (Some(o), None) => Some((k.to_string(), o.clone(), Value::Null)),
1071                    _ => None,
1072                }
1073            })
1074            .collect()
1075    }
1076
1077    /// Export all state as a HashMap (for persistence/serialization).
1078    pub fn to_hashmap(&self) -> std::collections::HashMap<String, serde_json::Value> {
1079        self.inner
1080            .iter()
1081            .map(|entry| (entry.key().clone(), entry.value().clone()))
1082            .collect()
1083    }
1084
1085    /// Restore state from a HashMap (for persistence/deserialization).
1086    pub fn from_hashmap(&self, map: std::collections::HashMap<String, serde_json::Value>) {
1087        for (key, value) in map {
1088            // Values are already `Value`, so this write cannot fail to serialize.
1089            let old = self.inner.insert(key.clone(), value.clone());
1090            self.record_mutation(key, old, Some(value), StateMutationOrigin::SetCommitted);
1091        }
1092    }
1093
1094    /// Remove all keys with the given prefix.
1095    ///
1096    /// In delta-tracking mode this writes tombstones for matching keys (from both
1097    /// the committed store and pending delta puts) without mutating the committed
1098    /// store, so `rollback()` restores everything that was cleared.
1099    pub fn clear_prefix(&self, prefix: &str) {
1100        if self.track_delta {
1101            let keys: Vec<String> = self
1102                .keys()
1103                .into_iter()
1104                .filter(|k| k.starts_with(prefix))
1105                .collect();
1106            for key in keys {
1107                let old = self.get_raw(&key);
1108                self.delta.insert(key.clone(), DeltaOp::Delete);
1109                if let Some(old) = old {
1110                    self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1111                }
1112            }
1113            return;
1114        }
1115        let keys_to_remove: Vec<String> = self
1116            .inner
1117            .iter()
1118            .filter(|entry| entry.key().starts_with(prefix))
1119            .map(|entry| entry.key().clone())
1120            .collect();
1121        for key in keys_to_remove {
1122            if let Some((_, old)) = self.inner.remove(&key) {
1123                self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1124            }
1125        }
1126    }
1127
1128    /// Return a snapshot of recent state mutations.
1129    pub fn recent_mutations(&self) -> Vec<StateMutation> {
1130        self.mutations
1131            .lock()
1132            .expect("state mutation journal poisoned")
1133            .iter()
1134            .cloned()
1135            .collect()
1136    }
1137
1138    /// Return the current monotonic cursor for the mutation journal.
1139    pub fn mutation_cursor(&self) -> u64 {
1140        self.next_mutation_sequence.load(Ordering::Relaxed) - 1
1141    }
1142
1143    /// Return mutations appended after a previously captured cursor.
1144    pub fn mutations_since(&self, cursor: u64) -> Vec<StateMutation> {
1145        let mutations = self
1146            .mutations
1147            .lock()
1148            .expect("state mutation journal poisoned");
1149        mutations
1150            .iter()
1151            .filter(|mutation| mutation.sequence > cursor)
1152            .cloned()
1153            .collect()
1154    }
1155
1156    /// Mutations appended after `cursor`, or `None` if the bounded journal
1157    /// has already dropped some of them.
1158    ///
1159    /// A consumer that must see every change (a derived-value cache, say)
1160    /// uses this instead of [`mutations_since`](Self::mutations_since) and
1161    /// falls back to a full rescan on `None`.
1162    pub fn try_mutations_since(&self, cursor: u64) -> Option<Vec<StateMutation>> {
1163        let mutations = self
1164            .mutations
1165            .lock()
1166            .expect("state mutation journal poisoned");
1167        let latest = self.next_mutation_sequence.load(Ordering::Relaxed) - 1;
1168        if latest > cursor {
1169            let oldest = mutations.front().map_or(u64::MAX, |m| m.sequence);
1170            if oldest > cursor + 1 {
1171                return None;
1172            }
1173        }
1174        Some(
1175            mutations
1176                .iter()
1177                .filter(|mutation| mutation.sequence > cursor)
1178                .cloned()
1179                .collect(),
1180        )
1181    }
1182
1183    /// Drain and return all recorded state mutations.
1184    pub fn drain_mutations(&self) -> Vec<StateMutation> {
1185        self.mutations
1186            .lock()
1187            .expect("state mutation journal poisoned")
1188            .drain(..)
1189            .collect()
1190    }
1191
1192    /// Aggregate the [`SlotEvidence`] for a key: its current value, provenance
1193    /// (`state_meta:{key}`), confidence, and most-recent journal write.
1194    pub fn evidence(&self, key: &str) -> SlotEvidence {
1195        let value = self.get_raw(key);
1196        let meta = self.get::<Value>(&format!("state_meta:{key}"));
1197        let source = meta
1198            .as_ref()
1199            .and_then(|m| m.get("source"))
1200            .and_then(|s| s.as_str().map(String::from));
1201        let confidence = meta
1202            .as_ref()
1203            .and_then(|m| m.get("confidence"))
1204            .and_then(serde_json::Value::as_f64);
1205
1206        let mut last_sequence: Option<u64> = None;
1207        let mut last_origin: Option<StateMutationOrigin> = None;
1208        for m in self.recent_mutations() {
1209            if m.key == key && last_sequence.is_none_or(|s| m.sequence > s) {
1210                last_sequence = Some(m.sequence);
1211                last_origin = Some(m.origin);
1212            }
1213        }
1214
1215        SlotEvidence {
1216            key: key.to_string(),
1217            present: value.is_some(),
1218            value,
1219            source,
1220            confidence,
1221            last_sequence,
1222            last_origin,
1223        }
1224    }
1225
1226    fn record_mutation(
1227        &self,
1228        key: String,
1229        old: Option<Value>,
1230        new: Option<Value>,
1231        origin: StateMutationOrigin,
1232    ) {
1233        self.record_mutation_with_delta(key, old, new, origin, self.track_delta);
1234    }
1235
1236    fn record_mutation_with_delta(
1237        &self,
1238        key: String,
1239        old: Option<Value>,
1240        new: Option<Value>,
1241        origin: StateMutationOrigin,
1242        delta: bool,
1243    ) {
1244        let mut mutations = self
1245            .mutations
1246            .lock()
1247            .expect("state mutation journal poisoned");
1248        if mutations.len() >= self.mutation_capacity {
1249            mutations.pop_front();
1250        }
1251        let sequence = self.next_mutation_sequence.fetch_add(1, Ordering::Relaxed);
1252        let mutation = StateMutation {
1253            sequence,
1254            key,
1255            old,
1256            new,
1257            origin,
1258            timestamp: self.clock().system_time(),
1259            delta,
1260        };
1261        // Durable sink runs under the journal lock so the file order matches
1262        // the ring order exactly. Sinks are sync + cheap by contract.
1263        if let Some(sink) = self.journal_sink.0.read().as_ref() {
1264            if self.is_redacted(&mutation.key) {
1265                let masked = |v: &Option<Value>| v.as_ref().map(|_| Value::String(REDACTED.into()));
1266                sink.write(&StateMutation {
1267                    old: masked(&mutation.old),
1268                    new: masked(&mutation.new),
1269                    ..mutation.clone()
1270                });
1271            } else {
1272                sink.write(&mutation);
1273            }
1274        }
1275        mutations.push_back(mutation);
1276    }
1277}
1278
1279/// A borrowed view of state that automatically prepends a prefix to all keys.
1280pub struct PrefixedState<'a> {
1281    state: &'a State,
1282    prefix: &'static str,
1283}
1284
1285impl<'a> PrefixedState<'a> {
1286    fn prefixed_key(&self, key: &str) -> String {
1287        format!("{}{}", self.prefix, key)
1288    }
1289
1290    /// Get a value by key (with prefix applied).
1291    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1292        self.state.get(&self.prefixed_key(key))
1293    }
1294
1295    /// Get a raw JSON value by key (with prefix applied).
1296    pub fn get_raw(&self, key: &str) -> Option<Value> {
1297        self.state.get_raw(&self.prefixed_key(key))
1298    }
1299
1300    /// Zero-copy borrow a value by key (with prefix applied).
1301    pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1302    where
1303        F: FnOnce(&Value) -> R,
1304    {
1305        self.state.with(&self.prefixed_key(key), f)
1306    }
1307
1308    /// Set a value by key (with prefix applied).
1309    ///
1310    /// Returns [`StateError`] if `value` cannot be serialized to JSON.
1311    pub fn set(
1312        &self,
1313        key: impl AsRef<str>,
1314        value: impl serde::Serialize,
1315    ) -> Result<(), StateError> {
1316        self.state.set(self.prefixed_key(key.as_ref()), value)
1317    }
1318
1319    /// Check if a key exists (with prefix applied).
1320    pub fn contains(&self, key: &str) -> bool {
1321        self.state.contains(&self.prefixed_key(key))
1322    }
1323
1324    /// Remove a key (with prefix applied).
1325    pub fn remove(&self, key: &str) -> Option<Value> {
1326        self.state.remove(&self.prefixed_key(key))
1327    }
1328
1329    /// Get all keys within this prefix scope (prefix stripped from results).
1330    pub fn keys(&self) -> Vec<String> {
1331        self.state
1332            .keys()
1333            .into_iter()
1334            .filter_map(|k| {
1335                k.strip_prefix(self.prefix)
1336                    .map(std::string::ToString::to_string)
1337            })
1338            .collect()
1339    }
1340}
1341
1342/// A borrowed, read-only view of state that automatically prepends a prefix to all keys.
1343///
1344/// Unlike `PrefixedState`, this does not expose `set()` or `remove()` methods,
1345/// making it suitable for computed/derived state that user code should not mutate.
1346pub struct ReadOnlyPrefixedState<'a> {
1347    state: &'a State,
1348    prefix: &'static str,
1349}
1350
1351impl<'a> ReadOnlyPrefixedState<'a> {
1352    fn prefixed_key(&self, key: &str) -> String {
1353        format!("{}{}", self.prefix, key)
1354    }
1355
1356    /// Get a value by key (with prefix applied).
1357    pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1358        self.state.get(&self.prefixed_key(key))
1359    }
1360
1361    /// Get a raw JSON value by key (with prefix applied).
1362    pub fn get_raw(&self, key: &str) -> Option<Value> {
1363        self.state.get_raw(&self.prefixed_key(key))
1364    }
1365
1366    /// Zero-copy borrow a value by key (with prefix applied).
1367    pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1368    where
1369        F: FnOnce(&Value) -> R,
1370    {
1371        self.state.with(&self.prefixed_key(key), f)
1372    }
1373
1374    /// Check if a key exists (with prefix applied).
1375    pub fn contains(&self, key: &str) -> bool {
1376        self.state.contains(&self.prefixed_key(key))
1377    }
1378
1379    /// Get all keys within this prefix scope (prefix stripped from results).
1380    pub fn keys(&self) -> Vec<String> {
1381        self.state
1382            .keys()
1383            .into_iter()
1384            .filter_map(|k| {
1385                k.strip_prefix(self.prefix)
1386                    .map(std::string::ToString::to_string)
1387            })
1388            .collect()
1389    }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394    use super::*;
1395
1396    #[test]
1397    fn journal_sink_receives_every_mutation_in_ring_order() {
1398        let state = State::new();
1399        let sink = Arc::new(MemoryJournalSink::new());
1400        state.set_journal_sink(sink.clone());
1401
1402        let _ = state.set("a", 1);
1403        let _ = state.set("b", "two");
1404        state.remove("a");
1405
1406        let entries = sink.entries();
1407        assert_eq!(entries.len(), 3);
1408        assert_eq!(entries, state.recent_mutations());
1409        assert_eq!(entries[0].key, "a");
1410        assert_eq!(entries[2].origin, StateMutationOrigin::Remove);
1411    }
1412
1413    #[test]
1414    fn journal_sink_is_shared_with_clones_and_delta_views() {
1415        let state = State::new();
1416        let sink = Arc::new(MemoryJournalSink::new());
1417        state.set_journal_sink(sink.clone());
1418
1419        let clone = state.clone();
1420        let _ = clone.set("from_clone", true);
1421
1422        let tracked = state.with_delta_tracking();
1423        let _ = tracked.set("from_delta", 1);
1424        tracked.commit();
1425
1426        let keys: Vec<_> = sink.entries().iter().map(|m| m.key.clone()).collect();
1427        assert!(keys.contains(&"from_clone".to_string()));
1428        assert!(keys.contains(&"from_delta".to_string()));
1429        // Commit re-records the delta write into the committed store.
1430        assert!(
1431            sink.entries()
1432                .iter()
1433                .any(|m| m.origin == StateMutationOrigin::Commit)
1434        );
1435    }
1436
1437    #[test]
1438    fn journal_sink_outlives_ring_capacity() {
1439        // The ring is bounded; the sink is not.
1440        let state = State::new();
1441        let sink = Arc::new(MemoryJournalSink::new());
1442        state.set_journal_sink(sink.clone());
1443
1444        for i in 0..(DEFAULT_MUTATION_JOURNAL_CAPACITY + 10) {
1445            let _ = state.set(format!("k{i}"), i);
1446        }
1447
1448        assert_eq!(
1449            state.recent_mutations().len(),
1450            DEFAULT_MUTATION_JOURNAL_CAPACITY
1451        );
1452        assert_eq!(sink.len(), DEFAULT_MUTATION_JOURNAL_CAPACITY + 10);
1453        assert_eq!(sink.entries()[0].key, "k0");
1454    }
1455
1456    #[test]
1457    fn state_mutation_serde_round_trip_uses_epoch_millis() {
1458        let m = StateMutation {
1459            sequence: 42,
1460            key: "app:last_city".into(),
1461            old: None,
1462            new: Some(serde_json::json!("London")),
1463            origin: StateMutationOrigin::Set,
1464            timestamp: std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_718_000_000_123),
1465            delta: false,
1466        };
1467        let json = serde_json::to_string(&m).unwrap();
1468        assert!(json.contains("\"timestamp_ms\":1718000000123"));
1469        assert!(json.contains("\"origin\":\"set\""));
1470        let back: StateMutation = serde_json::from_str(&json).unwrap();
1471        assert_eq!(back, m);
1472    }
1473
1474    #[test]
1475    fn file_journal_sink_round_trip() {
1476        let dir = std::env::temp_dir().join(format!(
1477            "gemini-rs-journal-test-{}-{}",
1478            std::process::id(),
1479            std::time::SystemTime::now()
1480                .duration_since(std::time::UNIX_EPOCH)
1481                .unwrap()
1482                .as_nanos()
1483        ));
1484        std::fs::create_dir_all(&dir).unwrap();
1485        let path = dir.join("session.journal.jsonl");
1486
1487        let state = State::new();
1488        {
1489            let sink = Arc::new(FileJournalSink::create(&path).unwrap());
1490            state.set_journal_sink(sink);
1491            let _ = state.set("a", 1);
1492            let _ = state.set("a", 2);
1493            state.remove("a");
1494            // Replace the sink so the file sink drops (and flushes).
1495            state.set_journal_sink(Arc::new(MemoryJournalSink::new()));
1496        }
1497
1498        let data = std::fs::read_to_string(&path).unwrap();
1499        let parsed: Vec<StateMutation> = data
1500            .lines()
1501            .filter(|l| !l.trim().is_empty())
1502            .map(|l| serde_json::from_str(l).unwrap())
1503            .collect();
1504        assert_eq!(parsed.len(), 3);
1505        assert_eq!(parsed[0].new, Some(serde_json::json!(1)));
1506        assert_eq!(parsed[2].origin, StateMutationOrigin::Remove);
1507
1508        let _ = std::fs::remove_dir_all(&dir);
1509    }
1510
1511    #[test]
1512    fn set_and_get_string() {
1513        let state = State::new();
1514        let _ = state.set("name", "Alice");
1515        assert_eq!(state.get::<String>("name"), Some("Alice".to_string()));
1516    }
1517
1518    #[test]
1519    fn set_and_get_json() {
1520        let state = State::new();
1521        let _ = state.set("data", serde_json::json!({"temp": 22}));
1522        let v: Value = state.get("data").unwrap();
1523        assert_eq!(v["temp"], 22);
1524    }
1525
1526    #[test]
1527    fn pick_subset() {
1528        let state = State::new();
1529        let _ = state.set("a", 1);
1530        let _ = state.set("b", 2);
1531        let _ = state.set("c", 3);
1532        let picked = state.pick(&["a", "c"]);
1533        assert!(picked.contains("a"));
1534        assert!(!picked.contains("b"));
1535        assert!(picked.contains("c"));
1536    }
1537
1538    #[test]
1539    fn merge_states() {
1540        let s1 = State::new();
1541        let _ = s1.set("a", 1);
1542        let s2 = State::new();
1543        let _ = s2.set("b", 2);
1544        s1.merge(&s2);
1545        assert!(s1.contains("a"));
1546        assert!(s1.contains("b"));
1547    }
1548
1549    #[test]
1550    fn rename_key() {
1551        let state = State::new();
1552        let _ = state.set("old", "value");
1553        state.rename("old", "new");
1554        assert!(!state.contains("old"));
1555        assert_eq!(state.get::<String>("new"), Some("value".to_string()));
1556    }
1557
1558    #[test]
1559    fn remove_returns_value() {
1560        let state = State::new();
1561        let _ = state.set("key", 42);
1562        let removed = state.remove("key");
1563        assert!(removed.is_some());
1564        assert!(!state.contains("key"));
1565    }
1566
1567    #[test]
1568    fn get_missing_returns_none() {
1569        let state = State::new();
1570        assert_eq!(state.get::<String>("nope"), None);
1571    }
1572
1573    // ── Delta tracking tests ──────────────────────────────────────────────
1574
1575    #[test]
1576    fn delta_tracking_writes_to_delta() {
1577        let state = State::new();
1578        let _ = state.set("committed", "yes");
1579
1580        let tracked = state.with_delta_tracking();
1581        let _ = tracked.set("new_key", "new_value");
1582
1583        // New key visible through tracked state
1584        assert_eq!(
1585            tracked.get::<String>("new_key"),
1586            Some("new_value".to_string())
1587        );
1588        // But NOT visible in original (non-delta) state's inner
1589        assert!(!state.contains("new_key"));
1590        // Committed key still visible through tracked state
1591        assert_eq!(tracked.get::<String>("committed"), Some("yes".to_string()));
1592    }
1593
1594    #[test]
1595    fn delta_has_delta_reports_correctly() {
1596        let state = State::new();
1597        let tracked = state.with_delta_tracking();
1598        assert!(!tracked.has_delta());
1599
1600        let _ = tracked.set("key", "val");
1601        assert!(tracked.has_delta());
1602    }
1603
1604    #[test]
1605    fn delta_commit_merges_to_inner() {
1606        let state = State::new();
1607        let tracked = state.with_delta_tracking();
1608        let _ = tracked.set("key", "val");
1609        assert!(!state.contains("key"));
1610
1611        tracked.commit();
1612        // Now visible in original state
1613        assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1614        assert!(!tracked.has_delta());
1615    }
1616
1617    #[test]
1618    fn delta_rollback_discards_changes() {
1619        let state = State::new();
1620        let tracked = state.with_delta_tracking();
1621        let _ = tracked.set("key", "val");
1622        assert!(tracked.has_delta());
1623
1624        tracked.rollback();
1625        assert!(!tracked.has_delta());
1626        assert!(!state.contains("key"));
1627        assert!(!tracked.contains("key"));
1628    }
1629
1630    #[test]
1631    fn delta_snapshot() {
1632        let state = State::new();
1633        let tracked = state.with_delta_tracking();
1634        let _ = tracked.set("a", 1);
1635        let _ = tracked.set("b", 2);
1636
1637        let snapshot = tracked.delta();
1638        assert_eq!(snapshot.len(), 2);
1639        assert!(snapshot.contains_key("a"));
1640        assert!(snapshot.contains_key("b"));
1641    }
1642
1643    #[test]
1644    fn set_committed_bypasses_delta() {
1645        let state = State::new();
1646        let tracked = state.with_delta_tracking();
1647        let _ = tracked.set_committed("direct", "value");
1648
1649        // Visible immediately in inner
1650        assert_eq!(state.get::<String>("direct"), Some("value".to_string()));
1651        // Not in delta
1652        assert!(!tracked.has_delta());
1653        // Still visible through tracked (reads inner too)
1654        assert_eq!(tracked.get::<String>("direct"), Some("value".to_string()));
1655    }
1656
1657    #[test]
1658    fn mutation_journal_records_set_and_remove() {
1659        let state = State::new();
1660        let _ = state.set("key", "first");
1661        let _ = state.set("key", "second");
1662        state.remove("key");
1663
1664        let mutations = state.recent_mutations();
1665        assert_eq!(mutations.len(), 3);
1666        assert_eq!(mutations[0].key, "key");
1667        assert_eq!(mutations[0].old, None);
1668        assert_eq!(mutations[0].new, Some(serde_json::json!("first")));
1669        assert_eq!(mutations[0].origin, StateMutationOrigin::Set);
1670
1671        assert_eq!(mutations[1].old, Some(serde_json::json!("first")));
1672        assert_eq!(mutations[1].new, Some(serde_json::json!("second")));
1673
1674        assert_eq!(mutations[2].old, Some(serde_json::json!("second")));
1675        assert_eq!(mutations[2].new, None);
1676        assert_eq!(mutations[2].origin, StateMutationOrigin::Remove);
1677    }
1678
1679    #[test]
1680    fn mutation_journal_is_shared_with_delta_tracking() {
1681        let state = State::new();
1682        let _ = state.set("committed", "yes");
1683
1684        let tracked = state.with_delta_tracking();
1685        let _ = tracked.set("committed", "maybe");
1686        tracked.commit();
1687
1688        let mutations = state.recent_mutations();
1689        assert_eq!(mutations.len(), 3);
1690        assert_eq!(mutations[1].key, "committed");
1691        assert_eq!(mutations[1].old, Some(serde_json::json!("yes")));
1692        assert_eq!(mutations[1].new, Some(serde_json::json!("maybe")));
1693        assert_eq!(mutations[1].origin, StateMutationOrigin::Set);
1694        assert!(mutations[1].delta);
1695
1696        assert_eq!(mutations[2].origin, StateMutationOrigin::Commit);
1697        assert!(!mutations[2].delta);
1698    }
1699
1700    #[test]
1701    fn drain_mutations_clears_journal() {
1702        let state = State::new();
1703        let _ = state.set("a", 1);
1704        let _ = state.set("b", 2);
1705
1706        let drained = state.drain_mutations();
1707        assert_eq!(drained.len(), 2);
1708        assert!(state.recent_mutations().is_empty());
1709    }
1710
1711    #[test]
1712    fn redacted_keys_are_masked_wherever_state_leaves_the_process() {
1713        let sink = Arc::new(MemoryJournalSink::new());
1714        let state = State::new().with_journal_sink(sink.clone());
1715        state.redact_keys(["card_number"]);
1716        let _ = state.set("card_number", "4111111111111111");
1717        let _ = state.set("app:card_number", "4111111111111111");
1718        let _ = state.set("name", "Ada");
1719
1720        // Inside the process the value is real.
1721        assert_eq!(
1722            state.get::<String>("card_number").as_deref(),
1723            Some("4111111111111111")
1724        );
1725        // The durable journal never sees it, under either scope.
1726        for entry in sink.entries() {
1727            let leaked = entry
1728                .new
1729                .as_ref()
1730                .is_some_and(|v| v.to_string().contains("4111"));
1731            assert!(!leaked, "{} leaked into the journal sink", entry.key);
1732        }
1733        // Nor does a snapshot.
1734        let snapshot = state.to_redacted_hashmap();
1735        assert_eq!(snapshot["card_number"], serde_json::json!(REDACTED));
1736        assert_eq!(snapshot["app:card_number"], serde_json::json!(REDACTED));
1737        assert_eq!(snapshot["name"], serde_json::json!("Ada"));
1738        // Nor an object handed out, one level deep.
1739        assert_eq!(
1740            state.redact_fields(&serde_json::json!({ "card_number": "4111", "name": "Ada" })),
1741            serde_json::json!({ "card_number": REDACTED, "name": "Ada" })
1742        );
1743        // The in-memory journal keeps real values for evidence and watchers.
1744        assert!(
1745            state
1746                .recent_mutations()
1747                .iter()
1748                .any(|m| m.new == Some(serde_json::json!("4111111111111111")))
1749        );
1750    }
1751
1752    #[test]
1753    fn try_mutations_since_reports_a_gap_the_ring_dropped() {
1754        let state = State::new();
1755        let cursor = state.mutation_cursor();
1756        let _ = state.set("a", 1);
1757        assert_eq!(state.try_mutations_since(cursor).map(|m| m.len()), Some(1));
1758
1759        state.drain_mutations();
1760        let _ = state.set("b", 2);
1761        assert!(
1762            state.try_mutations_since(cursor).is_none(),
1763            "the write to `a` is gone from the journal"
1764        );
1765        let after_drain = state.mutation_cursor() - 1;
1766        assert_eq!(
1767            state.try_mutations_since(after_drain).map(|m| m.len()),
1768            Some(1)
1769        );
1770        assert_eq!(
1771            state
1772                .try_mutations_since(state.mutation_cursor())
1773                .map(|m| m.len()),
1774            Some(0)
1775        );
1776    }
1777
1778    #[test]
1779    fn mutation_cursor_reads_only_later_changes() {
1780        let state = State::new();
1781        let _ = state.set("before", 1);
1782        let cursor = state.mutation_cursor();
1783
1784        let _ = state.set("after", 2);
1785        state.remove("before");
1786
1787        let mutations = state.mutations_since(cursor);
1788        assert_eq!(mutations.len(), 2);
1789        assert_eq!(mutations[0].key, "after");
1790        assert_eq!(mutations[1].key, "before");
1791    }
1792
1793    #[test]
1794    fn no_delta_tracking_preserves_existing_behavior() {
1795        let state = State::new();
1796        assert!(!state.is_tracking_delta());
1797        let _ = state.set("key", "val");
1798        assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1799        assert!(!state.has_delta());
1800    }
1801
1802    // ── Prefix tests ──────────────────────────────────────────────────────
1803
1804    #[test]
1805    fn prefix_app_set_and_get() {
1806        let state = State::new();
1807        let _ = state.app().set("flag", true);
1808
1809        // Accessible via prefix accessor
1810        assert_eq!(state.app().get::<bool>("flag"), Some(true));
1811        // Also accessible via raw key
1812        assert_eq!(state.get::<bool>("app:flag"), Some(true));
1813    }
1814
1815    #[test]
1816    fn prefix_user_set_and_get() {
1817        let state = State::new();
1818        let _ = state.user().set("name", "Alice");
1819        assert_eq!(
1820            state.user().get::<String>("name"),
1821            Some("Alice".to_string())
1822        );
1823        assert_eq!(state.get::<String>("user:name"), Some("Alice".to_string()));
1824    }
1825
1826    #[test]
1827    fn prefix_temp_set_and_get() {
1828        let state = State::new();
1829        let _ = state.temp().set("scratch", 42);
1830        assert_eq!(state.temp().get::<i32>("scratch"), Some(42));
1831    }
1832
1833    #[test]
1834    fn prefix_contains_and_remove() {
1835        let state = State::new();
1836        let _ = state.app().set("x", 1);
1837        assert!(state.app().contains("x"));
1838        state.app().remove("x");
1839        assert!(!state.app().contains("x"));
1840    }
1841
1842    #[test]
1843    fn prefix_keys() {
1844        let state = State::new();
1845        let _ = state.app().set("a", 1);
1846        let _ = state.app().set("b", 2);
1847        let _ = state.user().set("c", 3);
1848
1849        let app_keys = state.app().keys();
1850        assert_eq!(app_keys.len(), 2);
1851        assert!(app_keys.contains(&"a".to_string()));
1852        assert!(app_keys.contains(&"b".to_string()));
1853
1854        let user_keys = state.user().keys();
1855        assert_eq!(user_keys.len(), 1);
1856        assert!(user_keys.contains(&"c".to_string()));
1857    }
1858
1859    #[test]
1860    fn prefix_with_delta_tracking() {
1861        let state = State::new();
1862        let tracked = state.with_delta_tracking();
1863        let _ = tracked.app().set("flag", true);
1864
1865        // Visible in tracked state via prefix
1866        assert_eq!(tracked.app().get::<bool>("flag"), Some(true));
1867        // In delta, not committed
1868        assert!(tracked.has_delta());
1869        assert!(!state.contains("app:flag"));
1870
1871        tracked.commit();
1872        assert_eq!(state.get::<bool>("app:flag"), Some(true));
1873    }
1874
1875    // ── New prefix accessor tests ────────────────────────────────────────
1876
1877    #[test]
1878    fn prefix_session_set_and_get() {
1879        let state = State::new();
1880        let _ = state.session().set("turn_count", 5);
1881        assert_eq!(state.session().get::<i32>("turn_count"), Some(5));
1882        assert_eq!(state.get::<i32>("session:turn_count"), Some(5));
1883    }
1884
1885    #[test]
1886    fn prefix_turn_set_and_get() {
1887        let state = State::new();
1888        let _ = state.turn().set("transcript", "hello");
1889        assert_eq!(
1890            state.turn().get::<String>("transcript"),
1891            Some("hello".to_string())
1892        );
1893        assert_eq!(
1894            state.get::<String>("turn:transcript"),
1895            Some("hello".to_string())
1896        );
1897    }
1898
1899    #[test]
1900    fn prefix_bg_set_and_get() {
1901        let state = State::new();
1902        let _ = state.bg().set("task_id", "abc-123");
1903        assert_eq!(
1904            state.bg().get::<String>("task_id"),
1905            Some("abc-123".to_string())
1906        );
1907        assert_eq!(
1908            state.get::<String>("bg:task_id"),
1909            Some("abc-123".to_string())
1910        );
1911    }
1912
1913    #[test]
1914    fn prefix_session_contains_and_remove() {
1915        let state = State::new();
1916        let _ = state.session().set("x", 1);
1917        assert!(state.session().contains("x"));
1918        state.session().remove("x");
1919        assert!(!state.session().contains("x"));
1920    }
1921
1922    #[test]
1923    fn prefix_turn_keys() {
1924        let state = State::new();
1925        let _ = state.turn().set("a", 1);
1926        let _ = state.turn().set("b", 2);
1927        let _ = state.session().set("c", 3);
1928
1929        let turn_keys = state.turn().keys();
1930        assert_eq!(turn_keys.len(), 2);
1931        assert!(turn_keys.contains(&"a".to_string()));
1932        assert!(turn_keys.contains(&"b".to_string()));
1933    }
1934
1935    // ── try_get / try_get_key ─────────────────────────────────────────
1936
1937    #[test]
1938    fn try_get_distinguishes_absent_from_wrong_type() {
1939        let state = State::new();
1940        assert!(matches!(state.try_get::<u32>("missing"), Ok(None)));
1941
1942        state.set("n", 5u32).unwrap();
1943        assert_eq!(state.try_get::<u32>("n").unwrap(), Some(5));
1944
1945        state.set("s", "not a number").unwrap();
1946        // Lenient read folds the type error into `None`…
1947        assert_eq!(state.get::<u32>("s"), None);
1948        // …the strict read reports it.
1949        match state.try_get::<u32>("s") {
1950            Err(StateError::WrongType { key, .. }) => assert_eq!(key, "s"),
1951            other => panic!("expected WrongType, got {other:?}"),
1952        }
1953
1954        // Same derived: fallback as `get`.
1955        state.set("derived:risk", 0.5f64).unwrap();
1956        assert_eq!(state.try_get::<f64>("risk").unwrap(), Some(0.5));
1957
1958        const N: StateKey<u32> = StateKey::new("n");
1959        assert_eq!(state.try_get_key(&N).unwrap(), Some(5));
1960        const S: StateKey<u32> = StateKey::new("s");
1961        assert!(state.try_get_key(&S).is_err());
1962    }
1963
1964    // ── ReadOnlyPrefixedState (derived) tests ────────────────────────────
1965
1966    #[test]
1967    fn derived_read_only_get() {
1968        let state = State::new();
1969        // Write via raw key (simulating ComputedRegistry)
1970        let _ = state.set("derived:sentiment", "positive");
1971        assert_eq!(
1972            state.derived().get::<String>("sentiment"),
1973            Some("positive".to_string())
1974        );
1975    }
1976
1977    #[test]
1978    fn derived_read_only_get_raw() {
1979        let state = State::new();
1980        let _ = state.set("derived:score", serde_json::json!(0.95));
1981        let raw = state.derived().get_raw("score");
1982        assert!(raw.is_some());
1983        assert_eq!(raw.unwrap(), serde_json::json!(0.95));
1984    }
1985
1986    #[test]
1987    fn derived_read_only_contains() {
1988        let state = State::new();
1989        let _ = state.set("derived:exists", true);
1990        assert!(state.derived().contains("exists"));
1991        assert!(!state.derived().contains("missing"));
1992    }
1993
1994    #[test]
1995    fn derived_read_only_keys() {
1996        let state = State::new();
1997        let _ = state.set("derived:a", 1);
1998        let _ = state.set("derived:b", 2);
1999        let _ = state.set("app:c", 3);
2000
2001        let derived_keys = state.derived().keys();
2002        assert_eq!(derived_keys.len(), 2);
2003        assert!(derived_keys.contains(&"a".to_string()));
2004        assert!(derived_keys.contains(&"b".to_string()));
2005    }
2006
2007    #[test]
2008    fn derived_missing_key_returns_none() {
2009        let state = State::new();
2010        assert_eq!(state.derived().get::<String>("nope"), None);
2011        assert_eq!(state.derived().get_raw("nope"), None);
2012    }
2013
2014    // ── snapshot_values tests ────────────────────────────────────────────
2015
2016    #[test]
2017    fn snapshot_values_captures_existing_keys() {
2018        let state = State::new();
2019        let _ = state.set("a", 1);
2020        let _ = state.set("b", "hello");
2021        let _ = state.set("c", true);
2022
2023        let snap = state.snapshot_values(&["a", "b", "missing"]);
2024        assert_eq!(snap.len(), 2);
2025        assert_eq!(snap.get("a"), Some(&serde_json::json!(1)));
2026        assert_eq!(snap.get("b"), Some(&serde_json::json!("hello")));
2027        assert!(!snap.contains_key("missing"));
2028    }
2029
2030    #[test]
2031    fn snapshot_values_empty_keys() {
2032        let state = State::new();
2033        let _ = state.set("a", 1);
2034        let snap = state.snapshot_values(&[]);
2035        assert!(snap.is_empty());
2036    }
2037
2038    // ── diff_values tests ────────────────────────────────────────────────
2039
2040    #[test]
2041    fn diff_values_detects_changed_value() {
2042        let state = State::new();
2043        let _ = state.set("x", 1);
2044        let snap = state.snapshot_values(&["x"]);
2045
2046        let _ = state.set("x", 2);
2047        let diffs = state.diff_values(&snap, &["x"]);
2048        assert_eq!(diffs.len(), 1);
2049        assert_eq!(diffs[0].0, "x");
2050        assert_eq!(diffs[0].1, serde_json::json!(1));
2051        assert_eq!(diffs[0].2, serde_json::json!(2));
2052    }
2053
2054    #[test]
2055    fn diff_values_detects_new_key() {
2056        let state = State::new();
2057        let snap = state.snapshot_values(&["y"]);
2058
2059        let _ = state.set("y", "new");
2060        let diffs = state.diff_values(&snap, &["y"]);
2061        assert_eq!(diffs.len(), 1);
2062        assert_eq!(diffs[0].0, "y");
2063        assert_eq!(diffs[0].1, Value::Null);
2064        assert_eq!(diffs[0].2, serde_json::json!("new"));
2065    }
2066
2067    #[test]
2068    fn diff_values_detects_removed_key() {
2069        let state = State::new();
2070        let _ = state.set("z", 42);
2071        let snap = state.snapshot_values(&["z"]);
2072
2073        state.remove("z");
2074        let diffs = state.diff_values(&snap, &["z"]);
2075        assert_eq!(diffs.len(), 1);
2076        assert_eq!(diffs[0].0, "z");
2077        assert_eq!(diffs[0].1, serde_json::json!(42));
2078        assert_eq!(diffs[0].2, Value::Null);
2079    }
2080
2081    #[test]
2082    fn diff_values_no_change() {
2083        let state = State::new();
2084        let _ = state.set("stable", 10);
2085        let snap = state.snapshot_values(&["stable"]);
2086
2087        // No mutation
2088        let diffs = state.diff_values(&snap, &["stable"]);
2089        assert!(diffs.is_empty());
2090    }
2091
2092    #[test]
2093    fn diff_values_multiple_keys_mixed_changes() {
2094        let state = State::new();
2095        let _ = state.set("a", 1);
2096        let _ = state.set("b", 2);
2097        let snap = state.snapshot_values(&["a", "b", "c"]);
2098
2099        let _ = state.set("a", 10); // changed
2100        // b unchanged
2101        let _ = state.set("c", 3); // new
2102
2103        let diffs = state.diff_values(&snap, &["a", "b", "c"]);
2104        assert_eq!(diffs.len(), 2); // a changed, c new; b unchanged
2105        let diff_keys: Vec<&str> = diffs.iter().map(|(k, _, _)| k.as_str()).collect();
2106        assert!(diff_keys.contains(&"a"));
2107        assert!(diff_keys.contains(&"c"));
2108    }
2109
2110    // ── clear_prefix tests ───────────────────────────────────────────────
2111
2112    #[test]
2113    fn clear_prefix_removes_matching_keys() {
2114        let state = State::new();
2115        let _ = state.set("turn:a", 1);
2116        let _ = state.set("turn:b", 2);
2117        let _ = state.set("app:c", 3);
2118        let _ = state.set("session:d", 4);
2119
2120        state.clear_prefix("turn:");
2121        assert!(!state.contains("turn:a"));
2122        assert!(!state.contains("turn:b"));
2123        assert!(state.contains("app:c"));
2124        assert!(state.contains("session:d"));
2125    }
2126
2127    #[test]
2128    fn clear_prefix_no_matching_keys_is_noop() {
2129        let state = State::new();
2130        let _ = state.set("app:x", 1);
2131        state.clear_prefix("turn:");
2132        assert!(state.contains("app:x"));
2133    }
2134
2135    #[test]
2136    fn clear_prefix_also_clears_delta() {
2137        let state = State::new();
2138        let _ = state.set("turn:committed", 1);
2139        let tracked = state.with_delta_tracking();
2140        let _ = tracked.set("turn:delta_val", 2);
2141
2142        // Both committed and delta have turn: keys
2143        assert!(tracked.contains("turn:committed"));
2144        assert!(tracked.contains("turn:delta_val"));
2145
2146        tracked.clear_prefix("turn:");
2147        assert!(!tracked.contains("turn:committed"));
2148        assert!(!tracked.contains("turn:delta_val"));
2149    }
2150
2151    #[test]
2152    fn clear_prefix_via_turn_accessor() {
2153        let state = State::new();
2154        let _ = state.turn().set("x", 1);
2155        let _ = state.turn().set("y", 2);
2156        let _ = state.app().set("z", 3);
2157
2158        state.clear_prefix("turn:");
2159        assert!(state.turn().keys().is_empty());
2160        assert!(state.app().contains("z"));
2161    }
2162
2163    // ── modify() tests ──────────────────────────────────────────────────
2164
2165    #[test]
2166    fn modify_increment_existing() {
2167        let state = State::new();
2168        let _ = state.set("count", 5u32);
2169        let result = state.modify("count", 0u32, |n| n + 1).unwrap();
2170        assert_eq!(result, 6);
2171        assert_eq!(state.get::<u32>("count"), Some(6));
2172    }
2173
2174    #[test]
2175    fn modify_uses_default_when_missing() {
2176        let state = State::new();
2177        let result = state.modify("new_count", 0u32, |n| n + 1).unwrap();
2178        assert_eq!(result, 1);
2179        assert_eq!(state.get::<u32>("new_count"), Some(1));
2180    }
2181
2182    #[test]
2183    fn modify_with_delta_tracking() {
2184        let state = State::new();
2185        let _ = state.set("x", 10u32);
2186        let tracked = state.with_delta_tracking();
2187        let result = tracked.modify("x", 0u32, |n| n * 2).unwrap();
2188        assert_eq!(result, 20);
2189        // Written to delta, not committed
2190        assert_eq!(tracked.get::<u32>("x"), Some(20));
2191        assert_eq!(state.get::<u32>("x"), Some(10)); // original unchanged
2192    }
2193
2194    // ── derived fallback tests ──────────────────────────────────────────
2195
2196    #[test]
2197    fn get_falls_back_to_derived_prefix() {
2198        let state = State::new();
2199        let _ = state.set("derived:risk", 0.85);
2200        // Access without prefix — should find derived:risk
2201        assert_eq!(state.get::<f64>("risk"), Some(0.85));
2202    }
2203
2204    #[test]
2205    fn get_prefers_direct_key_over_derived() {
2206        let state = State::new();
2207        let _ = state.set("score", 1.0);
2208        let _ = state.set("derived:score", 0.5);
2209        // Direct key should win
2210        assert_eq!(state.get::<f64>("score"), Some(1.0));
2211    }
2212
2213    #[test]
2214    fn get_derived_fallback_skipped_for_prefixed_keys() {
2215        let state = State::new();
2216        let _ = state.set("derived:risk", 0.85);
2217        // Prefixed key should NOT trigger fallback
2218        assert_eq!(state.get::<f64>("app:risk"), None);
2219    }
2220
2221    #[test]
2222    fn get_derived_fallback_with_delta_tracking() {
2223        let state = State::new();
2224        let tracked = state.with_delta_tracking();
2225        let _ = tracked.set("derived:computed_val", 42);
2226        assert_eq!(tracked.get::<i32>("computed_val"), Some(42));
2227    }
2228
2229    // ── with() zero-copy borrow tests ──────────────────────────────────
2230
2231    #[test]
2232    fn with_reads_from_inner() {
2233        let state = State::new();
2234        let _ = state.set("name", "Alice");
2235        let len = state.with("name", |v| v.as_str().unwrap().len());
2236        assert_eq!(len, Some(5));
2237    }
2238
2239    #[test]
2240    fn with_reads_from_delta_first() {
2241        let state = State::new();
2242        let _ = state.set("x", 1);
2243        let tracked = state.with_delta_tracking();
2244        let _ = tracked.set("x", 99);
2245        let val = tracked.with("x", |v| v.as_i64().unwrap());
2246        assert_eq!(val, Some(99));
2247    }
2248
2249    #[test]
2250    fn with_falls_back_to_inner_when_not_in_delta() {
2251        let state = State::new();
2252        let _ = state.set("committed", "yes");
2253        let tracked = state.with_delta_tracking();
2254        let val = tracked.with("committed", |v| v.as_str().unwrap().to_string());
2255        assert_eq!(val, Some("yes".to_string()));
2256    }
2257
2258    #[test]
2259    fn with_falls_back_to_derived() {
2260        let state = State::new();
2261        let _ = state.set("derived:risk", 0.85);
2262        let val = state.with("risk", |v| v.as_f64().unwrap());
2263        assert_eq!(val, Some(0.85));
2264    }
2265
2266    #[test]
2267    fn with_derived_fallback_skipped_for_prefixed() {
2268        let state = State::new();
2269        let _ = state.set("derived:risk", 0.85);
2270        let val = state.with("app:risk", |v| v.as_f64().unwrap());
2271        assert_eq!(val, None);
2272    }
2273
2274    #[test]
2275    fn with_returns_none_for_missing() {
2276        let state = State::new();
2277        let val = state.with("missing", std::clone::Clone::clone);
2278        assert_eq!(val, None);
2279    }
2280
2281    #[test]
2282    fn with_on_prefixed_state() {
2283        let state = State::new();
2284        let _ = state.app().set("flag", true);
2285        let val = state.app().with("flag", |v| v.as_bool().unwrap());
2286        assert_eq!(val, Some(true));
2287    }
2288
2289    #[test]
2290    fn with_on_read_only_prefixed_state() {
2291        let state = State::new();
2292        let _ = state.set("derived:score", serde_json::json!(0.95));
2293        let val = state.derived().with("score", |v| v.as_f64().unwrap());
2294        assert_eq!(val, Some(0.95));
2295    }
2296
2297    // ── StateKey typed key tests ───────────────────────────────────────
2298
2299    const TURN_COUNT: StateKey<u32> = StateKey::new("session:turn_count");
2300    const NAME: StateKey<String> = StateKey::new("user:name");
2301
2302    #[test]
2303    fn state_key_get_and_set() {
2304        let state = State::new();
2305        let _ = state.set_key(&TURN_COUNT, 5);
2306        assert_eq!(state.get_key(&TURN_COUNT), Some(5));
2307    }
2308
2309    #[test]
2310    fn state_key_get_missing() {
2311        let state = State::new();
2312        assert_eq!(state.get_key(&TURN_COUNT), None);
2313    }
2314
2315    #[test]
2316    fn state_key_string_type() {
2317        let state = State::new();
2318        let _ = state.set_key(&NAME, "Alice".to_string());
2319        assert_eq!(state.get_key(&NAME), Some("Alice".to_string()));
2320    }
2321
2322    #[test]
2323    fn state_key_with() {
2324        let state = State::new();
2325        let _ = state.set_key(&TURN_COUNT, 42);
2326        let val = state.with_key(&TURN_COUNT, |v| v.as_u64().unwrap());
2327        assert_eq!(val, Some(42));
2328    }
2329
2330    #[test]
2331    fn state_key_interop_with_raw() {
2332        let state = State::new();
2333        let _ = state.set_key(&TURN_COUNT, 10);
2334        // Can also read via raw key
2335        assert_eq!(state.get::<u32>("session:turn_count"), Some(10));
2336    }
2337
2338    #[test]
2339    fn slot_evidence_aggregates_value_provenance_and_journal() {
2340        let state = State::new();
2341        let _ = state.set("party_size", 6u8);
2342        // Provenance written under the state_meta convention (as resolvers do).
2343        let _ = state.set(
2344            "state_meta:party_size",
2345            serde_json::json!({ "source": "extraction", "confidence": 0.9 }),
2346        );
2347
2348        let ev = state.evidence("party_size");
2349        assert!(ev.present);
2350        assert_eq!(ev.value, Some(serde_json::json!(6)));
2351        assert_eq!(ev.source.as_deref(), Some("extraction"));
2352        assert_eq!(ev.confidence, Some(0.9));
2353        assert!(ev.last_sequence.is_some());
2354        assert_eq!(ev.last_origin, Some(StateMutationOrigin::Set));
2355
2356        // An absent key reports no evidence.
2357        let missing = state.evidence("nope");
2358        assert!(!missing.present);
2359        assert!(missing.source.is_none());
2360    }
2361
2362    // ── Transaction-invariant tests (the verified correctness bugs) ──────────
2363
2364    #[test]
2365    fn rollback_restores_base_after_remove() {
2366        // Regression: previously remove() in delta mode deleted from the committed
2367        // store, so rollback() could not restore it.
2368        let base = State::new();
2369        let _ = base.set("k", "original");
2370
2371        let tx = base.with_delta_tracking();
2372        assert_eq!(tx.remove("k"), Some(serde_json::json!("original")));
2373        assert_eq!(tx.get::<String>("k"), None); // tombstoned in the tx view
2374        assert_eq!(base.get::<String>("k"), Some("original".into())); // base intact
2375
2376        tx.rollback();
2377        assert_eq!(tx.get::<String>("k"), Some("original".into()));
2378        assert_eq!(base.get::<String>("k"), Some("original".into()));
2379    }
2380
2381    #[test]
2382    fn rollback_restores_base_after_clear_prefix() {
2383        // Regression: clear_prefix() used to mutate the committed store directly.
2384        let base = State::new();
2385        let _ = base.set("app:a", 1u32);
2386        let _ = base.set("app:b", 2u32);
2387        let _ = base.set("user:c", 3u32);
2388
2389        let tx = base.with_delta_tracking();
2390        tx.clear_prefix("app:");
2391        assert_eq!(tx.get::<u32>("app:a"), None);
2392        assert_eq!(tx.get::<u32>("app:b"), None);
2393        assert_eq!(tx.get::<u32>("user:c"), Some(3));
2394        // Base untouched until commit.
2395        assert_eq!(base.get::<u32>("app:a"), Some(1));
2396
2397        tx.rollback();
2398        assert_eq!(tx.get::<u32>("app:a"), Some(1));
2399        assert_eq!(tx.get::<u32>("app:b"), Some(2));
2400    }
2401
2402    #[test]
2403    fn commit_applies_removals() {
2404        let base = State::new();
2405        let _ = base.set("k", "v");
2406        let tx = base.with_delta_tracking();
2407        tx.remove("k");
2408        tx.commit();
2409        assert_eq!(base.get::<String>("k"), None);
2410    }
2411
2412    #[test]
2413    fn commit_applies_prefix_clear() {
2414        let base = State::new();
2415        let _ = base.set("app:a", 1u32);
2416        let _ = base.set("user:c", 3u32);
2417        let tx = base.with_delta_tracking();
2418        tx.clear_prefix("app:");
2419        tx.commit();
2420        assert_eq!(base.get::<u32>("app:a"), None);
2421        assert_eq!(base.get::<u32>("user:c"), Some(3));
2422    }
2423
2424    #[test]
2425    fn modify_is_atomic_under_concurrency() {
2426        use std::sync::Arc;
2427        use std::thread;
2428
2429        let state = Arc::new(State::new());
2430        let _ = state.set("count", 0u64);
2431
2432        let threads = 8;
2433        let per_thread = 1000;
2434        let handles: Vec<_> = (0..threads)
2435            .map(|_| {
2436                let state = state.clone();
2437                thread::spawn(move || {
2438                    for _ in 0..per_thread {
2439                        let _ = state.modify("count", 0u64, |n| n + 1);
2440                    }
2441                })
2442            })
2443            .collect();
2444        for h in handles {
2445            h.join().unwrap();
2446        }
2447        // With a real per-key atomic RMW, no increments are lost.
2448        assert_eq!(
2449            state.get::<u64>("count"),
2450            Some((threads * per_thread) as u64)
2451        );
2452    }
2453}
2454
2455#[cfg(test)]
2456mod proptests {
2457    use super::*;
2458    use proptest::prelude::*;
2459
2460    // A transaction's puts and removes never leak to the base before commit, and
2461    // a rollback always restores the exact committed base.
2462    proptest! {
2463        #[test]
2464        fn rollback_always_restores_base(
2465            base_keys in proptest::collection::vec(("[a-c]", 0u32..5), 0..6),
2466            ops in proptest::collection::vec(
2467                prop_oneof![
2468                    ("[a-c]", 0u32..5).prop_map(|(k, v)| (k, Some(v))),
2469                    "[a-c]".prop_map(|k| (k, None)),
2470                ],
2471                0..12,
2472            ),
2473        ) {
2474            let base = State::new();
2475            for (k, v) in &base_keys {
2476                let _ = base.set(k.clone(), *v);
2477            }
2478            let snapshot = |s: &State| -> std::collections::BTreeMap<String, Value> {
2479                s.keys().into_iter().filter_map(|k| s.get_raw(&k).map(|v| (k, v))).collect()
2480            };
2481            let before = snapshot(&base);
2482
2483            let tx = base.with_delta_tracking();
2484            for (k, v) in &ops {
2485                match v {
2486                    Some(v) => { let _ = tx.set(k.clone(), *v); }
2487                    None => { tx.remove(k); }
2488                }
2489            }
2490            // Base is never mutated while the tx is open.
2491            prop_assert_eq!(&before, &snapshot(&base));
2492
2493            tx.rollback();
2494            prop_assert_eq!(&before, &snapshot(&tx));
2495        }
2496    }
2497}
2498
2499#[cfg(test)]
2500mod derived_contains_fallback {
2501    //! `contains` must agree with `get` about the transparent `derived:`
2502    //! fallback. Flow predicates (`is_set`, `captured`) evaluate through
2503    //! `contains`, so a computed variable that `get` returns but `contains`
2504    //! denies reads as permanently unknown to the flow.
2505    use super::State;
2506
2507    #[test]
2508    fn contains_sees_a_derived_value_through_the_unprefixed_key() {
2509        let state = State::new();
2510        state.set("derived:risk", 0.85).unwrap();
2511        assert_eq!(
2512            state.get::<f64>("risk"),
2513            Some(0.85),
2514            "precondition: get falls back"
2515        );
2516        assert!(
2517            state.contains("risk"),
2518            "contains must fall back the same way get does"
2519        );
2520    }
2521
2522    #[test]
2523    fn contains_fallback_respects_delta_tracking_and_tombstones() {
2524        let state = State::new();
2525        state.set("derived:score", 1u32).unwrap();
2526        let tracked = state.with_delta_tracking();
2527        assert!(
2528            tracked.contains("score"),
2529            "inner derived value visible through tracked view"
2530        );
2531        tracked.remove("derived:score");
2532        assert!(
2533            !tracked.contains("score"),
2534            "a tombstone on the derived key shadows inner"
2535        );
2536    }
2537
2538    #[test]
2539    fn contains_does_not_fall_back_for_prefixed_keys() {
2540        let state = State::new();
2541        state.set("derived:flag", true).unwrap();
2542        assert!(
2543            !state.contains("session:flag"),
2544            "only unprefixed keys get the fallback"
2545        );
2546    }
2547}