1mod discovery;
8mod load;
9
10use gate4agent_adapters::{
11 history_source_variants, parse_history, HistoryDocument, HistorySession, HistorySourceLayout,
12 HISTORY_DOCUMENT_MAX_BYTES, HISTORY_METADATA_MAX_BYTES,
13};
14use gate4agent_provider_ports::{
15 HistoryAuthority, HistoryCandidate, HistoryCandidateId, HistoryDiscoveryRequest,
16 HistoryLoadRequest,
17};
18use gate4agent_types::{
19 AdapterBinding, AdapterId, AgentId, ProviderSessionIdentity, ProviderSessionKey,
20};
21use std::collections::{HashMap, HashSet, VecDeque};
22use std::fmt;
23use std::path::{Path, PathBuf};
24use thiserror::Error;
25use uuid::Uuid;
26
27pub const HISTORY_ROOTS_MAX: usize = 64;
28pub const HISTORY_CANDIDATES_MAX: usize = 50_000;
29pub const HISTORY_CACHE_ENTRIES_MAX: usize = 256;
30pub const HISTORY_WALK_ENTRIES_MAX: usize = 50_000;
31pub const HISTORY_WALK_DEPTH_MAX: usize = 16;
32pub const HISTORY_SIBLING_FILES_MAX: usize = 1_024;
33pub const HISTORY_AUXILIARY_INDEX_MAX_BYTES: usize = 8 * 1024 * 1024;
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct NativeHistoryLimits {
37 pub max_candidates: usize,
38 pub max_cache_entries: usize,
39 pub max_walk_entries: usize,
40 pub max_walk_depth: usize,
41 pub max_sibling_files: usize,
42}
43
44impl Default for NativeHistoryLimits {
45 fn default() -> Self {
46 Self {
47 max_candidates: HISTORY_CANDIDATES_MAX,
48 max_cache_entries: HISTORY_CACHE_ENTRIES_MAX,
49 max_walk_entries: HISTORY_WALK_ENTRIES_MAX,
50 max_walk_depth: HISTORY_WALK_DEPTH_MAX,
51 max_sibling_files: HISTORY_SIBLING_FILES_MAX,
52 }
53 }
54}
55
56impl NativeHistoryLimits {
57 fn validate(self) -> Result<Self, NativeHistoryConfigError> {
58 if self.max_candidates == 0 || self.max_candidates > HISTORY_CANDIDATES_MAX {
59 return Err(NativeHistoryConfigError::InvalidLimit("max_candidates"));
60 }
61 if self.max_cache_entries == 0 || self.max_cache_entries > HISTORY_CACHE_ENTRIES_MAX {
62 return Err(NativeHistoryConfigError::InvalidLimit("max_cache_entries"));
63 }
64 if self.max_walk_entries == 0 || self.max_walk_entries > HISTORY_WALK_ENTRIES_MAX {
65 return Err(NativeHistoryConfigError::InvalidLimit("max_walk_entries"));
66 }
67 if self.max_walk_depth == 0 || self.max_walk_depth > HISTORY_WALK_DEPTH_MAX {
68 return Err(NativeHistoryConfigError::InvalidLimit("max_walk_depth"));
69 }
70 if self.max_sibling_files == 0 || self.max_sibling_files > HISTORY_SIBLING_FILES_MAX {
71 return Err(NativeHistoryConfigError::InvalidLimit("max_sibling_files"));
72 }
73 Ok(self)
74 }
75}
76
77#[derive(Clone, Eq, PartialEq)]
78pub struct NativeHistoryRoot {
79 adapter_id: AdapterId,
80 layout: HistorySourceLayout,
81 path: PathBuf,
82}
83
84impl NativeHistoryRoot {
85 pub fn new(
86 adapter_id: AdapterId,
87 layout: HistorySourceLayout,
88 path: impl Into<PathBuf>,
89 ) -> Result<Self, NativeHistoryConfigError> {
90 let path = path.into();
91 if !path.is_absolute() {
92 return Err(NativeHistoryConfigError::RootMustBeAbsolute);
93 }
94 let supported = history_source_variants(&adapter_id)
95 .map_err(|_| NativeHistoryConfigError::UnsupportedAdapter(adapter_id.clone()))?;
96 if !supported.iter().any(|variant| variant.layout == layout) {
97 return Err(NativeHistoryConfigError::UnsupportedLayout { adapter_id, layout });
98 }
99 Ok(Self {
100 adapter_id,
101 layout,
102 path,
103 })
104 }
105
106 pub fn adapter_id(&self) -> &AdapterId {
107 &self.adapter_id
108 }
109
110 pub fn layout(&self) -> HistorySourceLayout {
111 self.layout
112 }
113}
114
115impl fmt::Debug for NativeHistoryRoot {
116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117 formatter
118 .debug_struct("NativeHistoryRoot")
119 .field("adapter_id", &self.adapter_id)
120 .field("layout", &self.layout)
121 .field("path", &"[REDACTED]")
122 .finish()
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct NativeHistoryConfig {
128 roots: Vec<NativeHistoryRoot>,
129 limits: NativeHistoryLimits,
130}
131
132impl NativeHistoryConfig {
133 pub fn new(roots: Vec<NativeHistoryRoot>) -> Result<Self, NativeHistoryConfigError> {
134 Self::with_limits(roots, NativeHistoryLimits::default())
135 }
136
137 pub fn with_limits(
138 roots: Vec<NativeHistoryRoot>,
139 limits: NativeHistoryLimits,
140 ) -> Result<Self, NativeHistoryConfigError> {
141 if roots.len() > HISTORY_ROOTS_MAX {
142 return Err(NativeHistoryConfigError::TooManyRoots);
143 }
144 let limits = limits.validate()?;
145 let mut unique = HashSet::new();
146 for root in &roots {
147 if !unique.insert((root.adapter_id.clone(), root.layout, root.path.clone())) {
148 return Err(NativeHistoryConfigError::DuplicateRoot);
149 }
150 }
151 Ok(Self { roots, limits })
152 }
153
154 pub fn roots(&self) -> &[NativeHistoryRoot] {
155 &self.roots
156 }
157
158 pub fn limits(&self) -> NativeHistoryLimits {
159 self.limits
160 }
161}
162
163pub fn orca_home_roots(
168 home: impl AsRef<Path>,
169) -> Result<Vec<NativeHistoryRoot>, NativeHistoryConfigError> {
170 let home = home.as_ref();
171 if !home.is_absolute() {
172 return Err(NativeHistoryConfigError::RootMustBeAbsolute);
173 }
174 let mut roots = Vec::new();
175 let mut push = |id: &str, layout, path: PathBuf| -> Result<(), NativeHistoryConfigError> {
176 let adapter_id = AdapterId::new(id)
177 .map_err(|_| NativeHistoryConfigError::UnsupportedAdapterValue(id.to_owned()))?;
178 roots.push(NativeHistoryRoot::new(adapter_id, layout, path)?);
179 Ok(())
180 };
181
182 push(
183 "claude-code",
184 HistorySourceLayout::SingleNdjson,
185 home.join(".claude").join("projects"),
186 )?;
187 push(
188 "claude-code",
189 HistorySourceLayout::SingleNdjson,
190 home.join(".claude").join("archive"),
191 )?;
192 push(
193 "codex",
194 HistorySourceLayout::NdjsonWithOptionalIndex,
195 home.join(".codex").join("sessions"),
196 )?;
197 push(
198 "cursor",
199 HistorySourceLayout::SingleNdjson,
200 home.join(".cursor").join("projects"),
201 )?;
202 push(
203 "grok",
204 HistorySourceLayout::SummaryJsonWithSiblingNdjson,
205 home.join(".grok").join("sessions"),
206 )?;
207 push(
208 "rovo",
209 HistorySourceLayout::MetadataJsonWithSiblingJson,
210 home.join(".rovodev").join("sessions"),
211 )?;
212 for state_root in [home.join(".openclaw"), home.join(".clawdbot")] {
213 push(
214 "openclaw",
215 HistorySourceLayout::SingleNdjson,
216 state_root.join("agents"),
217 )?;
218 }
219 push(
220 "pi",
221 HistorySourceLayout::SingleNdjson,
222 home.join(".pi").join("agent").join("sessions"),
223 )?;
224 push(
225 "omp",
226 HistorySourceLayout::SingleNdjson,
227 home.join(".omp").join("agent").join("sessions"),
228 )?;
229 push(
230 "kimi",
231 HistorySourceLayout::StateJsonWithIndexAndSiblingNdjson,
232 home.join(".kimi-code").join("sessions"),
233 )?;
234 Ok(roots)
235}
236
237#[derive(Clone, Debug, Eq, Hash, PartialEq)]
238struct SourceIdentity {
239 agent_id: AgentId,
240 binding: AdapterBinding,
241 working_directory: Option<String>,
242}
243
244impl SourceIdentity {
245 fn from_request(request: &HistoryDiscoveryRequest) -> Self {
246 Self {
247 agent_id: request.agent_id().clone(),
248 binding: request.binding().clone(),
249 working_directory: request.working_directory().map(str::to_owned),
250 }
251 }
252
253 fn matches_load(&self, request: &HistoryLoadRequest) -> bool {
254 &self.agent_id == request.agent_id()
255 && &self.binding == request.binding()
256 && self.working_directory.as_deref() == request.working_directory()
257 }
258}
259
260#[derive(Clone, Debug, Eq, Hash, PartialEq)]
261enum CandidateLocator {
262 File {
263 root: PathBuf,
264 primary: PathBuf,
265 layout: HistorySourceLayout,
266 },
267}
268
269#[derive(Clone, Debug, Eq, Hash, PartialEq)]
270struct CandidateKey {
271 source: SourceIdentity,
272 locator: CandidateLocator,
273}
274
275#[derive(Clone, Debug)]
276struct DiscoveredCandidate {
277 locator: CandidateLocator,
278 session_id_hint: String,
279 modified_at_unix_ms: Option<u64>,
280}
281
282#[derive(Clone, Debug)]
283struct CandidateRecord {
284 key: CandidateKey,
285 session_id_hint: String,
286}
287
288#[derive(Clone, Debug, Eq, PartialEq)]
289struct ComponentSignature {
290 path: PathBuf,
291 present: bool,
292 directory: bool,
293 len: u64,
294 modified_nanos: u128,
295}
296
297#[derive(Clone, Debug)]
298struct LoadedDocument {
299 document: HistoryDocument,
300 signatures: Vec<ComponentSignature>,
301}
302
303#[derive(Clone, Debug)]
304struct CacheEntry {
305 loaded: LoadedDocument,
306}
307
308#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
309pub struct NativeHistoryCacheStats {
310 pub entries: usize,
311 pub hits: u64,
312 pub misses: u64,
313}
314
315#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct NativeHistoryDiscoveryIssue {
317 pub root_slot: usize,
318 pub adapter_id: AdapterId,
319 pub kind: NativeHistoryDiscoveryIssueKind,
320}
321
322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323pub enum NativeHistoryDiscoveryIssueKind {
324 Inaccessible,
325 EntryLimitReached,
326 CandidateLimitReached,
327 InvalidDatabase,
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
331pub struct NativeHistoryLocator {
332 candidate: HistoryCandidate,
333 session_id: String,
334 cwd: Option<String>,
335 title: Option<String>,
336 model: Option<String>,
337}
338
339pub struct NativeHistoryPreviewLoad {
340 session: HistorySession,
341 source_truncated: bool,
342}
343
344impl NativeHistoryPreviewLoad {
345 pub fn session(self) -> HistorySession {
346 self.session
347 }
348
349 pub fn source_truncated(&self) -> bool {
350 self.source_truncated
351 }
352}
353
354impl NativeHistoryLocator {
355 pub fn candidate(&self) -> &HistoryCandidate {
356 &self.candidate
357 }
358
359 pub fn session_id(&self) -> &str {
360 &self.session_id
361 }
362
363 pub fn cwd(&self) -> Option<&str> {
364 self.cwd.as_deref()
365 }
366
367 pub fn title(&self) -> Option<&str> {
368 self.title.as_deref()
369 }
370
371 pub fn model(&self) -> Option<&str> {
372 self.model.as_deref()
373 }
374}
375
376pub struct NativeHistoryAuthority {
377 config: NativeHistoryConfig,
378 candidates: HashMap<String, CandidateRecord>,
379 ids_by_key: HashMap<CandidateKey, String>,
380 source_order: VecDeque<SourceIdentity>,
381 cache: HashMap<String, CacheEntry>,
382 cache_order: VecDeque<String>,
383 cache_hits: u64,
384 cache_misses: u64,
385 issues: Vec<NativeHistoryDiscoveryIssue>,
386}
387
388impl NativeHistoryAuthority {
389 pub fn new(config: NativeHistoryConfig) -> Self {
390 Self {
391 config,
392 candidates: HashMap::new(),
393 ids_by_key: HashMap::new(),
394 source_order: VecDeque::new(),
395 cache: HashMap::new(),
396 cache_order: VecDeque::new(),
397 cache_hits: 0,
398 cache_misses: 0,
399 issues: Vec::new(),
400 }
401 }
402
403 pub fn cache_stats(&self) -> NativeHistoryCacheStats {
404 NativeHistoryCacheStats {
405 entries: self.cache.len(),
406 hits: self.cache_hits,
407 misses: self.cache_misses,
408 }
409 }
410
411 pub fn active_candidate_count(&self) -> usize {
412 self.candidates.len()
413 }
414
415 pub fn clear_cache(&mut self) {
416 self.cache.clear();
417 self.cache_order.clear();
418 }
419
420 pub fn take_discovery_issues(&mut self) -> Vec<NativeHistoryDiscoveryIssue> {
421 std::mem::take(&mut self.issues)
422 }
423
424 pub fn discover_locator_index(
430 &mut self,
431 request: &HistoryDiscoveryRequest,
432 ) -> Result<Vec<NativeHistoryLocator>, NativeHistoryError> {
433 let candidates = self.refresh_candidates(
434 request,
435 self.config.limits.max_candidates,
436 true,
437 )?;
438 let mut locators = Vec::with_capacity(candidates.len());
439 for candidate in candidates {
440 let Some(record) = self.candidates.get(candidate.id().as_str()).cloned() else {
441 continue;
442 };
443 let Ok(document) = load::load_locator_document(&record, self.config.limits) else {
444 continue;
445 };
446 let Ok(session) = parse_history(&record.key.source.binding.id, &document) else {
447 continue;
448 };
449 let cwd = session.cwd.filter(|cwd| !cwd.trim().is_empty());
450 locators.push(NativeHistoryLocator {
451 candidate,
452 session_id: session.session_id,
453 cwd,
454 title: session.title,
455 model: session.model,
456 });
457 }
458 Ok(locators)
459 }
460
461 pub fn load_preview_session(
462 &mut self,
463 request: &HistoryLoadRequest,
464 ) -> Result<NativeHistoryPreviewLoad, NativeHistoryError> {
465 let id = request.candidate().id().as_str();
466 let record = self
467 .candidates
468 .get(id)
469 .ok_or(NativeHistoryError::CandidateExpired)?
470 .clone();
471 if !record.key.source.matches_load(request)
472 || record.session_id_hint != request.candidate().session_id_hint()
473 {
474 return Err(NativeHistoryError::CandidateSourceMismatch);
475 }
476 let (document, source_truncated) =
477 load::load_preview_document(&record, self.config.limits)?;
478 let session = parse_history(&record.key.source.binding.id, &document)
479 .map_err(|_| NativeHistoryError::InvalidDocument)?;
480 Ok(NativeHistoryPreviewLoad {
481 session,
482 source_truncated,
483 })
484 }
485
486 fn discover_candidates(
487 &mut self,
488 request: &HistoryDiscoveryRequest,
489 ) -> Result<Vec<HistoryCandidate>, NativeHistoryError> {
490 self.refresh_candidates(request, usize::from(request.limit()), false)
491 }
492
493 fn refresh_candidates(
494 &mut self,
495 request: &HistoryDiscoveryRequest,
496 scan_limit: usize,
497 report_candidate_overflow: bool,
498 ) -> Result<Vec<HistoryCandidate>, NativeHistoryError> {
499 self.issues.clear();
500 let source = SourceIdentity::from_request(request);
501 let mut discovered = Vec::new();
502 for (root_slot, root) in self.config.roots.iter().enumerate() {
503 if root.adapter_id != request.binding().id {
504 continue;
505 }
506 let result = discovery::discover_root(
507 root,
508 root_slot,
509 self.config.limits,
510 scan_limit,
511 report_candidate_overflow,
512 );
513 discovered.extend(result.candidates);
514 self.issues.extend(result.issues);
515 }
516 discovery::dedupe_and_sort(&mut discovered);
517 let scan_limit = scan_limit.min(self.config.limits.max_candidates);
518 if discovered.len() > scan_limit && report_candidate_overflow {
519 self.issues.push(NativeHistoryDiscoveryIssue {
520 root_slot: usize::MAX,
521 adapter_id: request.binding().id.clone(),
522 kind: NativeHistoryDiscoveryIssueKind::CandidateLimitReached,
523 });
524 }
525 discovered.truncate(scan_limit);
526
527 let current_locators = discovered
528 .iter()
529 .map(|candidate| candidate.locator.clone())
530 .collect::<HashSet<_>>();
531 let stale_ids = self
532 .candidates
533 .iter()
534 .filter(|(_, record)| {
535 record.key.source == source && !current_locators.contains(&record.key.locator)
536 })
537 .map(|(id, _)| id.clone())
538 .collect::<Vec<_>>();
539 for id in stale_ids {
540 self.remove_candidate(&id);
541 }
542 self.evict_sources_for(&source, discovered.len())?;
543
544 let mut candidates = Vec::with_capacity(discovered.len());
545 for discovered in discovered {
546 let key = CandidateKey {
547 source: source.clone(),
548 locator: discovered.locator.clone(),
549 };
550 let id = self
551 .ids_by_key
552 .get(&key)
553 .cloned()
554 .unwrap_or_else(|| self.fresh_candidate_id());
555 let candidate_id = HistoryCandidateId::new(id.clone())
556 .map_err(|_| NativeHistoryError::InvalidCandidate)?;
557 let Ok(candidate) = HistoryCandidate::new(
558 request,
559 candidate_id,
560 discovered.session_id_hint.clone(),
561 discovered.modified_at_unix_ms,
562 ) else {
563 continue;
564 };
565 let record = CandidateRecord {
566 key: key.clone(),
567 session_id_hint: discovered.session_id_hint,
568 };
569 self.ids_by_key.insert(key, id.clone());
570 self.candidates.insert(id, record);
571 candidates.push(candidate);
572 }
573 if candidates.is_empty() {
574 self.source_order.retain(|candidate| candidate != &source);
575 } else {
576 self.touch_source(&source);
577 }
578 Ok(candidates)
579 }
580
581 pub fn resume_provider_session(
582 &self,
583 request: &HistoryLoadRequest,
584 session_id: impl Into<String>,
585 ) -> Result<ProviderSessionIdentity, NativeHistoryError> {
586 let id = request.candidate().id().as_str();
587 let record = self
588 .candidates
589 .get(id)
590 .ok_or(NativeHistoryError::CandidateExpired)?;
591 if !record.key.source.matches_load(request)
592 || record.session_id_hint != request.candidate().session_id_hint()
593 {
594 return Err(NativeHistoryError::CandidateSourceMismatch);
595 }
596 let transcript_path = if request.binding().id.as_str() == "pi" {
597 let CandidateLocator::File { root, primary, .. } = &record.key.locator;
598 Some(
599 load::validated_resume_file(root, primary)?
600 .to_str()
601 .ok_or(NativeHistoryError::InvalidUtf8)?
602 .to_owned(),
603 )
604 } else {
605 None
606 };
607 Ok(ProviderSessionIdentity {
608 key: ProviderSessionKey::SessionId,
609 id: session_id.into(),
610 transcript_path,
611 })
612 }
613
614 fn load_candidate(
615 &mut self,
616 request: &HistoryLoadRequest,
617 ) -> Result<HistoryDocument, NativeHistoryError> {
618 let id = request.candidate().id().as_str();
619 let record = self
620 .candidates
621 .get(id)
622 .ok_or(NativeHistoryError::CandidateExpired)?
623 .clone();
624 if !record.key.source.matches_load(request)
625 || record.session_id_hint != request.candidate().session_id_hint()
626 {
627 return Err(NativeHistoryError::CandidateSourceMismatch);
628 }
629 self.touch_source(&record.key.source);
630
631 if let Some(entry) = self.cache.get(id).cloned() {
632 if load::signatures_are_current(&entry.loaded.signatures) {
633 self.cache_hits = self.cache_hits.saturating_add(1);
634 self.touch_cache(id);
635 return Ok(entry.loaded.document);
636 }
637 self.cache.remove(id);
638 self.cache_order.retain(|candidate| candidate != id);
639 }
640 self.cache_misses = self.cache_misses.saturating_add(1);
641 let loaded = load::load_document(&record, self.config.limits)?;
642 let document = loaded.document.clone();
643 self.cache.insert(id.to_owned(), CacheEntry { loaded });
644 self.touch_cache(id);
645 while self.cache.len() > self.config.limits.max_cache_entries {
646 if let Some(expired) = self.cache_order.pop_front() {
647 self.cache.remove(&expired);
648 }
649 }
650 Ok(document)
651 }
652
653 fn touch_cache(&mut self, id: &str) {
654 self.cache_order.retain(|candidate| candidate != id);
655 self.cache_order.push_back(id.to_owned());
656 }
657
658 fn touch_source(&mut self, source: &SourceIdentity) {
659 self.source_order.retain(|candidate| candidate != source);
660 self.source_order.push_back(source.clone());
661 }
662
663 fn evict_sources_for(
664 &mut self,
665 current: &SourceIdentity,
666 current_count: usize,
667 ) -> Result<(), NativeHistoryError> {
668 while self
669 .candidates
670 .values()
671 .filter(|record| record.key.source != *current)
672 .count()
673 .saturating_add(current_count)
674 > self.config.limits.max_candidates
675 {
676 let evicted = self
677 .source_order
678 .iter()
679 .find(|source| *source != current)
680 .cloned()
681 .or_else(|| {
682 self.candidates
683 .values()
684 .find(|record| record.key.source != *current)
685 .map(|record| record.key.source.clone())
686 })
687 .ok_or(NativeHistoryError::CandidateCapacity)?;
688 self.remove_source(&evicted);
689 }
690 Ok(())
691 }
692
693 fn remove_source(&mut self, source: &SourceIdentity) {
694 let ids = self
695 .candidates
696 .iter()
697 .filter(|(_, record)| record.key.source == *source)
698 .map(|(id, _)| id.clone())
699 .collect::<Vec<_>>();
700 for id in ids {
701 self.remove_candidate(&id);
702 }
703 self.source_order.retain(|candidate| candidate != source);
704 }
705
706 fn fresh_candidate_id(&self) -> String {
707 loop {
708 let id = format!("hist_{}", Uuid::new_v4().simple());
709 if !self.candidates.contains_key(&id) {
710 return id;
711 }
712 }
713 }
714
715 fn remove_candidate(&mut self, id: &str) {
716 if let Some(record) = self.candidates.remove(id) {
717 self.ids_by_key.remove(&record.key);
718 }
719 self.cache.remove(id);
720 self.cache_order.retain(|candidate| candidate != id);
721 }
722}
723
724impl HistoryAuthority for NativeHistoryAuthority {
725 type Error = NativeHistoryError;
726
727 fn discover(
728 &mut self,
729 request: &HistoryDiscoveryRequest,
730 ) -> Result<Vec<HistoryCandidate>, Self::Error> {
731 self.discover_candidates(request)
732 }
733
734 fn load(&mut self, request: &HistoryLoadRequest) -> Result<HistoryDocument, Self::Error> {
735 self.load_candidate(request)
736 }
737}
738
739#[derive(Clone, Debug, Error, Eq, PartialEq)]
740pub enum NativeHistoryConfigError {
741 #[error("history roots must be absolute")]
742 RootMustBeAbsolute,
743 #[error("history adapter is unsupported: {0}")]
744 UnsupportedAdapter(AdapterId),
745 #[error("history adapter ID is unsupported: {0}")]
746 UnsupportedAdapterValue(String),
747 #[error("history layout {layout:?} is not declared by adapter {adapter_id}")]
748 UnsupportedLayout {
749 adapter_id: AdapterId,
750 layout: HistorySourceLayout,
751 },
752 #[error("history root count exceeds the hard bound")]
753 TooManyRoots,
754 #[error("duplicate history root")]
755 DuplicateRoot,
756 #[error("history limit cannot be zero or exceed its hard bound: {0}")]
757 InvalidLimit(&'static str),
758}
759
760#[derive(Clone, Debug, Error, Eq, PartialEq)]
761pub enum NativeHistoryError {
762 #[error("history candidate capacity is exhausted")]
763 CandidateCapacity,
764 #[error("history candidate is invalid")]
765 InvalidCandidate,
766 #[error("history candidate is expired or unknown")]
767 CandidateExpired,
768 #[error("history candidate source does not match the request")]
769 CandidateSourceMismatch,
770 #[error("history source changed or escaped its authorized root")]
771 SourceChanged,
772 #[error("history source could not be read")]
773 ReadFailed,
774 #[error("history source is not valid UTF-8")]
775 InvalidUtf8,
776 #[error("history source document is invalid")]
777 InvalidDocument,
778 #[error("history source exceeds the {max}-byte bound")]
779 FileTooLarge { max: usize },
780 #[error("history SQLite source is unavailable")]
781 DatabaseUnavailable,
782 #[error("history SQLite source has an unsupported schema")]
783 DatabaseSchema,
784}
785
786fn transcript_limit() -> usize {
787 HISTORY_DOCUMENT_MAX_BYTES
788}
789
790fn metadata_limit() -> usize {
791 HISTORY_METADATA_MAX_BYTES
792}