Skip to main content

SessionIndex

Struct SessionIndex 

Source
pub struct SessionIndex {
Show 14 fields sessions: Mutex<HashMap<String, SessionMeta>>, by_working_dir: Mutex<HashMap<String, Vec<String>>>, by_identity: Mutex<HashMap<String, Vec<String>>>, meta_cache: Mutex<HashMap<String, (i64, Option<SessionMeta>)>>, cache_file: Option<PathBuf>, cache_loaded: AtomicBool, cache_written_at_ms: AtomicI64, pub(super) refresh_lock: Mutex<()>, refreshed_at_ms: AtomicI64, refreshes_started: AtomicU64, last_finished_refresh: AtomicU64, discovery_problems: Mutex<Vec<String>>, adapters: Vec<Box<dyn HistoryAdapter>>, isolated_roots: Vec<PathBuf>,
}
Expand description

In-memory index of discovered sessions.

Fields§

§sessions: Mutex<HashMap<String, SessionMeta>>

session_id -> SessionMeta

§by_working_dir: Mutex<HashMap<String, Vec<String>>>

normalized working directory -> session_ids.

The identity index below can only find sessions for an agent that has a db_agent_identity_links row binding it to the account its transcripts were written under. An agent running on ambient credentials has no such row (and its registry record’s identity_id reads "default"), so for that — the common — case the identity index resolves to nothing at all while the transcripts sit on disk perfectly intact. The working directory is recorded both in the transcript itself (cwd) and on the agent’s own row, which makes it the join key that actually holds for those agents. See docs/specs/SPEC_CROSS_CHANNEL_AGENT_HISTORY_RESOLUTION_2026_09_21.md. LOCK ORDER — sessions is always acquired BEFORE this map, and before by_identity. Never the reverse, anywhere.

refresh() holds the sessions guard while it swaps both index maps in, so it takes sessions -> by_working_dir. A reader that took by_working_dir -> sessions would be a textbook AB-BA deadlock the moment a refresh() (triggered by any list/sessions_for_agent call) overlapped it on the shared Arc<SessionIndex> — thread A holding sessions and blocking on the map, thread B holding the map and blocking on sessions. Both readers below therefore take sessions first, even though reading the map first would read more naturally. reagentx P1 on PR #3480.

§by_identity: Mutex<HashMap<String, Vec<String>>>

identity_id -> session_ids (only non-empty SessionMeta::identity_id values are indexed here). Maintained alongside sessions so “this identity’s sessions” is an O(k) HashMap lookup + small per-identity sort, not an O(total sessions) scan of everything on disk — see list_for_identity and docs/specs/SPEC_AGENT_IDENTITY_HISTORY_PERSISTENCE_PROTOCOL_2026_08_16.md §4.4.

§meta_cache: Mutex<HashMap<String, (i64, Option<SessionMeta>)>>

file path -> (mtime it was parsed at, what it parsed to). A cold refresh JSON-parses every line of every transcript on the machine (42k files / 3.8 GB on one real host), so later refreshes reuse this for files whose mtime hasn’t moved. Only touched under refresh_lock.

§cache_file: Option<PathBuf>

Where meta_cache is kept between launches (with_cache_file), so the first refresh after a start parses only transcripts that changed since, instead of every transcript on the machine (63k sessions took 92–154 s per launch; SPEC_AGENT_OPEN_LATENCY_2026_09_27.md §4.3). None: kept in memory only.

§cache_loaded: AtomicBool

The cache file has been read (once, at the first refresh).

§cache_written_at_ms: AtomicI64

Unix ms when the cache file was last written; 0 = never.

§refresh_lock: Mutex<()>

Serializes refreshes. Held for a whole refresh, never while taking any lock above, so it adds no ordering constraint.

§refreshed_at_ms: AtomicI64

Unix ms when the last refresh finished; 0 = never.

§refreshes_started: AtomicU64

Refreshes started so far; each takes the next number, under refresh_lock, before it lists anything.

§last_finished_refresh: AtomicU64

The number of the last refresh that finished. A request that finds it higher than refreshes_started was when the request began knows a refresh listed the disk after it began — see Self::refresh_covering_now.

§discovery_problems: Mutex<Vec<String>>

What the last refresh couldn’t read (see Discovery::problems), kept with the snapshot it produced.

§adapters: Vec<Box<dyn HistoryAdapter>>

Adapters for all registered providers

§isolated_roots: Vec<PathBuf>

Roots under which destructive ops (delete/clear) are allowed.

Implementations§

Source§

impl SessionIndex

Source

pub fn new(adapters: Vec<Box<dyn HistoryAdapter>>) -> Self

Source

