Skip to main content

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