Skip to main content

gate4agent_shell_history/
lib.rs

1//! Native, bounded history discovery/load/cache authority.
2//!
3//! Host paths stay behind opaque candidate IDs. The crate owns filesystem
4//! effects, while `gate4agent-adapters` remains the only owner of provider
5//! transcript semantics.
6
7mod 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
163/// Pinned Orca-compatible roots beneath an explicitly supplied native home.
164///
165/// Environment overrides and WSL/remote homes are intentionally not read here;
166/// an owning shell may append explicit roots after applying its own policy.
167pub 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    /// Builds a provider-wide opaque locator index from configured history roots.
425    ///
426    /// Unlike the public history discovery port, this scan uses the shell-owned
427    /// hard bound so callers can attribute workspaces before applying a UI page
428    /// limit. Transcript bodies are not retained or returned.
429    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}