pub fn with_isolated_roots( adapters: Vec<Box<dyn HistoryAdapter>>, isolated_roots: Vec<PathBuf>, ) -> Self

Construct with explicit isolated roots (used by new() and tests).

Source

pub fn with_cache_file(self, path: PathBuf) -> Self

Keep the parsed-metadata cache in path between launches. Read at the first refresh (not here: the file can be tens of MB, and construction runs at srv startup), written after the first build and then at most every META_CACHE_WRITE_INTERVAL_MS, only when something changed.

Source

pub fn refreshed_at_ms(&self) -> i64

Unix ms when the last refresh finished; 0 if none has.

Source

pub fn try_refresh(&self) -> Option<(u32, u32, u32)>

Self::refresh, unless another refresh is already running — then None at once instead of waiting it out.

Source

pub fn try_first_build(&self) -> FirstBuild

The first build, unless one isn’t needed or can’t start now: never waits on the refresh lock, and checks “already built” while holding it (refreshed_at_ms is stored under the lock), so it can’t follow another caller’s first build with a second full pass.

Source

fn load_cache_file_once(&self)

Fill meta_cache from the cache file, once per index, if it is still empty. A missing, unreadable or other-version file is ignored: the refresh then parses everything, as it always did.

Source

fn write_cache_file( &self, cache: &HashMap<String, (i64, Option<SessionMeta>)>, changed: bool, )

Save cache to the cache file when it changed: always after the first build, then at most every META_CACHE_WRITE_INTERVAL_MS (searches also refresh, and the file is tens of MB). Written to a temporary file and renamed over the old one, so a reader (another srv instance) never sees half a file. Failures are logged, never fatal: the cache only saves time.

Source

fn is_isolated(&self, path: &Path) -> bool

True if path lives under an AgentMux-isolated provider home and is therefore safe to delete. Personal global homes are never isolated.

Source

pub fn refresh(&self) -> (u32, u32, u32)

Full scan: discover all files and extract metadata. Returns (discovered, updated, new) counts.

Source

pub fn refresh_covering_now(&self)

Make the snapshot include every session on disk when this call began, sharing the work with concurrent callers. A refresh that started after this call began already listed everything this call needs, so once it has finished there’s nothing left to do; otherwise this runs its own. However many searches arrive together, that’s at most two walks — every search running its own, one after another, put the fourth past the MCP’s 10 s timeout on a heavy host (Codex P1 on #3693).

Source

pub fn discovery_problems(&self) -> Vec<String>

What the last refresh couldn’t read — a session there may be missing from the snapshot.

Source

fn refresh_holding(&self, _refreshing: MutexGuard<'_, ()>) -> (u32, u32, u32)

Source

pub fn list_for_working_directory( &self, working_directory: &str, ) -> Vec<SessionMeta>

Every session recorded as having run in working_directory.

Matching is on the normalized full path (normalize_working_dir), never a substring: list’s project filter uses contains, which would make an agent in /agents/foo also claim every session from /agents/foo-2.

Source

pub fn list_for_identity( &self, identity_id: &str, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, ) -> (Vec<SessionMeta>, u32, bool)

This identity’s sessions, sorted/paginated the same way list is — but via the by_identity index instead of scanning every session on disk. identity_id is a bundle/account id (SessionMeta::identity_id — see its own doc comment); resolving an agent_id to one is the caller’s job (Store::agent_identity_list_for_agent), not this index’s — a bundle is a filesystem fact, which agent(s) claim it is a database fact, and this module has no Store access by design (it’s pure discovery/indexing over the filesystem).

Source

pub(crate) fn sort_sessions( filtered: &mut [&SessionMeta], sort_by: &str, sort_dir: &str, )

Shared sort logic between list (scans everything) and list_for_identity (scans one identity’s sessions) — extracted so the two can’t silently diverge on sort semantics.

Source

pub fn list( &self, provider: Option<&str>, project: Option<&str>, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, ) -> (Vec<SessionMeta>, u32, bool)

List sessions with pagination and optional filters.

Source

pub fn get_meta(&self, session_id: &str) -> Option<SessionMeta>

Get a session by ID — returns just the meta from index.

Source

pub fn get_full( &self, session_id: &str, ) -> Result<Option<HistorySession>, HistoryError>

Full parse of a session by ID.

Source

pub fn delete(&self, session_id: &str) -> Result<bool, HistoryError>

Delete a session: remove its on-disk transcript (and the sibling <session_id>/ subagents dir Claude keeps next to it) and drop it from the index. Returns Ok(true) if a file was removed, Ok(false) if the session id is unknown.

Source

pub fn clear(&self, provider: Option<&str>, project: Option<&str>) -> u32

Bulk-delete all indexed sessions matching the optional provider/project filter (no filter = clear everything). Returns the number removed.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more