pub struct HistoryService {
index: Arc<SessionIndex>,
records: AgentRecords,
warm_pending: Arc<AtomicBool>,
warm_started_early: Arc<AtomicBool>,
}Expand description
The history service exposed to the RPC layer.
Fields§
§index: Arc<SessionIndex>§records: AgentRecordsAgentMux’s own record of each agent’s conversations — which sessions are whose, and the content of those whose transcript is gone.
warm_pending: Arc<AtomicBool>A deferred first build is scheduled and hasn’t finished yet.
warm_started_early: Arc<AtomicBool>A search already started the pending first build early.
Implementations§
Source§impl HistoryService
impl HistoryService
pub fn new() -> Self
Sourcepub fn warm_in_background(&self) -> JoinHandle<()>
pub fn warm_in_background(&self) -> JoinHandle<()>
Build the index off-thread at srv start, so the first search doesn’t pay for parsing every transcript on the machine inside the MCP’s 10 s timeout (SPEC_DURABLE_CONVERSATION_MEMORY_2026_09_23.md §4.5).
Deferred by WARM_DELAY and run at background priority: the first
build reads every transcript on the machine (61,066 sessions in 23.5 s
on one host) and used to start the moment srv did — exactly when the
user opens their first agents, competing with them for CPU and disk
(SPEC_AGENT_OPEN_LATENCY_2026_09_27.md F2). A search in the meantime
answers “index building”, as it does during the build itself, and
starts the build now; the History tab’s list/get still build on
demand.
Sourcepub fn warm_in_background_after(&self, delay: Duration) -> JoinHandle<()>
pub fn warm_in_background_after(&self, delay: Duration) -> JoinHandle<()>
Self::warm_in_background with an explicit delay (tests use zero).
Sourcefn start_pending_build_early(&self)
fn start_pending_build_early(&self)
A search arrived while the deferred first build is still waiting out
WARM_DELAY: someone wants the index now, so build it now, off the
search’s thread. Once.
Sourcefn ensure_built(&self)
fn ensure_built(&self)
Build the index if it never has been. Blocking; for callers without a tight deadline.
Sourcefn refresh_for_search(&self) -> Result<(), HistorySearchError>
fn refresh_for_search(&self) -> Result<(), HistorySearchError>
Bring the index up to date before a search, so that “no hits, complete” can mean nothing was there: every session on disk when the search began is in the snapshot it reads. Reusing a recent snapshot (it used to be reused for 30 s, or whenever another refresh was running) let a session written in that window go unseen while the answer claimed to be complete (Codex P1 on #3693).
An incremental refresh re-lists the transcript directories and
re-parses only files whose mtime moved: 51,671 files stat’d in 2.7 s on
one heavy host (2026-09-24), far less on most. A refresh already
running is waited for and then followed by this search’s own (unless
another search’s refresh began after this one did), since it may have
started before this search’s sessions were written. Only the
very first build answers “still building” instead of waiting: it reads
every transcript once and would outlast the MCP’s 10 s timeout.
Concurrent searches share refreshes (SessionIndex::refresh_covering_now).
Sourcepub fn list(
&self,
provider: Option<&str>,
project: Option<&str>,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
) -> Value
pub fn list( &self, provider: Option<&str>, project: Option<&str>, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, ) -> Value
List sessions with pagination and filters. Lazy-initializes the index on first call.
Sourcepub fn sessions_for_agent(
&self,
store: &Store,
agent_id: &str,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
force_refresh: bool,
) -> Result<(Vec<SessionMeta>, u32, bool), String>
pub fn sessions_for_agent( &self, store: &Store, agent_id: &str, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, force_refresh: bool, ) -> Result<(Vec<SessionMeta>, u32, bool), String>
Typed core of list_for_agent — the JSON-returning method wraps
this. Exists as its own function so other backend code (e.g.
bundle.rs’s bundle.export_for_agent_with_history) can get this
agent’s SessionMeta list directly, without a JSON
serialize/deserialize round-trip through the RPC-facing shape.
force_refresh: reagentx P1 on PR #2613 — the lazy
refresh-only-if-empty behavior (shared with list/get, fine for
an interactive browse where slight staleness is an acceptable
trade for speed) is wrong for a caller claiming completeness, like
bundle.export_for_agent_with_history: once the index has been
populated once (by ANY prior call, interactive or otherwise), a
session created since then would silently be missing from an
export that reports itself as the full record. list_for_agent
(the interactive RPC) passes false, preserving its existing
speed/freshness trade-off; the export path passes true.
Sourcepub fn sessions_for_owner(
&self,
store: &Store,
owner: &HistoryOwner,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
force_refresh: bool,
) -> Result<(Vec<SessionMeta>, u32, bool), String>
pub fn sessions_for_owner( &self, store: &Store, owner: &HistoryOwner, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, force_refresh: bool, ) -> Result<(Vec<SessionMeta>, u32, bool), String>
Self::sessions_for_agent for an owner already resolved — identity
M4c-2c: an attributed caller’s own row (HistoryOwner::of_row),
never a slug another agent may share.
Sourcepub fn search_for_agent(
&self,
store: &Store,
owner: &HistoryOwner,
opts: &HistorySearchOptions,
) -> Result<HistorySearchOutcome, HistorySearchError>
pub fn search_for_agent( &self, store: &Store, owner: &HistoryOwner, opts: &HistorySearchOptions, ) -> Result<HistorySearchOutcome, HistorySearchError>
Search this agent’s OWN conversation history.
SPEC_AGENT_HISTORY_SEARCH_2026_09_17.md. Closes the gap where an
agent’s memory of what it did is bounded by its current context window
while its actual actions are not — so a compaction or session reset
leaves it confidently misreporting its own past, with nothing marking
the boundary. Questions like “did I already send that message” are
answerable from disk; before this they were not answerable by the agent
being asked.
Own history only. Searching another agent’s transcript is already a
governed act — conversation_visibility plus the transcript_request
tier rules decide whether one agent may read another’s content. A
search verb that read other agents’ sessions directly would be a
second, ungoverned disclosure path around that machinery, and a more
dangerous one for looking like an ordinary read tool. Cross-agent
search must route through transcript_request, as its own phase.
opts.since_ms/until_ms drop whole sessions on indexed metadata
before any file is opened, so a narrow time window costs nothing on
irrelevant sessions, and then filter the messages of the sessions that
remain. opts.max_sessions bounds how many of those are parsed, newest
first; the rest are counted, not dropped.
Sourcefn load(
&self,
candidate: &SearchCandidate,
) -> Result<Option<HistorySession>, HistoryError>
fn load( &self, candidate: &SearchCandidate, ) -> Result<Option<HistorySession>, HistoryError>
A candidate’s conversation, from wherever it lives.
Sourcepub fn list_for_agent(
&self,
store: &Store,
agent_id: &str,
offset: usize,
limit: usize,
sort_by: &str,
sort_dir: &str,
) -> Value
pub fn list_for_agent( &self, store: &Store, agent_id: &str, offset: usize, limit: usize, sort_by: &str, sort_dir: &str, ) -> Value
This agent’s own sessions — the actual “fast Conversation History
lookup” protocol §4.4 asks for, resolving agent_id to its bound
account(s) (Store::agent_identity_list_for_agent, the
same db_agent_identity_links table identity_auth_dirs.rs
already keys off) and querying SessionIndex::list_for_identity’s
O(sessions for this identity) HashMap-backed path instead of
list()’s O(total sessions on disk) scan. An agent normally has
at most one linked account per provider, but this merges across
however many exist rather than assuming exactly one.
Auto Trait Implementations§
impl !Freeze for HistoryService
impl !RefUnwindSafe for HistoryService
impl !UnwindSafe for HistoryService
impl Send for HistoryService
impl Sync for HistoryService
impl Unpin for HistoryService
impl UnsafeUnpin for HistoryService
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