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: AtomicBoolThe cache file has been read (once, at the first refresh).
cache_written_at_ms: AtomicI64Unix 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: AtomicI64Unix ms when the last refresh finished; 0 = never.
refreshes_started: AtomicU64Refreshes started so far; each takes the next number, under
refresh_lock, before it lists anything.
last_finished_refresh: AtomicU64The 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
impl SessionIndex
pub fn new(adapters: Vec<Box<dyn HistoryAdapter>>) -> Self
Sourcepub fn with_isolated_roots(
adapters: Vec<Box<dyn HistoryAdapter>>,
isolated_roots: Vec<PathBuf>,
) -> Self
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).
Sourcepub fn with_cache_file(self, path: PathBuf) -> Self
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.
Sourcepub fn refreshed_at_ms(&self) -> i64
pub fn refreshed_at_ms(&self) -> i64
Unix ms when the last refresh finished; 0 if none has.
Sourcepub fn try_refresh(&self) -> Option<(u32, u32, u32)>
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.
Sourcepub fn try_first_build(&self) -> FirstBuild
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.
Sourcefn load_cache_file_once(&self)
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.
Sourcefn write_cache_file(
&self,
cache: &HashMap<String, (i64, Option<SessionMeta>)>,
changed: bool,
)
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.
Sourcefn is_isolated(&self, path: &Path) -> bool
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.
Sourcepub fn refresh(&self) -> (u32, u32, u32)
pub fn refresh(&self) -> (u32, u32, u32)
Full scan: discover all files and extract metadata. Returns (discovered, updated, new) counts.
Sourcepub fn refresh_covering_now(&self)
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).
Sourcepub fn discovery_problems(&self) -> Vec<String>
pub fn discovery_problems(&self) -> Vec<String>
What the last refresh couldn’t read — a session there may be missing from the snapshot.
fn refresh_holding(&self, _refreshing: MutexGuard<'_, ()>) -> (u32, u32, u32)
Sourcepub fn list_for_working_directory(
&self,
working_directory: &str,
) -> Vec<SessionMeta>
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.
Sourcepub fn list_for_identity(
&self,
identity_id: &str,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
) -> (Vec<SessionMeta>, u32, bool)
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).
Sourcepub(crate) fn sort_sessions(
filtered: &mut [&SessionMeta],
sort_by: &str,
sort_dir: &str,
)
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.
Sourcepub fn list(
&self,
provider: Option<&str>,
project: Option<&str>,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
) -> (Vec<SessionMeta>, u32, bool)
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.
Sourcepub fn get_meta(&self, session_id: &str) -> Option<SessionMeta>
pub fn get_meta(&self, session_id: &str) -> Option<SessionMeta>
Get a session by ID — returns just the meta from index.
Sourcepub fn get_full(
&self,
session_id: &str,
) -> Result<Option<HistorySession>, HistoryError>
pub fn get_full( &self, session_id: &str, ) -> Result<Option<HistorySession>, HistoryError>
Full parse of a session by ID.
Auto Trait Implementations§
impl !Freeze for SessionIndex
impl !RefUnwindSafe for SessionIndex
impl !UnwindSafe for SessionIndex
impl Send for SessionIndex
impl Sync for SessionIndex
impl Unpin for SessionIndex
impl UnsafeUnpin for SessionIndex
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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