1pub mod ulid;
14
15use crate::obs::log::Logger;
16use crate::store::{Envelope, KeySeq, PutOutcome, SharedStore, StoreError};
17use serde::{Deserialize, Serialize};
18use serde_json::{Value, json};
19use std::collections::BTreeSet;
20use std::collections::{BTreeMap, HashMap};
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Mutex, OnceLock};
23use std::time::{Duration, Instant};
24
25pub(crate) use crate::store::now_ms;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub enum Kind {
31 Manifest,
32 Inbox,
33 Context,
34 Run,
35 Subagent,
36 Task,
37 Memory,
38 Artifact,
39 Timer,
40 Event,
44 Audit,
45 Cred,
49}
50
51impl Kind {
52 pub fn as_str(self) -> &'static str {
53 match self {
54 Kind::Manifest => "manifest",
55 Kind::Inbox => "inbox",
56 Kind::Context => "context",
57 Kind::Run => "run",
58 Kind::Subagent => "subagent",
59 Kind::Task => "task",
60 Kind::Memory => "memory",
61 Kind::Artifact => "artifact",
62 Kind::Timer => "timer",
63 Kind::Event => "event",
64 Kind::Audit => "audit",
65 Kind::Cred => "cred",
66 }
67 }
68 pub fn parse(s: &str) -> Option<Kind> {
69 Some(match s {
70 "manifest" => Kind::Manifest,
71 "inbox" => Kind::Inbox,
72 "event" => Kind::Event,
73 "context" => Kind::Context,
74 "run" => Kind::Run,
75 "subagent" => Kind::Subagent,
76 "task" => Kind::Task,
77 "memory" => Kind::Memory,
78 "artifact" => Kind::Artifact,
79 "timer" => Kind::Timer,
80 "audit" => Kind::Audit,
81 "cred" => Kind::Cred,
82 _ => return None,
83 })
84 }
85 pub fn indexed(self) -> bool {
89 !matches!(
90 self,
91 Kind::Manifest | Kind::Memory | Kind::Audit | Kind::Cred | Kind::Event
92 )
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct EntityRef {
99 pub kind: String,
100 pub id: String,
101 pub seq: u64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
106pub struct StreamMeta {
107 pub seq: u64,
109 pub first: u64,
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
116pub struct Manifest {
117 #[serde(default)]
118 pub generation: u64,
119 #[serde(default)]
120 pub created: u64,
121 #[serde(default)]
122 pub updated: u64,
123 #[serde(default)]
124 pub entities: Vec<EntityRef>,
125 #[serde(default)]
127 pub starts: BTreeMap<String, Value>,
128 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
132 pub streams: BTreeMap<String, StreamMeta>,
133 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
138 pub breakers: BTreeMap<String, Value>,
139 #[serde(default)]
142 pub budget: Value,
143 #[serde(default)]
144 pub lifecycle: Value,
145 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
151 pub config_digest: BTreeMap<String, String>,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub retired: Vec<EntityRef>,
158}
159
160impl Manifest {
161 fn upsert(&mut self, kind: &str, id: &str, seq: u64) {
162 match self
163 .entities
164 .iter_mut()
165 .find(|e| e.kind == kind && e.id == id)
166 {
167 Some(e) => e.seq = seq,
168 None => self.entities.push(EntityRef {
169 kind: kind.to_string(),
170 id: id.to_string(),
171 seq,
172 }),
173 }
174 }
175 fn remove(&mut self, kind: &str, id: &str) {
176 self.entities.retain(|e| !(e.kind == kind && e.id == id));
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct InboxEvent {
184 pub id: String,
185 pub kind: String,
186 pub ts: u64,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub principal: Option<String>,
189 pub payload: Value,
190 #[serde(default)]
191 pub status: InboxStatus,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
195#[serde(rename_all = "lowercase")]
196pub enum InboxStatus {
197 #[default]
198 Pending,
199 Done,
200}
201
202impl InboxEvent {
203 pub fn new(kind: &str, principal: Option<String>, payload: Value) -> InboxEvent {
204 InboxEvent {
205 id: ulid::new(),
206 kind: kind.to_string(),
207 ts: now_ms(),
208 principal,
209 payload,
210 status: InboxStatus::Pending,
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct TimerRecord {
220 pub id: String,
221 pub deadline_ms: u64,
222 pub owner: Value,
223 #[serde(default)]
224 pub payload: Value,
225}
226
227#[derive(Debug, Clone)]
230pub struct Policy {
231 pub debounce: Duration,
232 pub on_error: crate::config::v2::StoreOnError,
233 pub retries: u32,
234 pub max_value_bytes: Option<u64>,
245}
246
247impl Default for Policy {
248 fn default() -> Self {
249 Policy {
250 debounce: Duration::from_millis(250),
251 on_error: crate::config::v2::StoreOnError::Halt,
252 retries: 3,
253 max_value_bytes: None,
254 }
255 }
256}
257
258impl Policy {
259 pub fn from_settings(s: &crate::config::v2::Store) -> Policy {
260 Policy {
261 debounce: Duration::from_millis(s.checkpoint.debounce_ms.unwrap_or(250)),
262 on_error: s.on_error,
263 retries: 3,
264 max_value_bytes: s.max_value_bytes,
265 }
266 }
267}
268
269static FRESH: AtomicBool = AtomicBool::new(false);
284static CONFIG_DIGEST: OnceLock<BTreeMap<String, String>> = OnceLock::new();
285
286pub fn request_fresh() {
289 FRESH.store(true, Ordering::Relaxed);
290}
291
292pub fn fresh_requested() -> bool {
294 FRESH.load(Ordering::Relaxed)
295}
296
297pub fn record_config_digest(settings: &crate::config::v2::Settings) {
301 let _ = CONFIG_DIGEST.set(config_digest(settings));
302}
303
304fn recorded_config_digest() -> BTreeMap<String, String> {
305 CONFIG_DIGEST.get().cloned().unwrap_or_default()
306}
307
308pub fn config_digest(settings: &crate::config::v2::Settings) -> BTreeMap<String, String> {
324 let mut out = BTreeMap::new();
325 let digest = |v: &Value| crate::sha::sha256_hex(v.to_string().as_bytes());
328 out.insert(
329 "workflows".to_string(),
330 digest(&Value::Array(settings.workflows.clone())),
331 );
332 out.insert("store".to_string(), digest(&store_shape(&settings.store)));
333 out.insert(
334 "limits".to_string(),
335 digest(&limits_shape(&settings.limits)),
336 );
337 out
338}
339
340fn store_shape(s: &crate::config::v2::Store) -> Value {
345 json!({
346 "kind": format!("{:?}", s.kind),
347 "prefix": s.prefix(),
348 "on_error": format!("{:?}", s.on_error),
349 "audit": s.audit,
350 "checkpoint_debounce_ms": s.checkpoint.debounce_ms,
351 "durability": format!("{:?}", s.durability),
352 "timeout_ms": s.timeout.map(|d| d.0.as_millis() as u64),
353 "mcp_server": s.mcp.as_ref().map(|m| m.server.clone()),
357 "http": s.http.is_some(),
358 })
359}
360
361fn limits_shape(s: &crate::config::v2::Limits) -> Value {
365 json!({
366 "max_runs": s.max_runs,
367 "run_steps": s.run.steps(),
368 "run_tokens": s.run.tokens(),
369 "run_deadline_ms": s.run.deadline().as_millis() as u64,
370 "subagents": format!("{:?}", s.subagents),
371 "inline_max_bytes": s.inline_max_bytes,
372 "step_timeout_ms": s.step_timeout.map(|d| d.0.as_millis() as u64),
373 })
374}
375
376fn changed_sections(
383 recorded: &BTreeMap<String, String>,
384 current: &BTreeMap<String, String>,
385) -> Vec<String> {
386 if recorded.is_empty() || current.is_empty() {
387 return Vec::new();
388 }
389 let mut out: Vec<String> = current
390 .iter()
391 .filter(|(k, v)| recorded.get(*k) != Some(*v))
392 .map(|(k, _)| k.clone())
393 .collect();
394 out.extend(
395 recorded
396 .keys()
397 .filter(|k| !current.contains_key(*k))
398 .cloned(),
399 );
400 out.sort();
401 out.dedup();
402 out
403}
404
405const RECONCILED: [Kind; 7] = [
408 Kind::Inbox,
409 Kind::Context,
410 Kind::Run,
411 Kind::Subagent,
412 Kind::Task,
413 Kind::Timer,
414 Kind::Artifact,
415];
416
417#[derive(Debug, Default)]
419pub struct Restored {
420 pub manifest: Option<Manifest>,
422 pub entities: BTreeMap<String, Vec<Envelope>>,
424 pub lost: Vec<EntityRef>,
426 pub unindexed: Vec<EntityRef>,
429}
430
431impl Restored {
432 pub fn inbox_pending(&self) -> Vec<InboxEvent> {
433 let mut out: Vec<InboxEvent> = self
434 .entities
435 .get("inbox")
436 .map(|v| {
437 v.iter()
438 .filter_map(|e| serde_json::from_value::<InboxEvent>(e.state.clone()).ok())
439 .filter(|e| e.status == InboxStatus::Pending)
440 .collect()
441 })
442 .unwrap_or_default();
443 out.sort_by(|a, b| a.ts.cmp(&b.ts).then(a.id.cmp(&b.id)));
444 out
445 }
446 pub fn timers(&self) -> Vec<TimerRecord> {
447 self.entities
448 .get("timer")
449 .map(|v| {
450 v.iter()
451 .filter_map(|e| serde_json::from_value(e.state.clone()).ok())
452 .collect()
453 })
454 .unwrap_or_default()
455 }
456 pub fn of(&self, kind: Kind) -> &[Envelope] {
457 self.entities
458 .get(kind.as_str())
459 .map(Vec::as_slice)
460 .unwrap_or(&[])
461 }
462 pub fn count(&self) -> usize {
463 self.entities.values().map(Vec::len).sum()
464 }
465}
466
467pub struct Durable {
469 store: SharedStore,
470 prefix: String,
471 instance: String,
472 policy: Policy,
473 seqs: Mutex<HashMap<String, u64>>,
476 manifest: Mutex<Manifest>,
477 manifest_dirty: AtomicBool,
478 last_flush: Mutex<Instant>,
479 degraded: AtomicBool,
480 log: Option<Logger>,
481 fresh: bool,
483 config_digest: BTreeMap<String, String>,
486}
487
488impl Durable {
489 pub fn new(
490 store: SharedStore,
491 prefix: &str,
492 instance: &str,
493 policy: Policy,
494 log: Option<Logger>,
495 ) -> Durable {
496 Durable {
497 store,
498 prefix: prefix.to_string(),
499 instance: instance.to_string(),
500 policy,
501 seqs: Mutex::new(HashMap::new()),
502 manifest: Mutex::new(Manifest::default()),
503 manifest_dirty: AtomicBool::new(false),
504 last_flush: Mutex::new(Instant::now()),
505 degraded: AtomicBool::new(false),
506 log,
507 fresh: fresh_requested(),
508 config_digest: recorded_config_digest(),
509 }
510 }
511
512 pub fn with_fresh(mut self, fresh: bool) -> Durable {
516 self.fresh = fresh;
517 self
518 }
519
520 pub fn with_config_digest(mut self, digest: BTreeMap<String, String>) -> Durable {
522 self.config_digest = digest;
523 self
524 }
525
526 pub fn store_kind(&self) -> &'static str {
527 self.store.kind()
528 }
529 pub fn instance(&self) -> &str {
530 &self.instance
531 }
532 pub fn prefix(&self) -> &str {
533 &self.prefix
534 }
535 pub fn key(&self, kind: Kind, id: &str) -> String {
536 crate::store::key(&self.prefix, &self.instance, kind.as_str(), id)
537 }
538 pub fn is_degraded(&self) -> bool {
540 self.degraded.load(Ordering::Relaxed)
541 }
542 pub fn policy(&self) -> &Policy {
543 &self.policy
544 }
545
546 pub fn put(
553 &self,
554 kind: Kind,
555 id: &str,
556 state: Value,
557 hash: Option<String>,
558 ) -> Result<u64, StoreError> {
559 let key = self.key(kind, id);
560 if let Some(cap) = self.policy.max_value_bytes {
564 let bytes = serde_json::to_vec(&state).map(|v| v.len()).unwrap_or(0) as u64;
565 if bytes > cap {
566 return Err(StoreError::TooLarge {
567 key: key.clone(),
568 bytes,
569 cap,
570 });
571 }
572 }
573 let mut adopted = false;
574 let started = std::time::Instant::now();
575 loop {
576 let (seq, warmed) = {
577 let seqs = self.seqs.lock().unwrap_or_else(|e| e.into_inner());
578 match seqs.get(&key).copied() {
579 Some(s) => (s + 1, true),
580 None => (1, false),
581 }
582 };
583 let env = Envelope::new(
584 kind.as_str(),
585 id,
586 seq,
587 &self.instance,
588 hash.clone(),
589 state.clone(),
590 );
591 kill_point("state.before_put");
592 let outcome = crate::store::with_retry(
593 || self.store.put(&key, seq, &env.to_value()),
594 self.policy.retries,
595 );
596 match outcome {
597 Ok(PutOutcome::Ok) => {
598 self.seqs
599 .lock()
600 .unwrap_or_else(|e| e.into_inner())
601 .insert(key.clone(), seq);
602 if kind.indexed() {
603 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
604 m.upsert(kind.as_str(), id, seq);
605 m.updated = now_ms();
606 self.manifest_dirty.store(true, Ordering::Relaxed);
607 }
608 self.degraded.store(false, Ordering::Relaxed);
609 kill_point("state.after_put");
610 crate::obs::metrics::record_store_op(
611 "ok",
612 started.elapsed().as_millis() as u64,
613 );
614 return Ok(seq);
615 }
616 Ok(PutOutcome::Conflict { latest_seq }) => {
617 if !warmed && !adopted {
618 if let Some(l) = latest_seq {
622 self.seqs
623 .lock()
624 .unwrap_or_else(|e| e.into_inner())
625 .insert(key.clone(), l);
626 adopted = true;
627 self.log_event("store.seq_adopted", json!({"key": key, "latest": l}));
628 continue;
629 }
630 }
631 self.log_event(
632 "store.conflict",
633 json!({"key": key, "seq": seq, "latest": latest_seq}),
634 );
635 crate::obs::metrics::record_store_op(
636 "conflict",
637 started.elapsed().as_millis() as u64,
638 );
639 return Err(StoreError::Conflict(format!(
640 "key {key}: another writer owns it (our seq {seq}, latest {latest_seq:?})"
641 )));
642 }
643 Err(e) => {
644 self.log_event("store.put.fail", json!({"key": key, "err": e.to_string()}));
645 crate::obs::metrics::record_store_op(
646 "error",
647 started.elapsed().as_millis() as u64,
648 );
649 if self.policy.on_error == crate::config::v2::StoreOnError::Degrade {
650 self.degraded.store(true, Ordering::Relaxed);
651 self.seqs
654 .lock()
655 .unwrap_or_else(|e| e.into_inner())
656 .insert(key.clone(), seq);
657 return Ok(seq);
658 }
659 return Err(e);
660 }
661 }
662 }
663 }
664
665 pub fn get(&self, kind: Kind, id: &str) -> Result<Option<Envelope>, StoreError> {
667 let key = self.key(kind, id);
668 let v = crate::store::with_retry(|| self.store.get(&key, None), self.policy.retries)?;
669 match v {
670 None => Ok(None),
671 Some(v) => {
672 let env = Envelope::from_value(v)?;
673 self.seqs
674 .lock()
675 .unwrap_or_else(|e| e.into_inner())
676 .insert(key, env.seq);
677 Ok(if env.is_tombstone() { None } else { Some(env) })
678 }
679 }
680 }
681
682 pub fn delete(&self, kind: Kind, id: &str) -> Result<(), StoreError> {
685 let key = self.key(kind, id);
686 match crate::store::with_retry(|| self.store.delete(&key), self.policy.retries) {
687 Ok(()) => {
688 self.seqs
689 .lock()
690 .unwrap_or_else(|e| e.into_inner())
691 .remove(&key);
692 }
693 Err(StoreError::Unsupported(_)) => {
694 self.put(kind, id, Value::Null, None)?;
695 }
696 Err(e) => return Err(e),
697 }
698 if kind.indexed() {
699 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
700 m.remove(kind.as_str(), id);
701 m.updated = now_ms();
702 self.manifest_dirty.store(true, Ordering::Relaxed);
703 }
704 Ok(())
705 }
706
707 pub fn list(&self, kind: Kind) -> Result<Vec<KeySeq>, StoreError> {
709 let prefix = format!("{}/{}/{}/", self.prefix, self.instance, kind.as_str());
710 self.store.list(&prefix)
711 }
712
713 pub fn inbox_put(&self, ev: &InboxEvent) -> Result<u64, StoreError> {
717 let seq = self.put(
718 Kind::Inbox,
719 &ev.id,
720 serde_json::to_value(ev).unwrap_or(Value::Null),
721 None,
722 )?;
723 kill_point("inbox.after_put");
724 Ok(seq)
725 }
726
727 pub fn inbox_done(&self, id: &str) -> Result<(), StoreError> {
729 self.delete(Kind::Inbox, id)
730 }
731
732 pub fn timer_arm(&self, t: &TimerRecord) -> Result<u64, StoreError> {
733 self.put(
734 Kind::Timer,
735 &t.id,
736 serde_json::to_value(t).unwrap_or(Value::Null),
737 None,
738 )
739 }
740
741 pub fn timer_disarm(&self, id: &str) -> Result<(), StoreError> {
742 self.delete(Kind::Timer, id)
743 }
744
745 pub fn manifest(&self) -> Manifest {
748 self.manifest
749 .lock()
750 .unwrap_or_else(|e| e.into_inner())
751 .clone()
752 }
753
754 pub fn manifest_update(&self, f: impl FnOnce(&mut Manifest)) {
757 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
758 f(&mut m);
759 m.updated = now_ms();
760 self.manifest_dirty.store(true, Ordering::Relaxed);
761 }
762
763 pub fn flush(&self, force: bool) -> Result<bool, StoreError> {
765 if !self.manifest_dirty.load(Ordering::Relaxed) {
766 return Ok(false);
767 }
768 {
769 let last = self.last_flush.lock().unwrap_or_else(|e| e.into_inner());
770 if !force && last.elapsed() < self.policy.debounce {
771 return Ok(false);
772 }
773 }
774 let snapshot = self.manifest();
775 self.put(
776 Kind::Manifest,
777 "agent",
778 serde_json::to_value(&snapshot).unwrap_or(Value::Null),
779 None,
780 )?;
781 self.manifest_dirty.store(false, Ordering::Relaxed);
782 *self.last_flush.lock().unwrap_or_else(|e| e.into_inner()) = Instant::now();
783 Ok(true)
784 }
785
786 pub fn restore(&self) -> Result<Restored, StoreError> {
796 let mut out = Restored::default();
797 let (manifest, fresh) = match self.get(Kind::Manifest, "agent")? {
798 None => (
799 Manifest {
800 generation: 0,
801 created: now_ms(),
802 updated: now_ms(),
803 ..Manifest::default()
804 },
805 true,
806 ),
807 Some(env) => (
808 serde_json::from_value::<Manifest>(env.state.clone())
809 .map_err(|e| StoreError::Corrupt(format!("manifest does not parse: {e}")))?,
810 false,
811 ),
812 };
813 if self.fresh {
818 return self.restore_fresh(manifest, fresh);
819 }
820 let moved = changed_sections(&manifest.config_digest, &self.config_digest);
827 if !moved.is_empty() {
828 self.log_event(
829 "store.config_changed",
830 json!({
831 "sections": moved,
832 "msg": "state was written under a different configuration — resuming anyway; --fresh to start a new generation",
833 }),
834 );
835 }
836 for r in &manifest.entities {
838 let Some(kind) = Kind::parse(&r.kind) else {
839 out.lost.push(r.clone());
840 continue;
841 };
842 match self.get(kind, &r.id)? {
843 Some(env) => out.entities.entry(r.kind.clone()).or_default().push(env),
844 None => out.lost.push(r.clone()),
845 }
846 }
847 let mut seen: Option<BTreeSet<(String, String)>> = None;
852 for kind in RECONCILED {
853 match self.list(kind) {
854 Ok(keys) => {
855 let seen = seen.get_or_insert_with(BTreeSet::new);
856 for ks in keys {
857 let Some((_, id)) =
858 crate::store::parse_key(&self.prefix, &self.instance, &ks.key)
859 else {
860 continue;
861 };
862 seen.insert((kind.as_str().to_string(), id.to_string()));
863 let indexed = manifest
864 .entities
865 .iter()
866 .any(|e| e.kind == kind.as_str() && e.id == id);
867 if indexed {
868 continue;
869 }
870 if manifest
875 .retired
876 .iter()
877 .any(|r| r.kind == kind.as_str() && r.id == id)
878 {
879 continue;
880 }
881 if let Some(env) = self.get(kind, id)? {
882 out.unindexed.push(EntityRef {
883 kind: kind.as_str().to_string(),
884 id: id.to_string(),
885 seq: env.seq,
886 });
887 out.entities
888 .entry(kind.as_str().to_string())
889 .or_default()
890 .push(env);
891 }
892 }
893 }
894 Err(StoreError::Unsupported(_)) => {}
895 Err(e) => return Err(e),
896 }
897 }
898 let mut m = manifest.clone();
902 m.entities.retain(|e| !out.lost.iter().any(|l| l == e));
903 for u in &out.unindexed {
904 m.upsert(&u.kind, &u.id, u.seq);
905 }
906 m.generation += 1;
907 m.updated = now_ms();
908 if !self.config_digest.is_empty() {
913 m.config_digest = self.config_digest.clone();
914 }
915 if let Some(seen) = &seen {
919 m.retired
920 .retain(|r| seen.contains(&(r.kind.clone(), r.id.clone())));
921 }
922 *self.manifest.lock().unwrap_or_else(|e| e.into_inner()) = m.clone();
923 self.manifest_dirty.store(true, Ordering::Relaxed);
924 self.flush(true)?;
925 if fresh && out.count() == 0 {
926 self.log_event("restore.fresh", json!({"generation": m.generation}));
927 return Ok(out);
928 }
929 self.log_event(
930 "restore.done",
931 json!({
932 "generation": m.generation,
933 "fresh_manifest": fresh,
934 "entities": out.count(),
935 "lost": out.lost.len(),
936 "unindexed": out.unindexed.len(),
937 "inbox_pending": out.inbox_pending().len(),
938 }),
939 );
940 out.manifest = Some(m);
941 Ok(out)
942 }
943
944 fn restore_fresh(
960 &self,
961 prior: Manifest,
962 no_prior_manifest: bool,
963 ) -> Result<Restored, StoreError> {
964 let mut retired: Vec<EntityRef> = Vec::new();
967 let mut push = |kind: &str, id: &str, seq: u64| {
968 if !retired.iter().any(|r| r.kind == kind && r.id == id) {
969 retired.push(EntityRef {
970 kind: kind.to_string(),
971 id: id.to_string(),
972 seq,
973 });
974 }
975 };
976 for kind in RECONCILED {
977 match self.list(kind) {
978 Ok(keys) => {
979 for ks in keys {
980 if let Some((_, id)) =
981 crate::store::parse_key(&self.prefix, &self.instance, &ks.key)
982 {
983 push(kind.as_str(), id, ks.seq.unwrap_or(0));
984 }
985 }
986 }
987 Err(StoreError::Unsupported(_)) => {}
988 Err(e) => return Err(e),
989 }
990 }
991 for e in &prior.entities {
992 push(&e.kind, &e.id, e.seq);
993 }
994 for e in &prior.retired {
995 push(&e.kind, &e.id, e.seq);
996 }
997 if !no_prior_manifest {
1000 self.put(
1001 Kind::Manifest,
1002 &format!("agent.gen{}", prior.generation),
1003 serde_json::to_value(&prior).unwrap_or(Value::Null),
1004 None,
1005 )?;
1006 }
1007 let m = Manifest {
1010 generation: prior.generation + 1,
1011 created: if prior.created == 0 {
1012 now_ms()
1013 } else {
1014 prior.created
1015 },
1016 updated: now_ms(),
1017 entities: Vec::new(),
1018 starts: BTreeMap::new(),
1019 streams: BTreeMap::new(),
1020 breakers: BTreeMap::new(),
1021 budget: Value::Null,
1022 lifecycle: Value::Null,
1023 config_digest: self.config_digest.clone(),
1024 retired,
1025 };
1026 *self.manifest.lock().unwrap_or_else(|e| e.into_inner()) = m.clone();
1027 self.manifest_dirty.store(true, Ordering::Relaxed);
1028 self.flush(true)?;
1029 self.log_event(
1030 "restore.fresh",
1031 json!({
1032 "generation": m.generation,
1033 "superseded": prior.generation,
1034 "retired": m.retired.len(),
1035 "msg": "--fresh: this generation starts empty; the previous one's records were kept, not deleted",
1036 }),
1037 );
1038 Ok(Restored::default())
1039 }
1040
1041 fn log_event(&self, event: &str, fields: Value) {
1042 if let Some(l) = &self.log {
1043 match event {
1044 e if e.ends_with(".fail")
1045 || e == "store.conflict"
1046 || e == "store.config_changed" =>
1049 {
1050 l.warn(event, fields)
1051 }
1052 _ => l.info(event, fields),
1053 }
1054 }
1055 }
1056}
1057
1058pub fn kill_point(name: &str) {
1064 #[cfg(any(feature = "internal-mocks", debug_assertions))]
1065 {
1066 if std::env::var("AGENTD_TEST_KILL_AT").as_deref() == Ok(name) {
1067 #[cfg(unix)]
1068 unsafe {
1069 libc::raise(libc::SIGKILL);
1070 }
1071 std::process::abort();
1072 }
1073 }
1074 #[cfg(not(any(feature = "internal-mocks", debug_assertions)))]
1075 {
1076 let _ = name;
1077 }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083 use crate::store::Store;
1084 use crate::store::memory::MemoryStore;
1085 use std::sync::Arc;
1086
1087 fn durable(store: Arc<MemoryStore>) -> Durable {
1088 Durable::new(
1089 store,
1090 "agentd",
1091 "inst",
1092 Policy {
1093 debounce: Duration::from_millis(0),
1094 ..Policy::default()
1095 },
1096 None,
1097 )
1098 }
1099
1100 #[test]
1101 fn put_allocates_seqs_indexes_and_flushes_manifest() {
1102 let mem = Arc::new(MemoryStore::new());
1103 let d = durable(mem.clone());
1104 assert!(d.restore().unwrap().manifest.is_none(), "fresh");
1105 assert_eq!(
1106 d.put(
1107 Kind::Run,
1108 "r1",
1109 json!({"status": "running"}),
1110 Some("h".into())
1111 )
1112 .unwrap(),
1113 1
1114 );
1115 assert_eq!(
1116 d.put(Kind::Run, "r1", json!({"status": "done"}), Some("h".into()))
1117 .unwrap(),
1118 2
1119 );
1120 assert_eq!(
1121 d.put(Kind::Context, "root", json!({"v": 1}), None).unwrap(),
1122 1
1123 );
1124 let env = d.get(Kind::Run, "r1").unwrap().unwrap();
1125 assert_eq!(env.seq, 2);
1126 assert_eq!(env.state["status"], json!("done"));
1127 assert_eq!(env.hash.as_deref(), Some("h"));
1128 assert!(d.flush(true).unwrap());
1130 let m = d.manifest();
1131 assert_eq!(m.entities.len(), 2);
1132 assert!(
1133 m.entities
1134 .iter()
1135 .any(|e| e.kind == "run" && e.id == "r1" && e.seq == 2)
1136 );
1137 assert!(!d.flush(true).unwrap(), "clean after a flush");
1138 d.delete(Kind::Context, "root").unwrap();
1140 assert!(d.get(Kind::Context, "root").unwrap().is_none());
1141 assert_eq!(d.manifest().entities.len(), 1);
1142 }
1143
1144 #[test]
1145 fn conflicts_are_fatal_on_owned_keys_but_adopted_on_first_touch() {
1146 let mem = Arc::new(MemoryStore::new());
1147 let stale = Envelope::new("run", "old", 5, "inst", None, json!({"x": 1}));
1149 mem.put("agentd/inst/run/old", 5, &stale.to_value())
1150 .unwrap();
1151 let d = durable(mem.clone());
1152 assert_eq!(d.put(Kind::Run, "old", json!({"x": 2}), None).unwrap(), 6);
1154 let other = Envelope::new("run", "old", 7, "other", None, json!({"x": 3}));
1156 mem.put("agentd/inst/run/old", 7, &other.to_value())
1157 .unwrap();
1158 assert!(matches!(
1159 d.put(Kind::Run, "old", json!({"x": 4}), None),
1160 Err(StoreError::Conflict(_))
1161 ));
1162 }
1163
1164 #[test]
1165 fn inbox_write_ahead_timers_and_restore() {
1166 let mem = Arc::new(MemoryStore::new());
1167 {
1168 let d = durable(mem.clone());
1169 d.restore().unwrap();
1170 let e1 = InboxEvent::new(
1171 "a2a_message",
1172 Some("user:andrii".into()),
1173 json!({"text": "hi"}),
1174 );
1175 let e2 = InboxEvent::new("start_fired", None, json!({"workflow": "w"}));
1176 d.inbox_put(&e1).unwrap();
1177 d.inbox_put(&e2).unwrap();
1178 d.inbox_done(&e1.id).unwrap();
1179 d.timer_arm(&TimerRecord {
1180 id: "t1".into(),
1181 deadline_ms: 42,
1182 owner: json!({"run": "r"}),
1183 payload: Value::Null,
1184 })
1185 .unwrap();
1186 d.put(
1187 Kind::Run,
1188 "r",
1189 json!({"status": "running"}),
1190 Some("hash".into()),
1191 )
1192 .unwrap();
1193 d.put(Kind::Task, "task-1", json!({"state": "working"}), None)
1194 .unwrap();
1195 d.manifest_update(|m| {
1196 m.starts.insert("w.s".into(), json!({"last_fired": 1}));
1197 });
1198 d.put(Kind::Subagent, "gone", json!({}), None).unwrap();
1200 d.flush(true).unwrap();
1201 mem.delete("agentd/inst/subagent/gone").unwrap();
1202 d.put(Kind::Run, "r2", json!({"status": "running"}), None)
1205 .unwrap();
1206 }
1207 let d2 = durable(mem.clone());
1209 let r = d2.restore().unwrap();
1210 let m = r.manifest.as_ref().unwrap();
1211 assert_eq!(m.generation, 2, "generation bumped");
1212 assert_eq!(m.starts["w.s"]["last_fired"], json!(1));
1213 let pending = r.inbox_pending();
1214 assert_eq!(pending.len(), 1, "the done event does not replay");
1215 assert_eq!(pending[0].kind, "start_fired");
1216 assert_eq!(r.timers().len(), 1);
1217 assert_eq!(r.timers()[0].deadline_ms, 42);
1218 assert_eq!(
1219 r.of(Kind::Run).len(),
1220 2,
1221 "indexed + unindexed runs restored"
1222 );
1223 assert!(r.unindexed.iter().any(|u| u.id == "r2"));
1224 assert!(
1225 r.lost
1226 .iter()
1227 .any(|l| l.kind == "subagent" && l.id == "gone")
1228 );
1229 assert_eq!(r.of(Kind::Task).len(), 1);
1230 assert_eq!(
1232 d2.put(
1233 Kind::Run,
1234 "r",
1235 json!({"status": "done"}),
1236 Some("hash".into())
1237 )
1238 .unwrap(),
1239 2
1240 );
1241 assert!(!d2.manifest().entities.iter().any(|e| e.id == "gone"));
1243 }
1244
1245 #[test]
1255 fn a_value_over_the_cap_is_refused_and_not_written() {
1256 let mem = Arc::new(MemoryStore::new());
1257 let d = Durable::new(
1258 mem.clone(),
1259 "agentd",
1260 "inst",
1261 Policy {
1262 debounce: Duration::from_millis(0),
1263 on_error: crate::config::v2::StoreOnError::Degrade,
1264 retries: 1,
1265 max_value_bytes: Some(512),
1266 },
1267 None,
1268 );
1269
1270 let small = json!({"text": "x".repeat(100)});
1272 assert!(d.put(Kind::Run, "small", small, None).is_ok());
1273 assert!(d.get(Kind::Run, "small").unwrap().is_some());
1274
1275 let big = json!({"text": "x".repeat(4096)});
1277 let err = d.put(Kind::Run, "big", big, None).unwrap_err();
1278 match &err {
1279 StoreError::TooLarge { key, bytes, cap } => {
1280 assert!(key.contains("big"), "the error names the key: {key}");
1281 assert!(*bytes > 4096 && *cap == 512, "{bytes} over {cap}");
1282 }
1283 other => panic!("expected TooLarge, got {other:?}"),
1284 }
1285 let msg = err.to_string();
1287 assert!(
1288 msg.contains("NOT written") && msg.contains("restore"),
1289 "{msg}"
1290 );
1291 assert!(d.get(Kind::Run, "big").unwrap().is_none());
1294 }
1295
1296 #[test]
1297 fn degrade_policy_keeps_going_and_flags_it() {
1298 let mem = Arc::new(MemoryStore::new());
1299 let d = Durable::new(
1300 mem.clone(),
1301 "agentd",
1302 "inst",
1303 Policy {
1304 debounce: Duration::from_millis(0),
1305 on_error: crate::config::v2::StoreOnError::Degrade,
1306 retries: 1,
1307 max_value_bytes: None,
1308 },
1309 None,
1310 );
1311 mem.fail_next(1);
1312 assert_eq!(
1313 d.put(Kind::Run, "r", json!({}), None).unwrap(),
1314 1,
1315 "degraded write reports the intended seq"
1316 );
1317 assert!(d.is_degraded());
1318 assert_eq!(
1319 d.put(Kind::Run, "r", json!({}), None).unwrap(),
1320 2,
1321 "seq not reused"
1322 );
1323 assert!(!d.is_degraded(), "a successful write clears the flag");
1324 let d2 = durable(mem.clone());
1326 mem.fail_next(5);
1327 assert!(matches!(
1328 d2.put(Kind::Run, "x", json!({}), None),
1329 Err(StoreError::Io(_))
1330 ));
1331 }
1332
1333 #[test]
1338 fn fresh_opens_a_new_generation_without_resuming_and_deletes_nothing() {
1339 let mem = Arc::new(MemoryStore::new());
1340
1341 let d = durable(mem.clone());
1343 assert!(d.restore().unwrap().manifest.is_none());
1344 d.put(Kind::Run, "r1", json!({"status": "running"}), None)
1345 .unwrap();
1346 let ev = InboxEvent::new("a2a.message", None, json!({"n": 1}));
1347 d.inbox_put(&ev).unwrap();
1348 assert_eq!(d.manifest().generation, 1);
1349
1350 let f = durable(mem.clone()).with_fresh(true);
1352 let r = f.restore().unwrap();
1353 assert!(
1354 r.manifest.is_none(),
1355 "a new generation reports no prior life"
1356 );
1357 assert_eq!(r.count(), 0);
1358 assert!(r.inbox_pending().is_empty(), "the inbox does not replay");
1359 let m = f.manifest();
1360 assert_eq!(m.generation, 2, "the counter is inherited, not reset");
1361 assert!(m.entities.is_empty());
1362
1363 assert!(
1365 f.get(Kind::Run, "r1").unwrap().is_some(),
1366 "--fresh keeps the abandoned records"
1367 );
1368 let kept = f
1369 .get(Kind::Manifest, "agent.gen1")
1370 .unwrap()
1371 .expect("the outgoing manifest is preserved");
1372 let kept: Manifest = serde_json::from_value(kept.state).unwrap();
1373 assert_eq!(kept.generation, 1);
1374 assert!(m.retired.iter().any(|e| e.kind == "run" && e.id == "r1"));
1375 assert!(m.retired.iter().any(|e| e.kind == "inbox" && e.id == ev.id));
1376
1377 let d3 = durable(mem.clone());
1380 let r3 = d3.restore().unwrap();
1381 assert_eq!(r3.count(), 0, "retired records stay retired");
1382 assert!(r3.unindexed.is_empty());
1383 assert_eq!(d3.manifest().generation, 3);
1384 }
1385
1386 #[test]
1390 fn a_moved_config_digest_reports_but_never_gates_the_resume() {
1391 let before: BTreeMap<String, String> = [
1392 ("workflows".to_string(), "aaa".to_string()),
1393 ("store".to_string(), "sss".to_string()),
1394 ]
1395 .into_iter()
1396 .collect();
1397 let mut after = before.clone();
1398 after.insert("workflows".to_string(), "bbb".to_string());
1399 assert_eq!(changed_sections(&before, &after), vec!["workflows"]);
1400 assert!(changed_sections(&BTreeMap::new(), &after).is_empty());
1403 assert!(changed_sections(&before, &BTreeMap::new()).is_empty());
1404
1405 let mem = Arc::new(MemoryStore::new());
1406 let d = durable(mem.clone()).with_config_digest(before.clone());
1407 d.restore().unwrap();
1408 d.put(Kind::Run, "r1", json!({"status": "running"}), None)
1409 .unwrap();
1410 assert_eq!(d.manifest().config_digest, before);
1411
1412 let d2 = durable(mem.clone()).with_config_digest(after.clone());
1413 let r = d2.restore().unwrap();
1414 assert_eq!(r.of(Kind::Run).len(), 1, "state is still resumed");
1415 assert_eq!(d2.manifest().generation, 2);
1416 assert_eq!(
1417 d2.manifest().config_digest,
1418 after,
1419 "the next life compares against what this one ran under"
1420 );
1421 }
1422
1423 #[test]
1426 fn the_digest_covers_workflows_store_and_limits_only() {
1427 let doc = json!({
1428 "config_version": "1",
1429 "agent": {"name": "a", "instruction": "one"},
1430 "workflows": [{"name": "w", "version": 3, "steps": {"s": {"kind": "once"}}}],
1431 "limits": {"run": {"steps": 10}},
1432 });
1433 let settings = |patch: &dyn Fn(&mut Value)| {
1434 let mut d = doc.clone();
1435 patch(&mut d);
1436 serde_json::from_value::<crate::config::v2::Settings>(d).expect("settings")
1437 };
1438 let base = config_digest(&settings(&|_| {}));
1439 assert_eq!(
1440 base.keys().collect::<Vec<_>>(),
1441 ["limits", "store", "workflows"]
1442 );
1443
1444 let elsewhere = config_digest(&settings(&|d| d["agent"]["instruction"] = json!("two")));
1445 assert_eq!(base, elsewhere, "an instruction edit is not a state change");
1446
1447 let wf = config_digest(&settings(&|d| {
1448 d["workflows"][0]["steps"]["t"] = json!({"kind": "noop"})
1449 }));
1450 assert_eq!(changed_sections(&base, &wf), vec!["workflows"]);
1451
1452 let lim = config_digest(&settings(&|d| d["limits"]["run"]["steps"] = json!(11)));
1453 assert_eq!(changed_sections(&base, &lim), vec!["limits"]);
1454 }
1455}