Skip to main content

HistoryService

Struct HistoryService 

Source
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: AgentRecords

AgentMux’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

Source

pub fn new() -> Self

Source

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.

Source

pub fn warm_in_background_after(&self, delay: Duration) -> JoinHandle<()>

Self::warm_in_background with an explicit delay (tests use zero).

Source

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.

Source

fn ensure_built(&self)

Build the index if it never has been. Blocking; for callers without a tight deadline.

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

fn load( &self, candidate: &SearchCandidate, ) -> Result<Option<HistorySession>, HistoryError>

A candidate’s conversation, from wherever it lives.

Source

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.

Source

pub fn get(&self, session_id: &str) -> Value

Get full conversation for a session.

Source

pub fn refresh(&self) -> Value

Re-scan disk and update the index.

Source

pub fn delete(&self, session_id: &str) -> Value

Delete a single session’s on-disk transcript and drop it from the index.

Source

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

Bulk-clear sessions matching the optional provider/project filter.

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